[vm] De-obfuscate function and file names in DWARF sections.

Note that when generating unstripped ELF snapshots with --obfuscate,
this means DWARF sections in the resulting snapshot will leak
otherwise obfuscated information. To avoid this, use both --strip
to strip the generated snapshot and --save-debugging-info=<...> to
save the unobfuscated DWARF information to a separate file.

Fixes https://github.com/dart-lang/sdk/issues/35563.

Change-Id: I8795e2a5623ad5fc5362257007f677e622a7700b
Cq-Include-Trybots: luci.dart.try:vm-kernel-precomp-linux-release-x64-try,vm-kernel-precomp-android-release-arm64-try,vm-kernel-precomp-mac-release-simarm_x64-try,vm-kernel-precomp-win-release-x64-try
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/127166
Commit-Queue: Teagan Strickland <sstrickl@google.com>
Reviewed-by: Martin Kustermann <kustermann@google.com>
This commit is contained in:
Teagan Strickland 2019-12-12 17:54:20 +00:00 committed by commit-bot@chromium.org
parent 3389b3bd32
commit 4b8fd3c412
5 changed files with 294 additions and 34 deletions

View file

@ -648,6 +648,13 @@ static void CreateAndWritePrecompiledSnapshot() {
/*debug_callback_data=*/nullptr);
}
CHECK_RESULT(result);
if (obfuscate && !strip) {
Syslog::PrintErr(
"Warning: The generated ELF library contains unobfuscated DWARF "
"debugging information.\n"
" To avoid this, use --strip to remove it and "
"--save-debugging-info=<...> to save it to a separate file.\n");
}
} else if (snapshot_kind == kAppAOTBlobs) {
Syslog::PrintErr(
"WARNING: app-aot-blobs snapshots have been deprecated and support for "

View file

@ -55,9 +55,53 @@ class InliningNode : public ZoneAllocated {
InliningNode* children_next;
};
template <typename T>
Trie<T>* Trie<T>::AddString(Zone* zone,
Trie<T>* trie,
const char* key,
const T* value) {
ASSERT(key != nullptr);
if (trie == nullptr) {
trie = new (zone) Trie<T>();
}
if (*key == '\0') {
ASSERT(trie->value_ == nullptr);
trie->value_ = value;
} else {
auto const index = ChildIndex(*key);
ASSERT(index >= 0 && index < kNumValidChars);
trie->children_[index] =
AddString(zone, trie->children_[index], key + 1, value);
}
return trie;
}
template <typename T>
const T* Trie<T>::Lookup(const Trie<T>* trie, const char* key, intptr_t* end) {
intptr_t i = 0;
for (; key[i] != '\0'; i++) {
auto const index = ChildIndex(key[i]);
ASSERT(index < kNumValidChars);
if (index < 0) {
if (end == nullptr) return nullptr;
break;
}
// Still find the longest valid trie prefix when no stored value.
if (trie == nullptr) continue;
trie = trie->children_[index];
}
if (end != nullptr) {
*end = i;
}
if (trie == nullptr) return nullptr;
return trie->value_;
}
Dwarf::Dwarf(Zone* zone, StreamingWriteStream* stream, Elf* elf)
: zone_(zone),
elf_(elf),
reverse_obfuscation_trie_(CreateReverseObfuscationTrie(zone)),
asm_stream_(stream),
bin_stream_(nullptr),
codes_(zone, 1024),
@ -357,8 +401,8 @@ void Dwarf::WriteAbstractFunctions() {
const Function& function = *(functions_[i]);
name = function.QualifiedUserVisibleName();
script = function.script();
intptr_t file = LookupScript(script);
intptr_t line = 0; // Not known. Script has already lost its token stream.
const intptr_t file = LookupScript(script);
const intptr_t line = 0; // Unknown, script already lost its token stream.
if (asm_stream_) {
Print(".Lfunc%" Pd ":\n",
@ -366,12 +410,14 @@ void Dwarf::WriteAbstractFunctions() {
} else {
abstract_origins_[i] = position();
}
auto const name_cstr = Deobfuscate(name.ToCString());
uleb128(kAbstractFunction);
string(name.ToCString()); // DW_AT_name
uleb128(file); // DW_AT_decl_file
uleb128(line); // DW_AT_decl_line
uleb128(DW_INL_inlined); // DW_AT_inline
uleb128(0); // End of children.
string(name_cstr); // DW_AT_name
uleb128(file); // DW_AT_decl_file
uleb128(line); // DW_AT_decl_line
uleb128(DW_INL_inlined); // DW_AT_inline
uleb128(0); // End of children.
}
}
@ -637,8 +683,10 @@ void Dwarf::WriteLines() {
for (intptr_t i = 0; i < scripts_.length(); i++) {
const Script& script = *(scripts_[i]);
uri = script.url();
RELEASE_ASSERT(strlen(uri.ToCString()) != 0);
string(uri.ToCString()); // NOLINT
auto const uri_cstr = Deobfuscate(uri.ToCString());
RELEASE_ASSERT(strlen(uri_cstr) != 0);
string(uri_cstr); // NOLINT
uleb128(0); // Include directory index.
uleb128(0); // File modification time.
uleb128(0); // File length.
@ -825,6 +873,50 @@ void Dwarf::WriteLines() {
}
}
const char* Dwarf::Deobfuscate(const char* cstr) {
if (reverse_obfuscation_trie_ == nullptr) return cstr;
TextBuffer buffer(256);
// Used to avoid Zone-allocating strings if no deobfuscation was performed.
bool changed = false;
intptr_t i = 0;
while (cstr[i] != '\0') {
intptr_t offset;
auto const value = reverse_obfuscation_trie_->Lookup(cstr + i, &offset);
if (offset == 0) {
// The first character was an invalid key element (that isn't the null
// terminator due to the while condition), copy it and skip to the next.
buffer.AddChar(cstr[i++]);
} else if (value != nullptr) {
changed = true;
buffer.AddString(value);
} else {
buffer.AddRaw(reinterpret_cast<const uint8_t*>(cstr + i), offset);
}
i += offset;
}
if (!changed) return cstr;
return OS::SCreate(zone_, "%s", buffer.buf());
}
Trie<const char>* Dwarf::CreateReverseObfuscationTrie(Zone* zone) {
auto const I = Thread::Current()->isolate();
auto const map_array = I->obfuscation_map();
if (map_array == nullptr) return nullptr;
Trie<const char>* trie = nullptr;
for (intptr_t i = 0; map_array[i] != nullptr; i += 2) {
auto const key = map_array[i];
auto const value = map_array[i + 1];
ASSERT(value != nullptr);
// Don't include identity mappings.
if (strcmp(key, value) == 0) continue;
// Otherwise, any value in the obfuscation map should be a valid key.
ASSERT(Trie<const char>::IsValidKey(value));
trie = Trie<const char>::AddString(zone, trie, value, key);
}
return trie;
}
#endif // DART_PRECOMPILER
} // namespace dart

View file

@ -117,6 +117,80 @@ struct CodeIndexPair {
typedef DirectChainedHashMap<CodeIndexPair> CodeIndexMap;
template <typename T>
class Trie : public ZoneAllocated {
public:
// Returns whether [key] is a valid trie key (that is, a C string that
// contains only characters for which charIndex returns a non-negative value).
static bool IsValidKey(const char* key) {
for (intptr_t i = 0; key[i] != '\0'; i++) {
if (ChildIndex(key[i]) < 0) return false;
}
return true;
}
// Adds a binding of [key] to [value] in [trie]. Assumes that the string in
// [key] is a valid trie key and does not already have a value in [trie].
//
// If [trie] is nullptr, then a new trie is created and a pointer to the new
// trie is returned. Otherwise, [trie] will be returned.
static Trie<T>* AddString(Zone* zone,
Trie<T>* trie,
const char* key,
const T* value);
// Adds a binding of [key] to [value]. Assumes that the string in [key] is a
// valid trie key and does not already have a value in this trie.
void AddString(Zone* zone, const char* key, const T* value) {
AddString(zone, this, key, value);
}
// Looks up the value stored for [key] in [trie]. If one is not found, then
// nullptr is returned.
//
// If [end] is not nullptr, then the longest prefix of [key] that is a valid
// trie key prefix will be used for the lookup and the value pointed to by
// [end] is set to the index after that prefix. Otherwise, the whole [key]
// is used.
static const T* Lookup(const Trie<T>* trie,
const char* key,
intptr_t* end = nullptr);
// Looks up the value stored for [key]. If one is not found, then nullptr is
// returned.
//
// If [end] is not nullptr, then the longest prefix of [key] that is a valid
// trie key prefix will be used for the lookup and the value pointed to by
// [end] is set to the index after that prefix. Otherwise, the whole [key]
// is used.
const T* Lookup(const char* key, intptr_t* end = nullptr) const {
return Lookup(this, key, end);
}
private:
// Currently, only the following characters can appear in obfuscated names:
// '_', '@', '0-9', 'a-z', 'A-Z'
static const intptr_t kNumValidChars = 64;
Trie() {
for (intptr_t i = 0; i < kNumValidChars; i++) {
children_[i] = nullptr;
}
}
static intptr_t ChildIndex(char c) {
if (c == '_') return 0;
if (c == '@') return 1;
if (c >= '0' && c <= '9') return ('9' - c) + 2;
if (c >= 'a' && c <= 'z') return ('z' - c) + 12;
if (c >= 'A' && c <= 'Z') return ('Z' - c) + 38;
return -1;
}
const T* value_ = nullptr;
Trie<T>* children_[kNumValidChars];
};
class Dwarf : public ZoneAllocated {
public:
Dwarf(Zone* zone, StreamingWriteStream* stream, Elf* elf);
@ -302,8 +376,12 @@ class Dwarf : public ZoneAllocated {
AssemblyCodeNamer* namer);
void WriteLines();
const char* Deobfuscate(const char* cstr);
static Trie<const char>* CreateReverseObfuscationTrie(Zone* zone);
Zone* const zone_;
Elf* const elf_;
Trie<const char>* const reverse_obfuscation_trie_;
StreamingWriteStream* asm_stream_;
WriteStream* bin_stream_;
ZoneGrowableArray<const Code*> codes_;

View file

@ -0,0 +1,79 @@
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
/// VMOptions=--dwarf-stack-traces --save-debugging-info=dwarf_obfuscate.so --obfuscate
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:vm/dwarf/dwarf.dart';
import 'dwarf_stack_trace_test.dart' as base;
@pragma("vm:prefer-inline")
bar() {
// Keep the 'throw' and its argument on separate lines.
throw // force linebreak with dartfmt
"Hello, Dwarf!";
}
@pragma("vm:never-inline")
foo() {
bar();
}
Future<void> main() async {
String rawStack;
try {
foo();
} catch (e, st) {
rawStack = st.toString();
}
if (path.basenameWithoutExtension(Platform.executable) !=
"dart_precompiled_runtime") {
return; // Not running from an AOT compiled snapshot.
}
if (Platform.isAndroid) {
return; // Generated dwarf.so not available on the test device.
}
final dwarf = Dwarf.fromFile("dwarf_obfuscate.so");
await base.checkStackTrace(rawStack, dwarf, expectedCallsInfo);
}
final expectedCallsInfo = <List<CallInfo>>[
// The first frame should correspond to the throw in bar, which was inlined
// into foo (so we'll get information for two calls for that PC address).
[
CallInfo(
function: "bar",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 17,
inlined: true),
CallInfo(
function: "foo",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 23,
inlined: false)
],
// The second frame corresponds to call to foo in main.
[
CallInfo(
function: "main",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 29,
inlined: false)
],
// Internal frames have non-positive line numbers in the call information.
[
CallInfo(
function: "main",
filename: "dwarf_stack_trace_obfuscate_test.dart",
line: 0,
inlined: false),
]
];

View file

@ -7,10 +7,10 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:unittest/unittest.dart';
import 'package:vm/dwarf/convert.dart';
import 'package:vm/dwarf/dwarf.dart';
import 'package:path/path.dart' as path;
@pragma("vm:prefer-inline")
bar() {
@ -32,14 +32,8 @@ Future<void> main() async {
rawStack = st.toString();
}
// Check that our expected information is consistent.
checkConsistency(expectedExternalCallInfo, expectedAllCallsInfo);
print("");
print("Raw stack trace:");
print(rawStack);
if (!Platform.executable.endsWith("dart_precompiled_runtime")) {
if (path.basenameWithoutExtension(Platform.executable) !=
"dart_precompiled_runtime") {
return; // Not running from an AOT compiled snapshot.
}
@ -49,6 +43,18 @@ Future<void> main() async {
final dwarf = Dwarf.fromFile("dwarf.so");
await checkStackTrace(rawStack, dwarf, expectedCallsInfo);
}
Future<void> checkStackTrace(String rawStack, Dwarf dwarf,
List<List<CallInfo>> expectedCallsInfo) async {
final expectedAllCallsInfo = expectedCallsInfo;
final expectedExternalCallInfo = removeInternalCalls(expectedCallsInfo);
print("");
print("Raw stack trace:");
print(rawStack);
var rawLines =
await Stream.value(rawStack).transform(const LineSplitter()).toList();
@ -131,7 +137,7 @@ Future<void> main() async {
expect(allCallsTrace, stringContainsInOrder(expectedStrings));
}
final expectedExternalCallInfo = <List<CallInfo>>[
final expectedCallsInfo = <List<CallInfo>>[
// The first frame should correspond to the throw in bar, which was inlined
// into foo (so we'll get information for two calls for that PC address).
[
@ -140,13 +146,13 @@ final expectedExternalCallInfo = <List<CallInfo>>[
filename: "dwarf_stack_trace_test.dart",
line: 18,
inlined: true),
// The second frame corresponds to call to foo in main.
CallInfo(
function: "foo",
filename: "dwarf_stack_trace_test.dart",
line: 24,
inlined: false)
],
// The second frame corresponds to call to foo in main.
[
CallInfo(
function: "main",
@ -154,22 +160,20 @@ final expectedExternalCallInfo = <List<CallInfo>>[
line: 30,
inlined: false)
],
// No call information for the main tearoff.
[],
// Internal frames have non-positive line numbers in the call information.
[
CallInfo(
function: "main",
filename: "dwarf_stack_trace_test.dart",
line: 0,
inlined: false),
]
];
// Replace the call information for the main tearoff frame.
final expectedAllCallsInfo = expectedExternalCallInfo.sublist(0, 2) +
<List<CallInfo>>[
// Internal frames have non-positive line numbers in the call information.
[
CallInfo(
function: "main",
filename: "dwarf_stack_trace_test.dart",
line: 0,
inlined: false),
]
];
List<List<CallInfo>> removeInternalCalls(List<List<CallInfo>> original) =>
original
.map((frame) => frame.where((call) => call.line > 0).toList())
.toList();
void checkConsistency(
List<List<CallInfo>> externalFrames, List<List<CallInfo>> allFrames) {