From 08139af589d0d32fbdec64b127c075ea3427cde8 Mon Sep 17 00:00:00 2001 From: Tony Date: Wed, 24 Jun 2026 03:00:14 +0800 Subject: [PATCH] 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. --- build/config/compiler/BUILD.gn | 6 + build/toolchain/win/tool_wrapper.py | 8 +- build/vs_toolchain.py | 27 +- pkg/vm/test/obfuscation_test.dart | 116 +++- runtime/BUILD.gn | 13 + runtime/bin/gen_snapshot.cc | 222 +++++++ runtime/include/dart_api.h | 76 +++ runtime/runtime_args.gni | 4 + .../vm/dart/aot_patching_enabled_test.dart | 24 + .../tests/vm/dart/exported_symbols_test.dart | 5 + runtime/vm/compiler/stub_code_compiler_x64.cc | 2 +- runtime/vm/dart_api_impl.cc | 616 +++++++++++++++++- runtime/vm/dart_api_impl_test.cc | 88 ++- runtime/vm/interpreter.cc | 4 +- 14 files changed, 1188 insertions(+), 23 deletions(-) create mode 100644 runtime/tests/vm/dart/aot_patching_enabled_test.dart diff --git a/build/config/compiler/BUILD.gn b/build/config/compiler/BUILD.gn index 18feb4698f9..898c4091e54 100644 --- a/build/config/compiler/BUILD.gn +++ b/build/config/compiler/BUILD.gn @@ -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 diff --git a/build/toolchain/win/tool_wrapper.py b/build/toolchain/win/tool_wrapper.py index 3f52ee0c87d..42d9ef364aa 100644 --- a/build/toolchain/win/tool_wrapper.py +++ b/build/toolchain/win/tool_wrapper.py @@ -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.+)$', 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: diff --git a/build/vs_toolchain.py b/build/vs_toolchain.py index ee033b367f3..661be7ca027 100644 --- a/build/vs_toolchain.py +++ b/build/vs_toolchain.py @@ -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 diff --git a/pkg/vm/test/obfuscation_test.dart b/pkg/vm/test/obfuscation_test.dart index 0a267756d33..dce5fda5b20 100644 --- a/pkg/vm/test/obfuscation_test.dart +++ b/pkg/vm/test/obfuscation_test.dart @@ -38,7 +38,11 @@ void alsoVerySecretFoo() { } """); - List mapping = getSnapshotMap(tmpDir, secretfilenameFile); + List 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 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 getSnapshotMap(Directory tmpDir, File compileDartFile) { +List 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 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 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 genSnapshotArgs = [ "--snapshot-kind=app-aot-elf", "--elf=${aotElfFile.path}", "--dwarf-stack-traces", @@ -135,7 +177,17 @@ List 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 readJsonMapping(File file) { return result; } +Map toMap(List 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 baseMapping, + List patchMapping, + String patchOnlyName, +) { + bool good = true; + final Map 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 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; +} diff --git a/runtime/BUILD.gn b/runtime/BUILD.gn index 5c3eb760675..508413c138f 100644 --- a/runtime/BUILD.gn +++ b/runtime/BUILD.gn @@ -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", diff --git a/runtime/bin/gen_snapshot.cc b/runtime/bin/gen_snapshot.cc index ff72e9b8f39..e3079280842 100644 --- a/runtime/bin/gen_snapshot.cc +++ b/runtime/bin/gen_snapshot.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=] \n" "[--save-obfuscation-map=] \n" +"[--load-obfuscation-map=] \n" " \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=] \n" "[--save-obfuscation-map=] \n" +"[--load-obfuscation-map=] \n" " \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=] \n" "[--save-obfuscation-map=] \n" +"[--load-obfuscation-map=] \n" " \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(code_point); + } else if (code_point <= 0x7FF) { + buffer[(*length)++] = static_cast(0xC0 | (code_point >> 6)); + buffer[(*length)++] = static_cast(0x80 | (code_point & 0x3F)); + } else if (code_point <= 0xFFFF) { + buffer[(*length)++] = static_cast(0xE0 | (code_point >> 12)); + buffer[(*length)++] = static_cast(0x80 | ((code_point >> 6) & 0x3F)); + buffer[(*length)++] = static_cast(0x80 | (code_point & 0x3F)); + } else { + buffer[(*length)++] = static_cast(0xF0 | (code_point >> 18)); + buffer[(*length)++] = static_cast(0x80 | ((code_point >> 12) & 0x3F)); + buffer[(*length)++] = static_cast(0x80 | ((code_point >> 6) & 0x3F)); + buffer[(*length)++] = static_cast(0x80 | (code_point & 0x3F)); + } +} + +static bool ParseJsonStringArray(const uint8_t* buffer, + intptr_t size, + MallocGrowableArray* 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(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(c); + continue; + } + if (cursor >= size) { + free(value); + *error = "unterminated escape"; + return false; + } + c = buffer[cursor++]; + switch (c) { + case '"': + case '\\': + case '/': + value[length++] = static_cast(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 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(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(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(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); diff --git a/runtime/include/dart_api.h b/runtime/include/dart_api.h index 73a18b45a71..225e4ccabcd 100644 --- a/runtime/include/dart_api.h +++ b/runtime/include/dart_api.h @@ -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 diff --git a/runtime/runtime_args.gni b/runtime/runtime_args.gni index d465b34fdfb..f78139be8c7 100644 --- a/runtime/runtime_args.gni +++ b/runtime/runtime_args.gni @@ -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() { diff --git a/runtime/tests/vm/dart/aot_patching_enabled_test.dart b/runtime/tests/vm/dart/aot_patching_enabled_test.dart new file mode 100644 index 00000000000..7e7ade17873 --- /dev/null +++ b/runtime/tests/vm/dart/aot_patching_enabled_test.dart @@ -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( + '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); +} diff --git a/runtime/tests/vm/dart/exported_symbols_test.dart b/runtime/tests/vm/dart/exported_symbols_test.dart index f820fbedcff..3eb1c671534 100644 --- a/runtime/tests/vm/dart/exported_symbols_test.dart +++ b/runtime/tests/vm/dart/exported_symbols_test.dart @@ -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", diff --git a/runtime/vm/compiler/stub_code_compiler_x64.cc b/runtime/vm/compiler/stub_code_compiler_x64.cc index 9a29d3e367f..2f147baab1e 100644 --- a/runtime/vm/compiler/stub_code_compiler_x64.cc +++ b/runtime/vm/compiler/stub_code_compiler_x64.cc @@ -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. diff --git a/runtime/vm/dart_api_impl.cc b/runtime/vm/dart_api_impl.cc index 77e6df1b869..586c517f7ef 100644 --- a/runtime/vm/dart_api_impl.cc +++ b/runtime/vm/dart_api_impl.cc @@ -9,6 +9,11 @@ #include #include +#if defined(DART_ENABLE_AOT_PATCHING) +#include +#include +#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(16)); + ObfuscationMap renames( + HashTables::New(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(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(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(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((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(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(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( + malloc(Utils::Maximum(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( + malloc(Utils::Maximum(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(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(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; diff --git a/runtime/vm/dart_api_impl_test.cc b/runtime/vm/dart_api_impl_test.cc index 711ac7f45bb..d2e0c746680 100644 --- a/runtime/vm/dart_api_impl_test.cc +++ b/runtime/vm/dart_api_impl_test.cc @@ -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(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(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); diff --git a/runtime/vm/interpreter.cc b/runtime/vm/interpreter.cc index 2ff17e415bd..fee078fb472 100644 --- a/runtime/vm/interpreter.cc +++ b/runtime/vm/interpreter.cc @@ -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)