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:
Tony
2026-06-24 03:00:14 +08:00
parent c3280addfc
commit 08139af589
14 changed files with 1188 additions and 23 deletions
+6
View File
@@ -633,6 +633,12 @@ if (is_win) {
default_warning_flags += [ "/WX" ] # Treat warnings as errors. 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) { if (is_clang) {
default_warning_flags += [ default_warning_flags += [
"-Wno-deprecated-declarations", # crashpad "-Wno-deprecated-declarations", # crashpad
+7 -1
View File
@@ -17,6 +17,11 @@ import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 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 # A regex matching an argument corresponding to the output filename passed to
# link.exe. # link.exe.
_LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE) _LINK_EXE_OUT_ARG = re.compile('/OUT:(?P<out>.+)$', re.IGNORECASE)
@@ -142,7 +147,8 @@ class WinTool(object):
env=env, env=env,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, 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 # Read output one line at a time as it shows up to avoid OOM failures when
# GBs of output is produced. # GBs of output is produced.
for line in link.stdout: for line in link.stdout:
+20 -7
View File
@@ -67,6 +67,7 @@ SDK_VERSION = '10.0.26100.0'
# which makes a difference for the arm64 runtime. # which makes a difference for the arm64 runtime.
# The second number is an alternate version number, only used in an error string # The second number is an alternate version number, only used in an error string
MSVS_VERSIONS = collections.OrderedDict([ 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. ('2022', '17.0'), # The VS version in our packaged toolchain.
('2019', '16.0'), ('2019', '16.0'),
('2017', '15.0'), ('2017', '15.0'),
@@ -75,11 +76,20 @@ MSVS_VERSIONS = collections.OrderedDict([
# List of preferred VC toolset version based on MSVS # List of preferred VC toolset version based on MSVS
# Order is not relevant for this dictionary. # Order is not relevant for this dictionary.
MSVC_TOOLSET_VERSION = { MSVC_TOOLSET_VERSION = {
'2026': 'VC145',
'2022': 'VC143', '2022': 'VC143',
'2019': 'VC142', '2019': 'VC142',
'2017': 'VC141', '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__)) script_dir = os.path.dirname(os.path.realpath(__file__))
json_data_file = os.path.join(script_dir, 'win_toolchain.json') json_data_file = os.path.join(script_dir, 'win_toolchain.json')
@@ -212,7 +222,8 @@ def GetVisualStudioVersion():
else: else:
program_files_path_variable = '%ProgramFiles(x86)%' program_files_path_variable = '%ProgramFiles(x86)%'
path = os.path.expandvars(program_files_path_variable + path = os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s' % version) '/Microsoft Visual Studio/%s' %
VisualStudioFolderName(version))
if path and any( if path and any(
os.path.exists(os.path.join(path, edition)) os.path.exists(os.path.join(path, edition))
for edition in ('Enterprise', 'Professional', 'Community', 'Preview', for edition in ('Enterprise', 'Professional', 'Community', 'Preview',
@@ -242,24 +253,25 @@ def DetectVisualStudioPath():
program_files_path_variable = '%ProgramFiles%' program_files_path_variable = '%ProgramFiles%'
else: else:
program_files_path_variable = '%ProgramFiles(x86)%' program_files_path_variable = '%ProgramFiles(x86)%'
folder_name = VisualStudioFolderName(version_as_year)
for path in (os.environ.get('vs%s_install' % version_as_year), for path in (os.environ.get('vs%s_install' % version_as_year),
os.path.expandvars(program_files_path_variable + os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s/Enterprise' % '/Microsoft Visual Studio/%s/Enterprise' %
version_as_year), folder_name),
os.path.expandvars(program_files_path_variable + os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s/Professional' % '/Microsoft Visual Studio/%s/Professional' %
version_as_year), folder_name),
os.path.expandvars(program_files_path_variable + os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s/Community' % '/Microsoft Visual Studio/%s/Community' %
version_as_year), folder_name),
os.path.expandvars(program_files_path_variable + os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s/Preview' % '/Microsoft Visual Studio/%s/Preview' %
version_as_year), folder_name),
os.path.expandvars(program_files_path_variable + os.path.expandvars(program_files_path_variable +
'/Microsoft Visual Studio/%s/BuildTools' % '/Microsoft Visual Studio/%s/BuildTools' %
version_as_year)): folder_name)):
if path and os.path.exists(path): if path and os.path.exists(path):
return path return path.strip()
raise Exception('Visual Studio Version %s not found.' % version_as_year) 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): def NormalizePath(path):
path = path.strip()
while path.endswith('\\'): while path.endswith('\\'):
path = path[:-1] path = path[:-1]
return path return path
+109 -7
View File
@@ -38,7 +38,11 @@ void alsoVerySecretFoo() {
} }
"""); """);
List<MappingPair> mapping = getSnapshotMap(tmpDir, secretfilenameFile); List<MappingPair> mapping = getSnapshotMap(
tmpDir,
secretfilenameFile,
outputPrefix: "base",
);
bool good = verify( bool good = verify(
mapping, mapping,
{ {
@@ -55,13 +59,51 @@ void alsoVerySecretFoo() {
}, },
); );
if (!good) throw "Obfuscation didn't work as expected"; 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"); print("Good");
} finally { } finally {
tmpDir.deleteSync(recursive: true); 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 Uri genKernel = Platform.script.resolve('../bin/gen_kernel.dart');
final File genKernelFile = new File.fromUri(genKernel); final File genKernelFile = new File.fromUri(genKernel);
if (!genKernelFile.existsSync()) { if (!genKernelFile.existsSync()) {
@@ -96,7 +138,7 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
final Uri tmpDirUri = tmpDir.uri; 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); final File kernelDillFile = new File.fromUri(kernelDill);
print("Running gen_kernel"); print("Running gen_kernel");
@@ -120,14 +162,14 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
"stderr: ${kernelRun.stderr}"; "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 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); final File obfuscationMapFile = new File.fromUri(obfuscationMap);
print("Running $genSnapshot"); print("Running $genSnapshot");
// Extracted from pkg/dart2native/lib/dart2native.dart. // Extracted from pkg/dart2native/lib/dart2native.dart.
final ProcessResult snapshotRun = Process.runSync(genSnapshotFile.path, [ final List<String> genSnapshotArgs = [
"--snapshot-kind=app-aot-elf", "--snapshot-kind=app-aot-elf",
"--elf=${aotElfFile.path}", "--elf=${aotElfFile.path}",
"--dwarf-stack-traces", "--dwarf-stack-traces",
@@ -135,7 +177,17 @@ List<MappingPair> getSnapshotMap(Directory tmpDir, File compileDartFile) {
"--strip", "--strip",
"--save-obfuscation-map=${obfuscationMapFile.path}", "--save-obfuscation-map=${obfuscationMapFile.path}",
kernelDillFile.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) { if (snapshotRun.exitCode != 0) {
throw "Got exit code ${snapshotRun.exitCode}\n" throw "Got exit code ${snapshotRun.exitCode}\n"
@@ -157,6 +209,10 @@ List<MappingPair> readJsonMapping(File file) {
return result; return result;
} }
Map<String, String> toMap(List<MappingPair> mapping) {
return {for (MappingPair pair in mapping) pair.from: pair.to};
}
class MappingPair { class MappingPair {
final String from; final String from;
final String to; final String to;
@@ -200,3 +256,49 @@ bool verify(
} }
return good; 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;
}
+13
View File
@@ -8,6 +8,9 @@ import("runtime_args.gni")
import("//build/config/sysroot.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") { config("dart_public_config") {
include_dirs = [ include_dirs = [
".", ".",
@@ -231,6 +234,10 @@ config("dart_config") {
defines += [ "DART_DYNAMIC_MODULES" ] defines += [ "DART_DYNAMIC_MODULES" ]
} }
if (dart_enable_aot_patching) {
defines += [ "DART_ENABLE_AOT_PATCHING" ]
}
if (include_experimental_vm_service) { if (include_experimental_vm_service) {
defines += [ "EXPERIMENTAL_VM_SERVICE" ] defines += [ "EXPERIMENTAL_VM_SERVICE" ]
} }
@@ -371,6 +378,9 @@ library_for_all_configs("libdart") {
if (dart_support_perfetto) { if (dart_support_perfetto) {
extra_deps += [ "//third_party/perfetto:libprotozero" ] extra_deps += [ "//third_party/perfetto:libprotozero" ]
} }
if (dart_enable_aot_patching) {
extra_deps += [ "//third_party/boringssl" ]
}
if (is_fuchsia) { if (is_fuchsia) {
extra_deps += [ extra_deps += [
"$fuchsia_sdk/pkg/fdio", "$fuchsia_sdk/pkg/fdio",
@@ -385,6 +395,9 @@ library_for_all_configs("libdart") {
compiler_lib = "vm:libdart_compiler" compiler_lib = "vm:libdart_compiler"
extra_configs = [ ":dart_shared_lib" ] extra_configs = [ ":dart_shared_lib" ]
include_dirs = [ "." ] include_dirs = [ "." ]
if (dart_enable_aot_patching) {
include_dirs += [ "//third_party/boringssl/src/include" ]
}
public_configs = [ ":dart_public_config" ] public_configs = [ ":dart_public_config" ]
sources = [ sources = [
"$target_gen_dir/version.cc", "$target_gen_dir/version.cc",
+222
View File
@@ -112,6 +112,7 @@ static const char* const kSnapshotKindNames[] = {
V(loading_unit_manifest, loading_unit_manifest_filename) \ V(loading_unit_manifest, loading_unit_manifest_filename) \
V(save_debugging_info, debugging_info_filename) \ V(save_debugging_info, debugging_info_filename) \
V(save_obfuscation_map, obfuscation_map_filename) \ V(save_obfuscation_map, obfuscation_map_filename) \
V(load_obfuscation_map, load_obfuscation_map_filename) \
V(ffi_callback_stub, ffi_callback_stub_filename) V(ffi_callback_stub, ffi_callback_stub_filename)
#define BOOL_OPTIONS_LIST(V) \ #define BOOL_OPTIONS_LIST(V) \
@@ -168,6 +169,7 @@ static void PrintUsage() {
"[--obfuscate] \n" "[--obfuscate] \n"
"[--save-debugging-info=<debug-filename>] \n" "[--save-debugging-info=<debug-filename>] \n"
"[--save-obfuscation-map=<map-filename>] \n" "[--save-obfuscation-map=<map-filename>] \n"
"[--load-obfuscation-map=<map-filename>] \n"
"<dart-kernel-file> \n" "<dart-kernel-file> \n"
" \n" " \n"
"To create an AOT application snapshot as an ELF shared library: \n" "To create an AOT application snapshot as an ELF shared library: \n"
@@ -177,6 +179,7 @@ static void PrintUsage() {
"[--obfuscate] \n" "[--obfuscate] \n"
"[--save-debugging-info=<debug-filename>] \n" "[--save-debugging-info=<debug-filename>] \n"
"[--save-obfuscation-map=<map-filename>] \n" "[--save-obfuscation-map=<map-filename>] \n"
"[--load-obfuscation-map=<map-filename>] \n"
"<dart-kernel-file> \n" "<dart-kernel-file> \n"
" \n" " \n"
"To create an AOT application snapshot as an Mach-O dynamic library (dylib): \n" "To create an AOT application snapshot as an Mach-O dynamic library (dylib): \n"
@@ -186,6 +189,7 @@ static void PrintUsage() {
"[--obfuscate] \n" "[--obfuscate] \n"
"[--save-debugging-info=<debug-filename>] \n" "[--save-debugging-info=<debug-filename>] \n"
"[--save-obfuscation-map=<map-filename>] \n" "[--save-obfuscation-map=<map-filename>] \n"
"[--load-obfuscation-map=<map-filename>] \n"
"<dart-kernel-file> \n" "<dart-kernel-file> \n"
" \n" " \n"
"AOT snapshots can be obfuscated: that is all identifiers will be renamed \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"); "obfuscation is enabled by the --obfuscate flag.\n\n");
return -1; 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 (!IsSnapshottingForPrecompilation()) {
if (obfuscate) { if (obfuscate) {
@@ -335,6 +345,13 @@ static int ParseArguments(int argc,
return -1; 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) { if (strip) {
Syslog::PrintErr( Syslog::PrintErr(
"Stripping can only be enabled when building an AOT snapshot.\n\n"); "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); 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) { static void MaybeLoadExtraInputs(const CommandLineOptions& inputs) {
for (intptr_t i = 1; i < inputs.count(); i++) { for (intptr_t i = 1; i < inputs.count(); i++) {
uint8_t* buffer = nullptr; uint8_t* buffer = nullptr;
@@ -656,6 +876,8 @@ static void CreateAndWritePrecompiledSnapshot() {
ASSERT(kind_str != nullptr); ASSERT(kind_str != nullptr);
ASSERT(filename != nullptr); ASSERT(filename != nullptr);
MaybeLoadObfuscationMap();
// Precompile with specified embedder entry points // Precompile with specified embedder entry points
Dart_Handle result = Dart_Precompile(); Dart_Handle result = Dart_Precompile();
CHECK_RESULT(result); CHECK_RESULT(result);
+76
View File
@@ -4241,6 +4241,82 @@ Dart_CreateAppJITSnapshotAsBlobs(uint8_t** snapshot_data_buffer,
DART_EXPORT DART_API_WARN_UNUSED_RESULT Dart_Handle DART_EXPORT DART_API_WARN_UNUSED_RESULT Dart_Handle
Dart_GetObfuscationMap(uint8_t** buffer, intptr_t* buffer_length); 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 * 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 * not from any other kind of snapshot or from source (that is, the VM was
+4
View File
@@ -74,6 +74,10 @@ declare_args() {
# Whether to support dynamic loading and interpretation of Dart bytecode. # Whether to support dynamic loading and interpretation of Dart bytecode.
dart_dynamic_modules = false 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() { 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 = [ var expectedSymbols = [
"Dart_AddSymbols", "Dart_AddSymbols",
"Dart_AotPatchingEnabled",
"Dart_Allocate", "Dart_Allocate",
"Dart_AllocateWithNativeFields", "Dart_AllocateWithNativeFields",
"Dart_BooleanValue", "Dart_BooleanValue",
@@ -111,6 +112,7 @@ main() {
"Dart_False", "Dart_False",
"Dart_FinalizeAllClasses", "Dart_FinalizeAllClasses",
"Dart_FinalizeLoading", "Dart_FinalizeLoading",
"Dart_FreeAotPatchPayload",
"Dart_FunctionIsStatic", "Dart_FunctionIsStatic",
"Dart_FunctionName", "Dart_FunctionName",
"Dart_FunctionOwner", "Dart_FunctionOwner",
@@ -158,6 +160,7 @@ main() {
"Dart_IdentityEquals", "Dart_IdentityEquals",
"Dart_Initialize", "Dart_Initialize",
"Dart_InitializeNativeAssetsResolver", "Dart_InitializeNativeAssetsResolver",
"Dart_InstallAotPatch",
"Dart_InstanceGetType", "Dart_InstanceGetType",
"Dart_IntegerFitsIntoInt64", "Dart_IntegerFitsIntoInt64",
"Dart_IntegerFitsIntoUint64", "Dart_IntegerFitsIntoUint64",
@@ -296,6 +299,7 @@ main() {
"Dart_SendPortGetId", "Dart_SendPortGetId",
"Dart_SendPortGetIdEx", "Dart_SendPortGetIdEx",
"Dart_ServiceSendDataEvent", "Dart_ServiceSendDataEvent",
"Dart_SetAotPatchKeyCallback",
"Dart_SetBooleanReturnValue", "Dart_SetBooleanReturnValue",
"Dart_SetCurrentUserTag", "Dart_SetCurrentUserTag",
"Dart_SetDartLibrarySourcesKernel", "Dart_SetDartLibrarySourcesKernel",
@@ -314,6 +318,7 @@ main() {
"Dart_SetMessageNotifyCallback", "Dart_SetMessageNotifyCallback",
"Dart_SetNativeInstanceField", "Dart_SetNativeInstanceField",
"Dart_SetNativeResolver", "Dart_SetNativeResolver",
"Dart_SetObfuscationMap",
"Dart_SetPausedOnExit", "Dart_SetPausedOnExit",
"Dart_SetPausedOnStart", "Dart_SetPausedOnStart",
"Dart_SetPeer", "Dart_SetPeer",
@@ -3070,7 +3070,7 @@ void StubCodeCompiler::GenerateInterpretCallStub() {
__ movq(CallingConventions::kArg3Reg, R11); // Negative argc. __ movq(CallingConventions::kArg3Reg, R11); // Negative argc.
__ movq(CallingConventions::kArg4Reg, R12); // Argv. __ movq(CallingConventions::kArg4Reg, R12); // Argv.
#if defined(TARGET_OS_WINDOWS) #if defined(DART_TARGET_OS_WINDOWS)
__ movq(Address(RSP, 0 * target::kWordSize), THR); // Thread. __ movq(Address(RSP, 0 * target::kWordSize), THR); // Thread.
#else #else
__ movq(CallingConventions::kArg5Reg, THR); // Thread. __ movq(CallingConventions::kArg5Reg, THR); // Thread.
+613 -3
View File
@@ -9,6 +9,11 @@
#include <memory> #include <memory>
#include <utility> #include <utility>
#if defined(DART_ENABLE_AOT_PATCHING)
#include <openssl/aead.h>
#include <openssl/sha.h>
#endif
#include "lib/stacktrace.h" #include "lib/stacktrace.h"
#include "platform/address_sanitizer.h" #include "platform/address_sanitizer.h"
#include "platform/assert.h" #include "platform/assert.h"
@@ -16,6 +21,7 @@
#include "platform/thread_sanitizer.h" #include "platform/thread_sanitizer.h"
#include "platform/unicode.h" #include "platform/unicode.h"
#include "vm/app_snapshot.h" #include "vm/app_snapshot.h"
#include "vm/base64.h"
#include "vm/bytecode_reader.h" #include "vm/bytecode_reader.h"
#include "vm/class_finalizer.h" #include "vm/class_finalizer.h"
#include "vm/compiler/jit/compiler.h" #include "vm/compiler/jit/compiler.h"
@@ -6721,9 +6727,10 @@ static void CreateAppAOTSnapshotHelper(
}; };
Dwarf* const dwarf = Dwarf* const dwarf =
(format == Dart_AotBinaryFormat_Assembly || strip) ? nullptr (format == Dart_AotBinaryFormat_Assembly || strip)
: generate_debug ? debug_dwarf ? nullptr
: new (Z) Dwarf(Z, deobfuscation_trie, identifier); : generate_debug ? debug_dwarf
: new (Z) Dwarf(Z, deobfuscation_trie, identifier);
SharedObjectWriter* so = nullptr; SharedObjectWriter* so = nullptr;
if (format == Dart_AotBinaryFormat_Elf) { if (format == Dart_AotBinaryFormat_Elf) {
so = new (Z) so = new (Z)
@@ -7122,6 +7129,126 @@ Dart_CreateAppJITSnapshotAsBlobs(uint8_t** isolate_snapshot_data_buffer,
#endif #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, DART_EXPORT Dart_Handle Dart_GetObfuscationMap(uint8_t** buffer,
intptr_t* buffer_length) { intptr_t* buffer_length) {
#if defined(DART_PRECOMPILED_RUNTIME) #if defined(DART_PRECOMPILED_RUNTIME)
@@ -7163,6 +7290,489 @@ DART_EXPORT Dart_Handle Dart_GetObfuscationMap(uint8_t** buffer,
#endif #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, &current_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() { DART_EXPORT bool Dart_IsPrecompiledRuntime() {
#if defined(DART_PRECOMPILED_RUNTIME) #if defined(DART_PRECOMPILED_RUNTIME)
return true; return true;
+86 -2
View File
@@ -297,7 +297,8 @@ TEST_CASE(DartAPI_IsolateOwnership) {
EXPECT_EQ(false, Dart_GetCurrentThreadOwnsIsolate(ILLEGAL_PORT)); EXPECT_EQ(false, Dart_GetCurrentThreadOwnsIsolate(ILLEGAL_PORT));
Dart_ShutdownIsolate(); Dart_ShutdownIsolate();
}).join(); })
.join();
EXPECT_EQ(true, Dart_GetCurrentThreadOwnsIsolate(port)); EXPECT_EQ(true, Dart_GetCurrentThreadOwnsIsolate(port));
EXPECT_EQ(false, Dart_GetCurrentThreadOwnsIsolate(other_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 // Causes an assertion failure, because the isolate is already owned by
// another thread. // another thread.
Dart_EnterIsolate(isolate); Dart_EnterIsolate(isolate);
}).join(); })
.join();
Dart_EnterIsolate(isolate); Dart_EnterIsolate(isolate);
} }
@@ -10913,6 +10915,88 @@ TEST_CASE(Dart_SetFfiNativeResolver_DoesNotResolve) {
EXPECT_ERROR(result, "Couldn't resolve function: '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) { TEST_CASE(DartAPI_UserTags) {
Dart_Handle default_tag = Dart_GetDefaultUserTag(); Dart_Handle default_tag = Dart_GetDefaultUserTag();
EXPECT_VALID(default_tag); EXPECT_VALID(default_tag);
+2 -2
View File
@@ -2078,7 +2078,7 @@ SwitchDispatch:
#undef TARGET #undef TARGET
#endif // !defined(PRODUCT) #endif // !defined(PRODUCT)
default: default:
FATAL1("Undefined opcode: %d\n", op); FATAL("Undefined opcode: %d\n", op);
} }
#if !defined(PRODUCT) #if !defined(PRODUCT)
SwitchDispatchNoSingleStep: SwitchDispatchNoSingleStep:
@@ -2089,7 +2089,7 @@ SwitchDispatchNoSingleStep:
KERNEL_BYTECODES_LIST(TARGET) KERNEL_BYTECODES_LIST(TARGET)
#undef TARGET #undef TARGET
default: default:
FATAL1("Undefined opcode: %d\n", op); FATAL("Undefined opcode: %d\n", op);
} }
#endif // !defined(PRODUCT) #endif // !defined(PRODUCT)
#endif // defined(DART_HAS_COMPUTED_GOTO) #endif // defined(DART_HAS_COMPUTED_GOTO)