Add compact AOT patching support and related APIs
- Introduced Dart_SetObfuscationMap to restore obfuscation maps before AOT precompilation. - Added Dart_AotPatchInstallOptions structure for AOT patch installation options. - Implemented Dart_AotPatchingEnabled to check if compact AOT patching is supported. - Created Dart_SetAotPatchKeyCallback for AES key resolution during AOT patch installation. - Developed Dart_InstallAotPatch for validating and installing encrypted AOT patches. - Added Dart_FreeAotPatchPayload to free memory allocated for patch payloads. - Updated runtime_args.gni to include dart_enable_aot_patching flag. - Added tests for AOT patching functionality and ensured exported symbols include new APIs. - Refactored existing code to accommodate new AOT patching features and improve error handling.
This commit is contained in:
@@ -633,6 +633,12 @@ if (is_win) {
|
||||
default_warning_flags += [ "/WX" ] # Treat warnings as errors.
|
||||
}
|
||||
|
||||
# Ensure UTF-8 source encoding on Chinese Windows (codepage 936)
|
||||
default_warning_flags += [
|
||||
"/utf-8",
|
||||
"/wd4819",
|
||||
]
|
||||
|
||||
if (is_clang) {
|
||||
default_warning_flags += [
|
||||
"-Wno-deprecated-declarations", # crashpad
|
||||
|
||||
@@ -17,6 +17,11 @@ import sys
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
if hasattr(sys.stdout, 'reconfigure'):
|
||||
sys.stdout.reconfigure(errors='replace')
|
||||
if hasattr(sys.stderr, 'reconfigure'):
|
||||
sys.stderr.reconfigure(errors='replace')
|
||||
|
||||
# A regex matching an argument corresponding to the output filename passed to
|
||||
# link.exe.
|
||||
_LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE)
|
||||
@@ -142,7 +147,8 @@ class WinTool(object):
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
universal_newlines=True)
|
||||
universal_newlines=True,
|
||||
errors='replace')
|
||||
# Read output one line at a time as it shows up to avoid OOM failures when
|
||||
# GBs of output is produced.
|
||||
for line in link.stdout:
|
||||
|
||||
+20
-7
@@ -67,6 +67,7 @@ SDK_VERSION = '10.0.26100.0'
|
||||
# which makes a difference for the arm64 runtime.
|
||||
# The second number is an alternate version number, only used in an error string
|
||||
MSVS_VERSIONS = collections.OrderedDict([
|
||||
('2026', '18.0'), # VS 2026 (user-installed) - uses folder name '18'
|
||||
('2022', '17.0'), # The VS version in our packaged toolchain.
|
||||
('2019', '16.0'),
|
||||
('2017', '15.0'),
|
||||
@@ -75,11 +76,20 @@ MSVS_VERSIONS = collections.OrderedDict([
|
||||
# List of preferred VC toolset version based on MSVS
|
||||
# Order is not relevant for this dictionary.
|
||||
MSVC_TOOLSET_VERSION = {
|
||||
'2026': 'VC145',
|
||||
'2022': 'VC143',
|
||||
'2019': 'VC142',
|
||||
'2017': 'VC141',
|
||||
}
|
||||
|
||||
|
||||
def VisualStudioFolderName(version_as_year):
|
||||
"""Return the install folder name for a supported Visual Studio version."""
|
||||
if version_as_year >= '2026':
|
||||
return MSVS_VERSIONS.get(version_as_year, version_as_year).split('.')[0]
|
||||
return version_as_year
|
||||
|
||||
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
json_data_file = os.path.join(script_dir, 'win_toolchain.json')
|
||||
|
||||
@@ -212,7 +222,8 @@ def GetVisualStudioVersion():
|
||||
else:
|
||||
program_files_path_variable = '%ProgramFiles(x86)%'
|
||||
path = os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s' % version)
|
||||
'/Microsoft Visual Studio/%s' %
|
||||
VisualStudioFolderName(version))
|
||||
if path and any(
|
||||
os.path.exists(os.path.join(path, edition))
|
||||
for edition in ('Enterprise', 'Professional', 'Community', 'Preview',
|
||||
@@ -242,24 +253,25 @@ def DetectVisualStudioPath():
|
||||
program_files_path_variable = '%ProgramFiles%'
|
||||
else:
|
||||
program_files_path_variable = '%ProgramFiles(x86)%'
|
||||
folder_name = VisualStudioFolderName(version_as_year)
|
||||
for path in (os.environ.get('vs%s_install' % version_as_year),
|
||||
os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s/Enterprise' %
|
||||
version_as_year),
|
||||
folder_name),
|
||||
os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s/Professional' %
|
||||
version_as_year),
|
||||
folder_name),
|
||||
os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s/Community' %
|
||||
version_as_year),
|
||||
folder_name),
|
||||
os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s/Preview' %
|
||||
version_as_year),
|
||||
folder_name),
|
||||
os.path.expandvars(program_files_path_variable +
|
||||
'/Microsoft Visual Studio/%s/BuildTools' %
|
||||
version_as_year)):
|
||||
folder_name)):
|
||||
if path and os.path.exists(path):
|
||||
return path
|
||||
return path.strip()
|
||||
|
||||
raise Exception('Visual Studio Version %s not found.' % version_as_year)
|
||||
|
||||
@@ -562,6 +574,7 @@ def Update(force=False, no_download=False):
|
||||
|
||||
|
||||
def NormalizePath(path):
|
||||
path = path.strip()
|
||||
while path.endswith('\\'):
|
||||
path = path[:-1]
|
||||
return path
|
||||
|
||||
@@ -38,7 +38,11 @@ void alsoVerySecretFoo() {
|
||||
}
|
||||
""");
|
||||
|
||||
List<MappingPair> mapping = getSnapshotMap(tmpDir, secretfilenameFile);
|
||||
List<MappingPair> mapping = getSnapshotMap(
|
||||
tmpDir,
|
||||
secretfilenameFile,
|
||||
outputPrefix: "base",
|
||||
);
|
||||
bool good = verify(
|
||||
mapping,
|
||||
{
|
||||
@@ -55,13 +59,51 @@ void alsoVerySecretFoo() {
|
||||
},
|
||||
);
|
||||
if (!good) throw "Obfuscation didn't work as expected";
|
||||
|
||||
secretfilename2File.writeAsStringSync("""
|
||||
@pragma('vm:entry-point')
|
||||
void verySecretFoo() {
|
||||
print("foo!");
|
||||
alsoVerySecretFoo();
|
||||
newPatchOnlySecretFoo();
|
||||
}
|
||||
|
||||
void alsoVerySecretFoo() {
|
||||
print("foo too!");
|
||||
}
|
||||
|
||||
void newPatchOnlySecretFoo() {
|
||||
print("patched foo!");
|
||||
}
|
||||
""");
|
||||
|
||||
final File baseObfuscationMapFile = new File.fromUri(
|
||||
tmpDirUri.resolve("obfuscation-base.map"),
|
||||
);
|
||||
List<MappingPair> patchMapping = getSnapshotMap(
|
||||
tmpDir,
|
||||
secretfilenameFile,
|
||||
outputPrefix: "patch",
|
||||
loadObfuscationMapFile: baseObfuscationMapFile,
|
||||
);
|
||||
good = verifyLoadedObfuscationMap(
|
||||
mapping,
|
||||
patchMapping,
|
||||
"newPatchOnlySecretFoo",
|
||||
);
|
||||
if (!good) throw "Loaded obfuscation map didn't work as expected";
|
||||
print("Good");
|
||||
} finally {
|
||||
tmpDir.deleteSync(recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
|
||||
List<MappingPair> getSnapshotMap(
|
||||
Directory tmpDir,
|
||||
File compileDartFile, {
|
||||
required String outputPrefix,
|
||||
File? loadObfuscationMapFile,
|
||||
}) {
|
||||
final Uri genKernel = Platform.script.resolve('../bin/gen_kernel.dart');
|
||||
final File genKernelFile = new File.fromUri(genKernel);
|
||||
if (!genKernelFile.existsSync()) {
|
||||
@@ -96,7 +138,7 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
|
||||
|
||||
final Uri tmpDirUri = tmpDir.uri;
|
||||
|
||||
final Uri kernelDill = tmpDirUri.resolve("kernel.dill");
|
||||
final Uri kernelDill = tmpDirUri.resolve("kernel-$outputPrefix.dill");
|
||||
final File kernelDillFile = new File.fromUri(kernelDill);
|
||||
|
||||
print("Running gen_kernel");
|
||||
@@ -120,14 +162,14 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
|
||||
"stderr: ${kernelRun.stderr}";
|
||||
}
|
||||
|
||||
final Uri aotElf = tmpDirUri.resolve("aot.elf");
|
||||
final Uri aotElf = tmpDirUri.resolve("aot-$outputPrefix.elf");
|
||||
final File aotElfFile = new File.fromUri(aotElf);
|
||||
final Uri obfuscationMap = tmpDirUri.resolve("obfuscation.map");
|
||||
final Uri obfuscationMap = tmpDirUri.resolve("obfuscation-$outputPrefix.map");
|
||||
final File obfuscationMapFile = new File.fromUri(obfuscationMap);
|
||||
|
||||
print("Running $genSnapshot");
|
||||
// Extracted from pkg/dart2native/lib/dart2native.dart.
|
||||
final ProcessResult snapshotRun = Process.runSync(genSnapshotFile.path, [
|
||||
final List<String> genSnapshotArgs = [
|
||||
"--snapshot-kind=app-aot-elf",
|
||||
"--elf=${aotElfFile.path}",
|
||||
"--dwarf-stack-traces",
|
||||
@@ -135,7 +177,17 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
|
||||
"--strip",
|
||||
"--save-obfuscation-map=${obfuscationMapFile.path}",
|
||||
kernelDillFile.path,
|
||||
]);
|
||||
];
|
||||
if (loadObfuscationMapFile != null) {
|
||||
genSnapshotArgs.insert(
|
||||
genSnapshotArgs.length - 1,
|
||||
"--load-obfuscation-map=${loadObfuscationMapFile.path}",
|
||||
);
|
||||
}
|
||||
final ProcessResult snapshotRun = Process.runSync(
|
||||
genSnapshotFile.path,
|
||||
genSnapshotArgs,
|
||||
);
|
||||
|
||||
if (snapshotRun.exitCode != 0) {
|
||||
throw "Got exit code ${snapshotRun.exitCode}\n"
|
||||
@@ -157,6 +209,10 @@ List<MappingPair> readJsonMapping(File file) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, String> toMap(List<MappingPair> mapping) {
|
||||
return {for (MappingPair pair in mapping) pair.from: pair.to};
|
||||
}
|
||||
|
||||
class MappingPair {
|
||||
final String from;
|
||||
final String to;
|
||||
@@ -200,3 +256,49 @@ bool verify(
|
||||
}
|
||||
return good;
|
||||
}
|
||||
|
||||
bool verifyLoadedObfuscationMap(
|
||||
List<MappingPair> baseMapping,
|
||||
List<MappingPair> patchMapping,
|
||||
String patchOnlyName,
|
||||
) {
|
||||
bool good = true;
|
||||
final Map<String, String> patchMap = toMap(patchMapping);
|
||||
for (MappingPair baseEntry in baseMapping) {
|
||||
final String? patchValue = patchMap[baseEntry.from];
|
||||
if (patchValue == null) {
|
||||
print("Patch map is missing loaded entry ${baseEntry.from}");
|
||||
good = false;
|
||||
} else if (patchValue != baseEntry.to) {
|
||||
print(
|
||||
"Expected ${baseEntry.from} to keep ${baseEntry.to}, "
|
||||
"but patch map used $patchValue",
|
||||
);
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
|
||||
final String? patchOnlyValue = patchMap[patchOnlyName];
|
||||
if (patchOnlyValue == null) {
|
||||
print("Patch map is missing $patchOnlyName");
|
||||
good = false;
|
||||
} else {
|
||||
if (patchOnlyValue == patchOnlyName) {
|
||||
print("Expected $patchOnlyName to be obfuscated");
|
||||
good = false;
|
||||
}
|
||||
final Set<String> baseValues = baseMapping
|
||||
.where((MappingPair pair) => pair.from != pair.to)
|
||||
.map((MappingPair pair) => pair.to)
|
||||
.toSet();
|
||||
if (baseValues.contains(patchOnlyValue)) {
|
||||
print(
|
||||
"Patch-only rename $patchOnlyName->$patchOnlyValue collides with "
|
||||
"the loaded map",
|
||||
);
|
||||
good = false;
|
||||
}
|
||||
}
|
||||
|
||||
return good;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,9 @@ import("runtime_args.gni")
|
||||
|
||||
import("//build/config/sysroot.gni")
|
||||
|
||||
assert(!dart_enable_aot_patching || !dart_dynamic_modules,
|
||||
"dart_enable_aot_patching must be built without dart_dynamic_modules.")
|
||||
|
||||
config("dart_public_config") {
|
||||
include_dirs = [
|
||||
".",
|
||||
@@ -231,6 +234,10 @@ config("dart_config") {
|
||||
defines += [ "DART_DYNAMIC_MODULES" ]
|
||||
}
|
||||
|
||||
if (dart_enable_aot_patching) {
|
||||
defines += [ "DART_ENABLE_AOT_PATCHING" ]
|
||||
}
|
||||
|
||||
if (include_experimental_vm_service) {
|
||||
defines += [ "EXPERIMENTAL_VM_SERVICE" ]
|
||||
}
|
||||
@@ -371,6 +378,9 @@ library_for_all_configs("libdart") {
|
||||
if (dart_support_perfetto) {
|
||||
extra_deps += [ "//third_party/perfetto:libprotozero" ]
|
||||
}
|
||||
if (dart_enable_aot_patching) {
|
||||
extra_deps += [ "//third_party/boringssl" ]
|
||||
}
|
||||
if (is_fuchsia) {
|
||||
extra_deps += [
|
||||
"$fuchsia_sdk/pkg/fdio",
|
||||
@@ -385,6 +395,9 @@ library_for_all_configs("libdart") {
|
||||
compiler_lib = "vm:libdart_compiler"
|
||||
extra_configs = [ ":dart_shared_lib" ]
|
||||
include_dirs = [ "." ]
|
||||
if (dart_enable_aot_patching) {
|
||||
include_dirs += [ "//third_party/boringssl/src/include" ]
|
||||
}
|
||||
public_configs = [ ":dart_public_config" ]
|
||||
sources = [
|
||||
"$target_gen_dir/version.cc",
|
||||
|
||||
@@ -112,6 +112,7 @@ static const char* const kSnapshotKindNames[] = {
|
||||
V(loading_unit_manifest, loading_unit_manifest_filename) \
|
||||
V(save_debugging_info, debugging_info_filename) \
|
||||
V(save_obfuscation_map, obfuscation_map_filename) \
|
||||
V(load_obfuscation_map, load_obfuscation_map_filename) \
|
||||
V(ffi_callback_stub, ffi_callback_stub_filename)
|
||||
|
||||
#define BOOL_OPTIONS_LIST(V) \
|
||||
@@ -168,6 +169,7 @@ static void PrintUsage() {
|
||||
"[--obfuscate] \n"
|
||||
"[--save-debugging-info=<debug-filename>] \n"
|
||||
"[--save-obfuscation-map=<map-filename>] \n"
|
||||
"[--load-obfuscation-map=<map-filename>] \n"
|
||||
"<dart-kernel-file> \n"
|
||||
" \n"
|
||||
"To create an AOT application snapshot as an ELF shared library: \n"
|
||||
@@ -177,6 +179,7 @@ static void PrintUsage() {
|
||||
"[--obfuscate] \n"
|
||||
"[--save-debugging-info=<debug-filename>] \n"
|
||||
"[--save-obfuscation-map=<map-filename>] \n"
|
||||
"[--load-obfuscation-map=<map-filename>] \n"
|
||||
"<dart-kernel-file> \n"
|
||||
" \n"
|
||||
"To create an AOT application snapshot as an Mach-O dynamic library (dylib): \n"
|
||||
@@ -186,6 +189,7 @@ static void PrintUsage() {
|
||||
"[--obfuscate] \n"
|
||||
"[--save-debugging-info=<debug-filename>] \n"
|
||||
"[--save-obfuscation-map=<map-filename>] \n"
|
||||
"[--load-obfuscation-map=<map-filename>] \n"
|
||||
"<dart-kernel-file> \n"
|
||||
" \n"
|
||||
"AOT snapshots can be obfuscated: that is all identifiers will be renamed \n"
|
||||
@@ -320,6 +324,12 @@ static int ParseArguments(int argc,
|
||||
"obfuscation is enabled by the --obfuscate flag.\n\n");
|
||||
return -1;
|
||||
}
|
||||
if (!obfuscate && load_obfuscation_map_filename != nullptr) {
|
||||
Syslog::PrintErr(
|
||||
"--load-obfuscation_map=<...> should only be specified when "
|
||||
"obfuscation is enabled by the --obfuscate flag.\n\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!IsSnapshottingForPrecompilation()) {
|
||||
if (obfuscate) {
|
||||
@@ -335,6 +345,13 @@ static int ParseArguments(int argc,
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (load_obfuscation_map_filename != nullptr) {
|
||||
Syslog::PrintErr(
|
||||
"--load-obfuscation-map=<...> can only be enabled when building an "
|
||||
"AOT snapshot.\n\n");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (strip) {
|
||||
Syslog::PrintErr(
|
||||
"Stripping can only be enabled when building an AOT snapshot.\n\n");
|
||||
@@ -394,6 +411,209 @@ static void MallocFinalizer(void* isolate_callback_data, void* peer) {
|
||||
free(peer);
|
||||
}
|
||||
|
||||
static bool IsJsonWhitespace(uint8_t c) {
|
||||
return c == ' ' || c == '\n' || c == '\r' || c == '\t';
|
||||
}
|
||||
|
||||
static int HexDigit(uint8_t c) {
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c - '0';
|
||||
}
|
||||
if (c >= 'a' && c <= 'f') {
|
||||
return 10 + (c - 'a');
|
||||
}
|
||||
if (c >= 'A' && c <= 'F') {
|
||||
return 10 + (c - 'A');
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static void AppendUtf8(char* buffer, intptr_t* length, uint32_t code_point) {
|
||||
if (code_point <= 0x7F) {
|
||||
buffer[(*length)++] = static_cast<char>(code_point);
|
||||
} else if (code_point <= 0x7FF) {
|
||||
buffer[(*length)++] = static_cast<char>(0xC0 | (code_point >> 6));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | (code_point & 0x3F));
|
||||
} else if (code_point <= 0xFFFF) {
|
||||
buffer[(*length)++] = static_cast<char>(0xE0 | (code_point >> 12));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | ((code_point >> 6) & 0x3F));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | (code_point & 0x3F));
|
||||
} else {
|
||||
buffer[(*length)++] = static_cast<char>(0xF0 | (code_point >> 18));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | ((code_point >> 12) & 0x3F));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | ((code_point >> 6) & 0x3F));
|
||||
buffer[(*length)++] = static_cast<char>(0x80 | (code_point & 0x3F));
|
||||
}
|
||||
}
|
||||
|
||||
static bool ParseJsonStringArray(const uint8_t* buffer,
|
||||
intptr_t size,
|
||||
MallocGrowableArray<const char*>* strings,
|
||||
const char** error) {
|
||||
intptr_t cursor = 0;
|
||||
auto skip_whitespace = [&]() {
|
||||
while (cursor < size && IsJsonWhitespace(buffer[cursor])) {
|
||||
cursor++;
|
||||
}
|
||||
};
|
||||
|
||||
skip_whitespace();
|
||||
if (cursor >= size || buffer[cursor++] != '[') {
|
||||
*error = "expected '['";
|
||||
return false;
|
||||
}
|
||||
skip_whitespace();
|
||||
if (cursor < size && buffer[cursor] == ']') {
|
||||
cursor++;
|
||||
skip_whitespace();
|
||||
if (cursor != size) {
|
||||
*error = "unexpected characters after ']'";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
while (cursor < size) {
|
||||
if (buffer[cursor++] != '"') {
|
||||
*error = "expected string";
|
||||
return false;
|
||||
}
|
||||
char* value = reinterpret_cast<char*>(malloc(size + 1));
|
||||
if (value == nullptr) {
|
||||
*error = "out of memory";
|
||||
return false;
|
||||
}
|
||||
intptr_t length = 0;
|
||||
while (cursor < size) {
|
||||
uint8_t c = buffer[cursor++];
|
||||
if (c == '"') {
|
||||
value[length] = '\0';
|
||||
strings->Add(value);
|
||||
value = nullptr;
|
||||
break;
|
||||
}
|
||||
if (c != '\\') {
|
||||
value[length++] = static_cast<char>(c);
|
||||
continue;
|
||||
}
|
||||
if (cursor >= size) {
|
||||
free(value);
|
||||
*error = "unterminated escape";
|
||||
return false;
|
||||
}
|
||||
c = buffer[cursor++];
|
||||
switch (c) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
value[length++] = static_cast<char>(c);
|
||||
break;
|
||||
case 'b':
|
||||
value[length++] = '\b';
|
||||
break;
|
||||
case 'f':
|
||||
value[length++] = '\f';
|
||||
break;
|
||||
case 'n':
|
||||
value[length++] = '\n';
|
||||
break;
|
||||
case 'r':
|
||||
value[length++] = '\r';
|
||||
break;
|
||||
case 't':
|
||||
value[length++] = '\t';
|
||||
break;
|
||||
case 'u': {
|
||||
if (cursor + 4 > size) {
|
||||
free(value);
|
||||
*error = "incomplete unicode escape";
|
||||
return false;
|
||||
}
|
||||
uint32_t code_point = 0;
|
||||
for (intptr_t i = 0; i < 4; i++) {
|
||||
const int digit = HexDigit(buffer[cursor++]);
|
||||
if (digit < 0) {
|
||||
free(value);
|
||||
*error = "invalid unicode escape";
|
||||
return false;
|
||||
}
|
||||
code_point = (code_point << 4) | digit;
|
||||
}
|
||||
AppendUtf8(value, &length, code_point);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
free(value);
|
||||
*error = "invalid escape";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (value != nullptr) {
|
||||
free(value);
|
||||
*error = "unterminated string";
|
||||
return false;
|
||||
}
|
||||
|
||||
skip_whitespace();
|
||||
if (cursor < size && buffer[cursor] == ',') {
|
||||
cursor++;
|
||||
skip_whitespace();
|
||||
continue;
|
||||
}
|
||||
if (cursor < size && buffer[cursor] == ']') {
|
||||
cursor++;
|
||||
skip_whitespace();
|
||||
if (cursor != size) {
|
||||
*error = "unexpected characters after ']'";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
*error = "expected ',' or ']'";
|
||||
return false;
|
||||
}
|
||||
|
||||
*error = "unterminated array";
|
||||
return false;
|
||||
}
|
||||
|
||||
static void MaybeLoadObfuscationMap() {
|
||||
if (load_obfuscation_map_filename == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t* buffer = nullptr;
|
||||
intptr_t size = 0;
|
||||
ReadFile(load_obfuscation_map_filename, &buffer, &size);
|
||||
|
||||
MallocGrowableArray<const char*> strings;
|
||||
const char* parse_error = nullptr;
|
||||
if (!ParseJsonStringArray(buffer, size, &strings, &parse_error)) {
|
||||
free(buffer);
|
||||
for (intptr_t i = 0; i < strings.length(); i++) {
|
||||
free(const_cast<char*>(strings[i]));
|
||||
}
|
||||
PrintErrAndExit("Error: Invalid obfuscation map %s: %s\n",
|
||||
load_obfuscation_map_filename, parse_error);
|
||||
}
|
||||
if ((strings.length() % 2) != 0) {
|
||||
free(buffer);
|
||||
for (intptr_t i = 0; i < strings.length(); i++) {
|
||||
free(const_cast<char*>(strings[i]));
|
||||
}
|
||||
PrintErrAndExit(
|
||||
"Error: Invalid obfuscation map %s: expected string pairs\n",
|
||||
load_obfuscation_map_filename);
|
||||
}
|
||||
|
||||
Dart_Handle result = Dart_SetObfuscationMap(strings.data(), strings.length());
|
||||
free(buffer);
|
||||
for (intptr_t i = 0; i < strings.length(); i++) {
|
||||
free(const_cast<char*>(strings[i]));
|
||||
}
|
||||
CHECK_RESULT(result);
|
||||
}
|
||||
|
||||
static void MaybeLoadExtraInputs(const CommandLineOptions& inputs) {
|
||||
for (intptr_t i = 1; i < inputs.count(); i++) {
|
||||
uint8_t* buffer = nullptr;
|
||||
@@ -656,6 +876,8 @@ static void CreateAndWritePrecompiledSnapshot() {
|
||||
ASSERT(kind_str != nullptr);
|
||||
ASSERT(filename != nullptr);
|
||||
|
||||
MaybeLoadObfuscationMap();
|
||||
|
||||
// Precompile with specified embedder entry points
|
||||
Dart_Handle result = Dart_Precompile();
|
||||
CHECK_RESULT(result);
|
||||
|
||||
@@ -4241,6 +4241,82 @@ Dart_CreateAppJITSnapshotAsBlobs(uint8_t** snapshot_data_buffer,
|
||||
DART_EXPORT DART_API_WARN_UNUSED_RESULT Dart_Handle
|
||||
Dart_GetObfuscationMap(uint8_t** buffer, intptr_t* buffer_length);
|
||||
|
||||
/**
|
||||
* Restore a previously saved obfuscation map before AOT precompilation.
|
||||
*
|
||||
* The map uses the same flat string-pair format returned by
|
||||
* Dart_GetObfuscationMap: [original0, obfuscated0, original1, obfuscated1, ...].
|
||||
* The VM rebuilds the precompiler obfuscation state so subsequent renames are
|
||||
* stable with the release build and newly introduced identifiers receive fresh
|
||||
* obfuscated names.
|
||||
*/
|
||||
DART_EXPORT DART_API_WARN_UNUSED_RESULT Dart_Handle
|
||||
Dart_SetObfuscationMap(const char* const* map, intptr_t map_length);
|
||||
|
||||
/*
|
||||
* ========
|
||||
* AOT patching
|
||||
* ========
|
||||
*/
|
||||
|
||||
typedef struct {
|
||||
const char* app_id;
|
||||
const char* app_build_id;
|
||||
const char* base_flavor_id;
|
||||
const char* base_license_type;
|
||||
const char* flavor_id;
|
||||
const char* license_type;
|
||||
const char* sdk_hash;
|
||||
const char* base_snapshot_hash;
|
||||
const char* patch_snapshot_hash;
|
||||
const char* obfuscation_map_hash;
|
||||
const char* target_os;
|
||||
const char* target_arch;
|
||||
} Dart_AotPatchInstallOptions;
|
||||
|
||||
typedef bool (*Dart_AotPatchKeyCallback)(const char* key_id,
|
||||
uint8_t* key_buffer,
|
||||
intptr_t key_buffer_length,
|
||||
intptr_t* key_length);
|
||||
|
||||
/**
|
||||
* Returns whether this VM was built with compact AOT patching support.
|
||||
*
|
||||
* This feature is intentionally independent of DART_DYNAMIC_MODULES and does
|
||||
* not enable the bytecode interpreter.
|
||||
*/
|
||||
DART_EXPORT bool Dart_AotPatchingEnabled(void);
|
||||
|
||||
/**
|
||||
* Sets the callback used to resolve AES keys for encrypted AOT patch payloads.
|
||||
* The callback is owned by the embedder and must remain valid while patches may
|
||||
* be installed.
|
||||
*/
|
||||
DART_EXPORT void Dart_SetAotPatchKeyCallback(Dart_AotPatchKeyCallback callback);
|
||||
|
||||
/**
|
||||
* Validates an encrypted compact AOT patch artifact before embedder install.
|
||||
*
|
||||
* The VM validates open artifact metadata and requests key material for the
|
||||
* artifact key id, then decrypts the AES-256-GCM compact payload into an owned
|
||||
* buffer. A success result means the artifact is accepted for embedder
|
||||
* installation. The caller owns `patch_payload_buffer` and must release it with
|
||||
* Dart_FreeAotPatchPayload. iOS-safe AOT patch loading maps patched isolate
|
||||
* snapshot data/instructions before isolate startup; this API does not mutate
|
||||
* live executable code.
|
||||
*/
|
||||
DART_EXPORT DART_API_WARN_UNUSED_RESULT Dart_Handle
|
||||
Dart_InstallAotPatch(const uint8_t* patch_buffer,
|
||||
intptr_t patch_buffer_length,
|
||||
const Dart_AotPatchInstallOptions* options,
|
||||
uint8_t** patch_payload_buffer,
|
||||
intptr_t* patch_payload_length);
|
||||
|
||||
/**
|
||||
* Frees the buffer returned by Dart_InstallAotPatch.
|
||||
*/
|
||||
DART_EXPORT void Dart_FreeAotPatchPayload(uint8_t* patch_payload_buffer);
|
||||
|
||||
/**
|
||||
* Returns whether the VM only supports running from precompiled snapshots and
|
||||
* not from any other kind of snapshot or from source (that is, the VM was
|
||||
|
||||
@@ -74,6 +74,10 @@ declare_args() {
|
||||
|
||||
# Whether to support dynamic loading and interpretation of Dart bytecode.
|
||||
dart_dynamic_modules = false
|
||||
|
||||
# Whether to expose the compact AOT patch installation API. This is separate
|
||||
# from dart_dynamic_modules and must not pull in the bytecode interpreter.
|
||||
dart_enable_aot_patching = false
|
||||
}
|
||||
|
||||
declare_args() {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) 2026, 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.
|
||||
|
||||
// Verifies that the compact AOT patching API is exported and callable without
|
||||
// enabling DART_DYNAMIC_MODULES or the bytecode interpreter.
|
||||
|
||||
import 'dart:ffi';
|
||||
|
||||
import 'package:expect/expect.dart';
|
||||
|
||||
typedef DartAotPatchingEnabledNative = Bool Function();
|
||||
typedef DartAotPatchingEnabled = bool Function();
|
||||
|
||||
void main() {
|
||||
final enabled = DynamicLibrary.executable()
|
||||
.lookupFunction<DartAotPatchingEnabledNative, DartAotPatchingEnabled>(
|
||||
'Dart_AotPatchingEnabled',
|
||||
)();
|
||||
|
||||
// The value depends on the build flag, but a successful call proves the API
|
||||
// is present in both enabled and default builds.
|
||||
Expect.isTrue(enabled == true || enabled == false);
|
||||
}
|
||||
@@ -59,6 +59,7 @@ main() {
|
||||
|
||||
var expectedSymbols = [
|
||||
"Dart_AddSymbols",
|
||||
"Dart_AotPatchingEnabled",
|
||||
"Dart_Allocate",
|
||||
"Dart_AllocateWithNativeFields",
|
||||
"Dart_BooleanValue",
|
||||
@@ -111,6 +112,7 @@ main() {
|
||||
"Dart_False",
|
||||
"Dart_FinalizeAllClasses",
|
||||
"Dart_FinalizeLoading",
|
||||
"Dart_FreeAotPatchPayload",
|
||||
"Dart_FunctionIsStatic",
|
||||
"Dart_FunctionName",
|
||||
"Dart_FunctionOwner",
|
||||
@@ -158,6 +160,7 @@ main() {
|
||||
"Dart_IdentityEquals",
|
||||
"Dart_Initialize",
|
||||
"Dart_InitializeNativeAssetsResolver",
|
||||
"Dart_InstallAotPatch",
|
||||
"Dart_InstanceGetType",
|
||||
"Dart_IntegerFitsIntoInt64",
|
||||
"Dart_IntegerFitsIntoUint64",
|
||||
@@ -296,6 +299,7 @@ main() {
|
||||
"Dart_SendPortGetId",
|
||||
"Dart_SendPortGetIdEx",
|
||||
"Dart_ServiceSendDataEvent",
|
||||
"Dart_SetAotPatchKeyCallback",
|
||||
"Dart_SetBooleanReturnValue",
|
||||
"Dart_SetCurrentUserTag",
|
||||
"Dart_SetDartLibrarySourcesKernel",
|
||||
@@ -314,6 +318,7 @@ main() {
|
||||
"Dart_SetMessageNotifyCallback",
|
||||
"Dart_SetNativeInstanceField",
|
||||
"Dart_SetNativeResolver",
|
||||
"Dart_SetObfuscationMap",
|
||||
"Dart_SetPausedOnExit",
|
||||
"Dart_SetPausedOnStart",
|
||||
"Dart_SetPeer",
|
||||
|
||||
@@ -3070,7 +3070,7 @@ void StubCodeCompiler::GenerateInterpretCallStub() {
|
||||
__ movq(CallingConventions::kArg3Reg, R11); // Negative argc.
|
||||
__ movq(CallingConventions::kArg4Reg, R12); // Argv.
|
||||
|
||||
#if defined(TARGET_OS_WINDOWS)
|
||||
#if defined(DART_TARGET_OS_WINDOWS)
|
||||
__ movq(Address(RSP, 0 * target::kWordSize), THR); // Thread.
|
||||
#else
|
||||
__ movq(CallingConventions::kArg5Reg, THR); // Thread.
|
||||
|
||||
+613
-3
@@ -9,6 +9,11 @@
|
||||
#include <memory>
|
||||
#include <utility>
|
||||
|
||||
#if defined(DART_ENABLE_AOT_PATCHING)
|
||||
#include <openssl/aead.h>
|
||||
#include <openssl/sha.h>
|
||||
#endif
|
||||
|
||||
#include "lib/stacktrace.h"
|
||||
#include "platform/address_sanitizer.h"
|
||||
#include "platform/assert.h"
|
||||
@@ -16,6 +21,7 @@
|
||||
#include "platform/thread_sanitizer.h"
|
||||
#include "platform/unicode.h"
|
||||
#include "vm/app_snapshot.h"
|
||||
#include "vm/base64.h"
|
||||
#include "vm/bytecode_reader.h"
|
||||
#include "vm/class_finalizer.h"
|
||||
#include "vm/compiler/jit/compiler.h"
|
||||
@@ -6721,9 +6727,10 @@ static void CreateAppAOTSnapshotHelper(
|
||||
};
|
||||
|
||||
Dwarf* const dwarf =
|
||||
(format == Dart_AotBinaryFormat_Assembly || strip) ? nullptr
|
||||
: generate_debug ? debug_dwarf
|
||||
: new (Z) Dwarf(Z, deobfuscation_trie, identifier);
|
||||
(format == Dart_AotBinaryFormat_Assembly || strip)
|
||||
? nullptr
|
||||
: generate_debug ? debug_dwarf
|
||||
: new (Z) Dwarf(Z, deobfuscation_trie, identifier);
|
||||
SharedObjectWriter* so = nullptr;
|
||||
if (format == Dart_AotBinaryFormat_Elf) {
|
||||
so = new (Z)
|
||||
@@ -7122,6 +7129,126 @@ Dart_CreateAppJITSnapshotAsBlobs(uint8_t** isolate_snapshot_data_buffer,
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
|
||||
static bool IsObfuscationNameChar(char c) {
|
||||
return (('a' <= c) && (c <= 'z')) || (('A' <= c) && (c <= 'Z'));
|
||||
}
|
||||
|
||||
static intptr_t ObfuscationNameCharRank(char c) {
|
||||
if (('a' <= c) && (c <= 'z')) {
|
||||
return c - 'a';
|
||||
}
|
||||
ASSERT(('A' <= c) && (c <= 'Z'));
|
||||
return 26 + (c - 'A');
|
||||
}
|
||||
|
||||
static bool IsObfuscationNameGreater(const char* left, const char* right) {
|
||||
const intptr_t left_length = strlen(left);
|
||||
const intptr_t right_length = strlen(right);
|
||||
if (left_length != right_length) {
|
||||
return left_length > right_length;
|
||||
}
|
||||
for (intptr_t i = left_length - 1; i >= 0; i--) {
|
||||
const intptr_t left_rank = ObfuscationNameCharRank(left[i]);
|
||||
const intptr_t right_rank = ObfuscationNameCharRank(right[i]);
|
||||
if (left_rank != right_rank) {
|
||||
return left_rank > right_rank;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ExtractObfuscationRenameStem(const char* rename,
|
||||
char* stem,
|
||||
intptr_t stem_length) {
|
||||
ASSERT(stem_length > 0);
|
||||
const char* cursor = rename;
|
||||
if (strncmp(cursor, "get:", 4) == 0 || strncmp(cursor, "set:", 4) == 0) {
|
||||
cursor += 4;
|
||||
}
|
||||
if (*cursor == '_') {
|
||||
cursor++;
|
||||
}
|
||||
intptr_t written = 0;
|
||||
while (*cursor != '\0' && *cursor != '@') {
|
||||
if (!IsObfuscationNameChar(*cursor)) {
|
||||
return false;
|
||||
}
|
||||
if (written >= stem_length - 1) {
|
||||
return false;
|
||||
}
|
||||
stem[written++] = *cursor++;
|
||||
}
|
||||
if (written == 0) {
|
||||
return false;
|
||||
}
|
||||
stem[written] = '\0';
|
||||
return true;
|
||||
}
|
||||
#endif // defined(DART_PRECOMPILER) && !defined(TARGET_ARCH_IA32)
|
||||
|
||||
DART_EXPORT Dart_Handle Dart_SetObfuscationMap(const char* const* map,
|
||||
intptr_t map_length) {
|
||||
#if defined(DART_PRECOMPILED_RUNTIME)
|
||||
return Api::NewError("No obfuscation map to load on an AOT runtime.");
|
||||
#elif !defined(DART_PRECOMPILER)
|
||||
return Api::NewError("Obfuscation is only supported for AOT compiler.");
|
||||
#elif defined(TARGET_ARCH_IA32)
|
||||
return Api::NewError("Obfuscation is not supported on IA32.");
|
||||
#else
|
||||
Thread* thread = Thread::Current();
|
||||
DARTSCOPE(thread);
|
||||
auto isolate_group = thread->isolate_group();
|
||||
|
||||
if (map == nullptr) {
|
||||
RETURN_NULL_ERROR(map);
|
||||
}
|
||||
if (map_length < 0 || (map_length % 2) != 0) {
|
||||
return Api::NewError(
|
||||
"Obfuscation map must contain an even number of strings.");
|
||||
}
|
||||
if (!isolate_group->obfuscate()) {
|
||||
return Api::NewError(
|
||||
"Obfuscation map can only be loaded when --obfuscate is enabled.");
|
||||
}
|
||||
|
||||
const intptr_t map_entries = map_length / 2;
|
||||
const intptr_t initial_capacity =
|
||||
Utils::Maximum(map_entries, static_cast<intptr_t>(16));
|
||||
ObfuscationMap renames(
|
||||
HashTables::New<ObfuscationMap>(initial_capacity, Heap::kOld));
|
||||
String& key = String::Handle(Z);
|
||||
String& value = String::Handle(Z);
|
||||
char last_name[100];
|
||||
last_name[0] = '\0';
|
||||
|
||||
for (intptr_t i = 0; i < map_length; i += 2) {
|
||||
if (map[i] == nullptr || map[i + 1] == nullptr) {
|
||||
renames.Release();
|
||||
return Api::NewError("Obfuscation map contains a null string.");
|
||||
}
|
||||
key = Symbols::New(thread, map[i]);
|
||||
value = Symbols::New(thread, map[i + 1]);
|
||||
renames.UpdateOrInsert(key, value);
|
||||
|
||||
if (strcmp(map[i], map[i + 1]) != 0) {
|
||||
char stem[100];
|
||||
if (ExtractObfuscationRenameStem(map[i + 1], stem, sizeof(stem)) &&
|
||||
(last_name[0] == '\0' || IsObfuscationNameGreater(stem, last_name))) {
|
||||
strncpy(last_name, stem, sizeof(last_name));
|
||||
last_name[sizeof(last_name) - 1] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Array& state = Array::Handle(Z, Array::New(2, Heap::kOld));
|
||||
state.SetAt(0, String::Handle(Z, String::New(last_name, Heap::kOld)));
|
||||
state.SetAt(1, renames.Release());
|
||||
isolate_group->object_store()->set_obfuscation_map(state);
|
||||
return Api::Success();
|
||||
#endif
|
||||
}
|
||||
|
||||
DART_EXPORT Dart_Handle Dart_GetObfuscationMap(uint8_t** buffer,
|
||||
intptr_t* buffer_length) {
|
||||
#if defined(DART_PRECOMPILED_RUNTIME)
|
||||
@@ -7163,6 +7290,489 @@ DART_EXPORT Dart_Handle Dart_GetObfuscationMap(uint8_t** buffer,
|
||||
#endif
|
||||
}
|
||||
|
||||
Dart_AotPatchKeyCallback g_aot_patch_key_callback = nullptr;
|
||||
|
||||
#if defined(DART_ENABLE_AOT_PATCHING) && defined(DART_DYNAMIC_MODULES)
|
||||
#error DART_ENABLE_AOT_PATCHING must be built without DART_DYNAMIC_MODULES.
|
||||
#endif
|
||||
|
||||
#if defined(DART_ENABLE_AOT_PATCHING)
|
||||
struct AotPatchJsonString {
|
||||
const char* chars;
|
||||
intptr_t length;
|
||||
};
|
||||
|
||||
static bool IsAotPatchJsonWhitespace(char c) {
|
||||
return c == ' ' || c == '\n' || c == '\r' || c == '\t';
|
||||
}
|
||||
|
||||
static void SkipAotPatchJsonWhitespace(const char* json,
|
||||
intptr_t json_length,
|
||||
intptr_t* cursor) {
|
||||
while (*cursor < json_length && IsAotPatchJsonWhitespace(json[*cursor])) {
|
||||
(*cursor)++;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ParseAotPatchJsonString(const char* json,
|
||||
intptr_t json_length,
|
||||
intptr_t* cursor,
|
||||
AotPatchJsonString* out) {
|
||||
if (*cursor >= json_length || json[*cursor] != '"') {
|
||||
return false;
|
||||
}
|
||||
(*cursor)++;
|
||||
const intptr_t start = *cursor;
|
||||
while (*cursor < json_length) {
|
||||
const char c = json[*cursor];
|
||||
if (c == '\\') {
|
||||
// Open patch artifacts use stable ASCII metadata. Reject escaped fields
|
||||
// in the VM validator instead of accepting ambiguous raw comparisons.
|
||||
return false;
|
||||
}
|
||||
if (c == '"') {
|
||||
out->chars = json + start;
|
||||
out->length = *cursor - start;
|
||||
(*cursor)++;
|
||||
return true;
|
||||
}
|
||||
if (static_cast<unsigned char>(c) < 0x20) {
|
||||
return false;
|
||||
}
|
||||
(*cursor)++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool AotPatchJsonStringEquals(const AotPatchJsonString& value,
|
||||
const char* expected) {
|
||||
const intptr_t expected_length = strlen(expected);
|
||||
return value.length == expected_length &&
|
||||
strncmp(value.chars, expected, value.length) == 0;
|
||||
}
|
||||
|
||||
static bool FindAotPatchJsonString(const char* json,
|
||||
intptr_t json_length,
|
||||
const char* key,
|
||||
AotPatchJsonString* out) {
|
||||
intptr_t cursor = 0;
|
||||
while (cursor < json_length) {
|
||||
if (json[cursor] != '"') {
|
||||
cursor++;
|
||||
continue;
|
||||
}
|
||||
|
||||
AotPatchJsonString current_key;
|
||||
if (!ParseAotPatchJsonString(json, json_length, &cursor, ¤t_key)) {
|
||||
return false;
|
||||
}
|
||||
SkipAotPatchJsonWhitespace(json, json_length, &cursor);
|
||||
if (cursor >= json_length || json[cursor] != ':') {
|
||||
continue;
|
||||
}
|
||||
cursor++;
|
||||
SkipAotPatchJsonWhitespace(json, json_length, &cursor);
|
||||
|
||||
if (!AotPatchJsonStringEquals(current_key, key)) {
|
||||
continue;
|
||||
}
|
||||
return ParseAotPatchJsonString(json, json_length, &cursor, out);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static Dart_Handle ValidateAotPatchJsonField(const char* json,
|
||||
intptr_t json_length,
|
||||
const char* field,
|
||||
const char* expected) {
|
||||
AotPatchJsonString actual;
|
||||
if (!FindAotPatchJsonString(json, json_length, field, &actual)) {
|
||||
return Api::NewError("AOT patch artifact is missing field \"%s\".", field);
|
||||
}
|
||||
if (!AotPatchJsonStringEquals(actual, expected)) {
|
||||
return Api::NewError("AOT patch artifact field \"%s\" does not match.",
|
||||
field);
|
||||
}
|
||||
return Api::Success();
|
||||
}
|
||||
|
||||
static char* CopyAotPatchJsonString(Thread* thread,
|
||||
const AotPatchJsonString& value) {
|
||||
char* copy = thread->zone()->Alloc<char>(value.length + 1);
|
||||
memmove(copy, value.chars, value.length);
|
||||
copy[value.length] = '\0';
|
||||
return copy;
|
||||
}
|
||||
|
||||
struct AotPatchOwnedBuffer {
|
||||
uint8_t* data = nullptr;
|
||||
intptr_t length = 0;
|
||||
|
||||
~AotPatchOwnedBuffer() { free(data); }
|
||||
|
||||
uint8_t* Release() {
|
||||
uint8_t* result = data;
|
||||
data = nullptr;
|
||||
length = 0;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
static Dart_Handle DecodeAotPatchBase64(Thread* thread,
|
||||
const AotPatchJsonString& value,
|
||||
const char* field,
|
||||
AotPatchOwnedBuffer* out) {
|
||||
if (value.length == 0) {
|
||||
out->data = reinterpret_cast<uint8_t*>(malloc(1));
|
||||
out->length = 0;
|
||||
if (out->data == nullptr) {
|
||||
return Api::NewError("Unable to allocate AOT patch field \"%s\".", field);
|
||||
}
|
||||
return Api::Success();
|
||||
}
|
||||
|
||||
intptr_t decoded_length = 0;
|
||||
uint8_t* decoded =
|
||||
DecodeBase64(CopyAotPatchJsonString(thread, value), &decoded_length);
|
||||
if (decoded == nullptr) {
|
||||
return Api::NewError("AOT patch field \"%s\" is not valid base64.", field);
|
||||
}
|
||||
out->data = decoded;
|
||||
out->length = decoded_length;
|
||||
return Api::Success();
|
||||
}
|
||||
|
||||
static intptr_t AotPatchHexDigit(char c) {
|
||||
if ('0' <= c && c <= '9') return c - '0';
|
||||
if ('a' <= c && c <= 'f') return 10 + (c - 'a');
|
||||
if ('A' <= c && c <= 'F') return 10 + (c - 'A');
|
||||
return -1;
|
||||
}
|
||||
|
||||
static bool AotPatchDigestEqualsHex(const uint8_t* digest,
|
||||
intptr_t digest_length,
|
||||
const AotPatchJsonString& expected) {
|
||||
if (expected.length != digest_length * 2) {
|
||||
return false;
|
||||
}
|
||||
for (intptr_t i = 0; i < digest_length; i++) {
|
||||
const intptr_t high = AotPatchHexDigit(expected.chars[i * 2]);
|
||||
const intptr_t low = AotPatchHexDigit(expected.chars[(i * 2) + 1]);
|
||||
if (high < 0 || low < 0) {
|
||||
return false;
|
||||
}
|
||||
if (digest[i] != static_cast<uint8_t>((high << 4) | low)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static void AppendAotPatchJsonString(ZoneTextBuffer* buffer,
|
||||
const AotPatchJsonString& value) {
|
||||
buffer->AddChar('"');
|
||||
buffer->AddEscapedUTF8(value.chars, value.length);
|
||||
buffer->AddChar('"');
|
||||
}
|
||||
|
||||
static Dart_Handle AppendAotPatchMetadataField(ZoneTextBuffer* buffer,
|
||||
const char* json,
|
||||
intptr_t json_length,
|
||||
const char* field,
|
||||
bool required,
|
||||
bool* first) {
|
||||
AotPatchJsonString value;
|
||||
if (!FindAotPatchJsonString(json, json_length, field, &value)) {
|
||||
if (required) {
|
||||
return Api::NewError("AOT patch artifact is missing field \"%s\".",
|
||||
field);
|
||||
}
|
||||
return Api::Success();
|
||||
}
|
||||
if (!*first) {
|
||||
buffer->AddChar(',');
|
||||
}
|
||||
*first = false;
|
||||
buffer->Printf("\"%s\":", field);
|
||||
AppendAotPatchJsonString(buffer, value);
|
||||
return Api::Success();
|
||||
}
|
||||
|
||||
static Dart_Handle BuildAotPatchMetadataAad(const char* json,
|
||||
intptr_t json_length,
|
||||
ZoneTextBuffer* out) {
|
||||
bool first = true;
|
||||
out->AddChar('{');
|
||||
#define APPEND_FIELD(name, required) \
|
||||
do { \
|
||||
Dart_Handle append_result = AppendAotPatchMetadataField( \
|
||||
out, json, json_length, name, required, &first); \
|
||||
if (Api::IsError(append_result)) return append_result; \
|
||||
} while (0)
|
||||
|
||||
APPEND_FIELD("app_build_id", true);
|
||||
APPEND_FIELD("app_id", true);
|
||||
APPEND_FIELD("base_flavor_id", false);
|
||||
APPEND_FIELD("base_license_type", false);
|
||||
APPEND_FIELD("base_snapshot_hash", true);
|
||||
APPEND_FIELD("flavor_id", true);
|
||||
APPEND_FIELD("license_type", true);
|
||||
APPEND_FIELD("obfuscation_map_hash", false);
|
||||
APPEND_FIELD("patch_snapshot_hash", true);
|
||||
APPEND_FIELD("sdk_hash", true);
|
||||
APPEND_FIELD("target_arch", true);
|
||||
APPEND_FIELD("target_os", true);
|
||||
#undef APPEND_FIELD
|
||||
out->AddChar('}');
|
||||
return Api::Success();
|
||||
}
|
||||
#endif // defined(DART_ENABLE_AOT_PATCHING)
|
||||
|
||||
DART_EXPORT bool Dart_AotPatchingEnabled() {
|
||||
#if defined(DART_ENABLE_AOT_PATCHING)
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
DART_EXPORT void Dart_SetAotPatchKeyCallback(
|
||||
Dart_AotPatchKeyCallback callback) {
|
||||
g_aot_patch_key_callback = callback;
|
||||
}
|
||||
|
||||
DART_EXPORT Dart_Handle
|
||||
Dart_InstallAotPatch(const uint8_t* patch_buffer,
|
||||
intptr_t patch_buffer_length,
|
||||
const Dart_AotPatchInstallOptions* options,
|
||||
uint8_t** patch_payload_buffer,
|
||||
intptr_t* patch_payload_length) {
|
||||
#if !defined(DART_ENABLE_AOT_PATCHING)
|
||||
return Api::NewError(
|
||||
"Compact AOT patching is not enabled in this VM. Rebuild with "
|
||||
"dart_enable_aot_patching=true.");
|
||||
#else
|
||||
Thread* thread = Thread::Current();
|
||||
DARTSCOPE(thread);
|
||||
API_TIMELINE_DURATION(thread);
|
||||
|
||||
if (patch_buffer == nullptr) {
|
||||
RETURN_NULL_ERROR(patch_buffer);
|
||||
}
|
||||
if (patch_buffer_length <= 0) {
|
||||
return Api::NewError("Patch buffer must not be empty.");
|
||||
}
|
||||
if (options == nullptr) {
|
||||
RETURN_NULL_ERROR(options);
|
||||
}
|
||||
if (patch_payload_buffer == nullptr) {
|
||||
RETURN_NULL_ERROR(patch_payload_buffer);
|
||||
}
|
||||
if (patch_payload_length == nullptr) {
|
||||
RETURN_NULL_ERROR(patch_payload_length);
|
||||
}
|
||||
*patch_payload_buffer = nullptr;
|
||||
*patch_payload_length = 0;
|
||||
if (options->app_id == nullptr || options->app_build_id == nullptr ||
|
||||
options->flavor_id == nullptr || options->license_type == nullptr ||
|
||||
options->sdk_hash == nullptr || options->base_snapshot_hash == nullptr ||
|
||||
options->patch_snapshot_hash == nullptr ||
|
||||
options->target_os == nullptr || options->target_arch == nullptr) {
|
||||
return Api::NewError("AOT patch install options are incomplete.");
|
||||
}
|
||||
if (g_aot_patch_key_callback == nullptr) {
|
||||
return Api::NewError("No AOT patch AES key callback has been configured.");
|
||||
}
|
||||
|
||||
const char* json = reinterpret_cast<const char*>(patch_buffer);
|
||||
Dart_Handle result = ValidateAotPatchJsonField(
|
||||
json, patch_buffer_length, "format", "open-aot-vmcode-encrypted-v1");
|
||||
if (Api::IsError(result)) return result;
|
||||
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "app_id",
|
||||
options->app_id);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "app_build_id",
|
||||
options->app_build_id);
|
||||
if (Api::IsError(result)) return result;
|
||||
if (options->base_flavor_id != nullptr) {
|
||||
result = ValidateAotPatchJsonField(
|
||||
json, patch_buffer_length, "base_flavor_id", options->base_flavor_id);
|
||||
if (Api::IsError(result)) return result;
|
||||
}
|
||||
if (options->base_license_type != nullptr) {
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length,
|
||||
"base_license_type",
|
||||
options->base_license_type);
|
||||
if (Api::IsError(result)) return result;
|
||||
}
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "flavor_id",
|
||||
options->flavor_id);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "license_type",
|
||||
options->license_type);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "sdk_hash",
|
||||
options->sdk_hash);
|
||||
if (Api::IsError(result)) return result;
|
||||
result =
|
||||
ValidateAotPatchJsonField(json, patch_buffer_length, "base_snapshot_hash",
|
||||
options->base_snapshot_hash);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length,
|
||||
"patch_snapshot_hash",
|
||||
options->patch_snapshot_hash);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "target_os",
|
||||
options->target_os);
|
||||
if (Api::IsError(result)) return result;
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "target_arch",
|
||||
options->target_arch);
|
||||
if (Api::IsError(result)) return result;
|
||||
if (options->obfuscation_map_hash != nullptr) {
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length,
|
||||
"obfuscation_map_hash",
|
||||
options->obfuscation_map_hash);
|
||||
if (Api::IsError(result)) return result;
|
||||
}
|
||||
|
||||
AotPatchJsonString key_id;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length, "key_id", &key_id)) {
|
||||
return Api::NewError("AOT patch artifact is missing encryption key_id.");
|
||||
}
|
||||
if (key_id.length == 0) {
|
||||
return Api::NewError("AOT patch artifact encryption key_id is empty.");
|
||||
}
|
||||
result = ValidateAotPatchJsonField(json, patch_buffer_length, "algorithm",
|
||||
"AES-256-GCM");
|
||||
if (Api::IsError(result)) return result;
|
||||
AotPatchJsonString encrypted_payload;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length,
|
||||
"encrypted_payload_base64", &encrypted_payload)) {
|
||||
return Api::NewError(
|
||||
"AOT patch artifact is missing encrypted_payload_base64.");
|
||||
}
|
||||
AotPatchJsonString nonce;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length, "nonce_base64",
|
||||
&nonce)) {
|
||||
return Api::NewError("AOT patch artifact is missing nonce_base64.");
|
||||
}
|
||||
AotPatchJsonString tag;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length, "tag_base64", &tag)) {
|
||||
return Api::NewError("AOT patch artifact is missing tag_base64.");
|
||||
}
|
||||
AotPatchJsonString aad_sha256;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length, "aad_sha256",
|
||||
&aad_sha256)) {
|
||||
return Api::NewError("AOT patch artifact is missing aad_sha256.");
|
||||
}
|
||||
AotPatchJsonString payload_sha256;
|
||||
if (!FindAotPatchJsonString(json, patch_buffer_length, "payload_sha256",
|
||||
&payload_sha256)) {
|
||||
return Api::NewError("AOT patch artifact is missing payload_sha256.");
|
||||
}
|
||||
|
||||
AotPatchOwnedBuffer encrypted_payload_buffer;
|
||||
result = DecodeAotPatchBase64(thread, encrypted_payload,
|
||||
"encrypted_payload_base64",
|
||||
&encrypted_payload_buffer);
|
||||
if (Api::IsError(result)) return result;
|
||||
AotPatchOwnedBuffer nonce_buffer;
|
||||
result = DecodeAotPatchBase64(thread, nonce, "nonce_base64", &nonce_buffer);
|
||||
if (Api::IsError(result)) return result;
|
||||
if (nonce_buffer.length != 12) {
|
||||
return Api::NewError("AOT patch AES-GCM nonce must be 12 bytes.");
|
||||
}
|
||||
AotPatchOwnedBuffer tag_buffer;
|
||||
result = DecodeAotPatchBase64(thread, tag, "tag_base64", &tag_buffer);
|
||||
if (Api::IsError(result)) return result;
|
||||
if (tag_buffer.length != 16) {
|
||||
return Api::NewError("AOT patch AES-GCM tag must be 16 bytes.");
|
||||
}
|
||||
|
||||
ZoneTextBuffer aad(thread->zone(), 512);
|
||||
result = BuildAotPatchMetadataAad(json, patch_buffer_length, &aad);
|
||||
if (Api::IsError(result)) return result;
|
||||
uint8_t aad_digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256(reinterpret_cast<const uint8_t*>(aad.buffer()), aad.length(),
|
||||
aad_digest);
|
||||
if (!AotPatchDigestEqualsHex(aad_digest, SHA256_DIGEST_LENGTH, aad_sha256)) {
|
||||
return Api::NewError("AOT patch artifact metadata AAD hash mismatch.");
|
||||
}
|
||||
|
||||
uint8_t key_buffer[32];
|
||||
intptr_t key_length = 0;
|
||||
const bool key_ok =
|
||||
g_aot_patch_key_callback(CopyAotPatchJsonString(thread, key_id),
|
||||
key_buffer, sizeof(key_buffer), &key_length);
|
||||
if (!key_ok || key_length != 32) {
|
||||
memset(key_buffer, 0, sizeof(key_buffer));
|
||||
return Api::NewError(
|
||||
"AOT patch AES key callback failed to provide a 32-byte key.");
|
||||
}
|
||||
|
||||
AotPatchOwnedBuffer sealed_payload;
|
||||
sealed_payload.length = encrypted_payload_buffer.length + tag_buffer.length;
|
||||
sealed_payload.data = reinterpret_cast<uint8_t*>(
|
||||
malloc(Utils::Maximum<intptr_t>(sealed_payload.length, 1)));
|
||||
if (sealed_payload.data == nullptr) {
|
||||
return Api::NewError("Unable to allocate AOT patch sealed payload.");
|
||||
}
|
||||
memmove(sealed_payload.data, encrypted_payload_buffer.data,
|
||||
encrypted_payload_buffer.length);
|
||||
memmove(sealed_payload.data + encrypted_payload_buffer.length,
|
||||
tag_buffer.data, tag_buffer.length);
|
||||
|
||||
AotPatchOwnedBuffer decrypted_payload_buffer;
|
||||
decrypted_payload_buffer.length = sealed_payload.length;
|
||||
decrypted_payload_buffer.data = reinterpret_cast<uint8_t*>(
|
||||
malloc(Utils::Maximum<intptr_t>(decrypted_payload_buffer.length, 1)));
|
||||
if (decrypted_payload_buffer.data == nullptr) {
|
||||
return Api::NewError("Unable to allocate AOT patch payload.");
|
||||
}
|
||||
|
||||
EVP_AEAD_CTX ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
if (EVP_AEAD_CTX_init(&ctx, EVP_aead_aes_256_gcm(), key_buffer,
|
||||
sizeof(key_buffer), tag_buffer.length, nullptr) != 1) {
|
||||
memset(key_buffer, 0, sizeof(key_buffer));
|
||||
return Api::NewError("Failed to initialize AOT patch AES-GCM context.");
|
||||
}
|
||||
size_t decrypted_length = 0;
|
||||
const int decrypt_ok = EVP_AEAD_CTX_open(
|
||||
&ctx, decrypted_payload_buffer.data, &decrypted_length,
|
||||
decrypted_payload_buffer.length, nonce_buffer.data, nonce_buffer.length,
|
||||
sealed_payload.data, sealed_payload.length,
|
||||
reinterpret_cast<const uint8_t*>(aad.buffer()), aad.length());
|
||||
EVP_AEAD_CTX_cleanup(&ctx);
|
||||
memset(key_buffer, 0, sizeof(key_buffer));
|
||||
if (decrypt_ok != 1) {
|
||||
return Api::NewError("AOT patch AES-GCM decryption failed.");
|
||||
}
|
||||
decrypted_payload_buffer.length = decrypted_length;
|
||||
|
||||
uint8_t payload_digest[SHA256_DIGEST_LENGTH];
|
||||
SHA256(decrypted_payload_buffer.data, decrypted_payload_buffer.length,
|
||||
payload_digest);
|
||||
if (!AotPatchDigestEqualsHex(payload_digest, SHA256_DIGEST_LENGTH,
|
||||
payload_sha256)) {
|
||||
return Api::NewError("AOT patch decrypted payload hash mismatch.");
|
||||
}
|
||||
|
||||
// This VM API validates the open artifact envelope and key contract. The
|
||||
// iOS-safe AOT patch model maps patched isolate data/instructions before
|
||||
// isolate startup, so embedders should install the decrypted artifact through
|
||||
// their snapshot mapping path rather than mutating executable code here.
|
||||
*patch_payload_buffer = decrypted_payload_buffer.Release();
|
||||
*patch_payload_length = static_cast<intptr_t>(decrypted_length);
|
||||
return Api::Success();
|
||||
#endif
|
||||
}
|
||||
|
||||
DART_EXPORT void Dart_FreeAotPatchPayload(uint8_t* patch_payload_buffer) {
|
||||
free(patch_payload_buffer);
|
||||
}
|
||||
|
||||
DART_EXPORT bool Dart_IsPrecompiledRuntime() {
|
||||
#if defined(DART_PRECOMPILED_RUNTIME)
|
||||
return true;
|
||||
|
||||
@@ -297,7 +297,8 @@ TEST_CASE(DartAPI_IsolateOwnership) {
|
||||
EXPECT_EQ(false, Dart_GetCurrentThreadOwnsIsolate(ILLEGAL_PORT));
|
||||
|
||||
Dart_ShutdownIsolate();
|
||||
}).join();
|
||||
})
|
||||
.join();
|
||||
|
||||
EXPECT_EQ(true, Dart_GetCurrentThreadOwnsIsolate(port));
|
||||
EXPECT_EQ(false, Dart_GetCurrentThreadOwnsIsolate(other_port));
|
||||
@@ -352,7 +353,8 @@ TEST_CASE_WITH_EXPECTATION(
|
||||
// Causes an assertion failure, because the isolate is already owned by
|
||||
// another thread.
|
||||
Dart_EnterIsolate(isolate);
|
||||
}).join();
|
||||
})
|
||||
.join();
|
||||
|
||||
Dart_EnterIsolate(isolate);
|
||||
}
|
||||
@@ -10913,6 +10915,88 @@ TEST_CASE(Dart_SetFfiNativeResolver_DoesNotResolve) {
|
||||
EXPECT_ERROR(result, "Couldn't resolve function: 'DoesNotResolve'");
|
||||
}
|
||||
|
||||
#if defined(DART_ENABLE_AOT_PATCHING)
|
||||
#if defined(DART_DYNAMIC_MODULES)
|
||||
#error DART_ENABLE_AOT_PATCHING must not require DART_DYNAMIC_MODULES.
|
||||
#endif
|
||||
|
||||
static bool TestAotPatchKeyCallback(const char* key_id,
|
||||
uint8_t* key_buffer,
|
||||
intptr_t key_buffer_length,
|
||||
intptr_t* key_length) {
|
||||
EXPECT_STREQ(key_id, "test-key");
|
||||
EXPECT(key_buffer_length >= 32);
|
||||
memset(key_buffer, 0x42, 32);
|
||||
*key_length = 32;
|
||||
return true;
|
||||
}
|
||||
#endif // defined(DART_ENABLE_AOT_PATCHING)
|
||||
|
||||
TEST_CASE(DartAPI_AotPatchingConfiguration) {
|
||||
const char patch[] = R"json({
|
||||
"format": "open-aot-vmcode-encrypted-v1",
|
||||
"metadata": {
|
||||
"app_id": "app.test",
|
||||
"app_build_id": "1",
|
||||
"base_flavor_id": "free",
|
||||
"base_license_type": "free",
|
||||
"flavor_id": "pro",
|
||||
"license_type": "pro",
|
||||
"sdk_hash": "sdk",
|
||||
"base_snapshot_hash": "cae662172fd450bb0cd710a769079c05bfc5d8e35efa6576edc7d0377afdd4a2",
|
||||
"patch_snapshot_hash": "05d9426b9dd03e5cc3404aab6c7c45ac24e0b90e840f4bd6da83c342430533dc",
|
||||
"target_os": "windows",
|
||||
"target_arch": "x64"
|
||||
},
|
||||
"payload_kind": "full-snapshot",
|
||||
"reconstructed_size": 13,
|
||||
"payload_sha256": "05d9426b9dd03e5cc3404aab6c7c45ac24e0b90e840f4bd6da83c342430533dc",
|
||||
"encrypted_payload_base64": "hNOLmjSh9YbgIyRuVQ==",
|
||||
"encryption": {
|
||||
"algorithm": "AES-256-GCM",
|
||||
"key_id": "test-key",
|
||||
"nonce_base64": "AAAAAAAAAAAAAAAA",
|
||||
"tag_base64": "W8uOg/f+g2SS3Ahxfaeuyg==",
|
||||
"aad_sha256": "1a0c6003fec49bbc26fbaaf0a6dfcd557061b4ae5f22e4b1114afe3b1a8d9796"
|
||||
}
|
||||
})json";
|
||||
Dart_AotPatchInstallOptions options = {};
|
||||
options.app_id = "app.test";
|
||||
options.app_build_id = "1";
|
||||
options.base_flavor_id = "free";
|
||||
options.base_license_type = "free";
|
||||
options.flavor_id = "pro";
|
||||
options.license_type = "pro";
|
||||
options.sdk_hash = "sdk";
|
||||
options.base_snapshot_hash =
|
||||
"cae662172fd450bb0cd710a769079c05bfc5d8e35efa6576edc7d0377afdd4a2";
|
||||
options.patch_snapshot_hash =
|
||||
"05d9426b9dd03e5cc3404aab6c7c45ac24e0b90e840f4bd6da83c342430533dc";
|
||||
options.target_os = "windows";
|
||||
options.target_arch = "x64";
|
||||
|
||||
uint8_t* patch_payload = nullptr;
|
||||
intptr_t patch_payload_length = 0;
|
||||
#if defined(DART_ENABLE_AOT_PATCHING)
|
||||
EXPECT(Dart_AotPatchingEnabled());
|
||||
Dart_SetAotPatchKeyCallback(TestAotPatchKeyCallback);
|
||||
Dart_Handle result = Dart_InstallAotPatch(
|
||||
reinterpret_cast<const uint8_t*>(patch), strlen(patch), &options,
|
||||
&patch_payload, &patch_payload_length);
|
||||
EXPECT_VALID(result);
|
||||
EXPECT_EQ(13, patch_payload_length);
|
||||
EXPECT_EQ(0, memcmp("patch-payload", patch_payload, patch_payload_length));
|
||||
Dart_FreeAotPatchPayload(patch_payload);
|
||||
Dart_SetAotPatchKeyCallback(nullptr);
|
||||
#else
|
||||
EXPECT(!Dart_AotPatchingEnabled());
|
||||
Dart_Handle result = Dart_InstallAotPatch(
|
||||
reinterpret_cast<const uint8_t*>(patch), strlen(patch), &options,
|
||||
&patch_payload, &patch_payload_length);
|
||||
EXPECT_ERROR(result, "Compact AOT patching is not enabled");
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE(DartAPI_UserTags) {
|
||||
Dart_Handle default_tag = Dart_GetDefaultUserTag();
|
||||
EXPECT_VALID(default_tag);
|
||||
|
||||
@@ -2078,7 +2078,7 @@ SwitchDispatch:
|
||||
#undef TARGET
|
||||
#endif // !defined(PRODUCT)
|
||||
default:
|
||||
FATAL1("Undefined opcode: %d\n", op);
|
||||
FATAL("Undefined opcode: %d\n", op);
|
||||
}
|
||||
#if !defined(PRODUCT)
|
||||
SwitchDispatchNoSingleStep:
|
||||
@@ -2089,7 +2089,7 @@ SwitchDispatchNoSingleStep:
|
||||
KERNEL_BYTECODES_LIST(TARGET)
|
||||
#undef TARGET
|
||||
default:
|
||||
FATAL1("Undefined opcode: %d\n", op);
|
||||
FATAL("Undefined opcode: %d\n", op);
|
||||
}
|
||||
#endif // !defined(PRODUCT)
|
||||
#endif // defined(DART_HAS_COMPUTED_GOTO)
|
||||
|
||||
Reference in New Issue
Block a user