Skip to content

Commit 785193e

Browse files
trop[bot]indutny-signalVerteDindeckerr
authored
fix: don't copy 'package.json's out of ASAR file (#46478)
* fix: don't copy 'package.json's out of ASAR file New Node.js module resolution system reads `package.json` from imported modules by reading from the file natively in C++ without calling into `fs.readFileSync`. The ASAR FS wrapper code had copied files out into a temporary folder as a workaround, but it is inefficient and does not cover all module resolution mechanisms in Node.js. In this change we expose `overrideReadFileSync` method on the `modules` binding in Node.js, and use this override to call into ASAR-supporting `fs.readFileSync`. Co-authored-by: Fedor Indutny <[email protected]> * chore: remove erroneous patch * chore: re-add line ending --------- Co-authored-by: trop[bot] <37223003+trop[bot]@users.noreply.github.com> Co-authored-by: Fedor Indutny <[email protected]> Co-authored-by: Keeley Hammond <[email protected]> Co-authored-by: Charles Kerr <[email protected]>
1 parent 3e48104 commit 785193e

File tree

3 files changed

+146
-20
lines changed

3 files changed

+146
-20
lines changed

lib/node/asar-fs-wrapper.ts

+22-20
Original file line numberDiff line numberDiff line change
@@ -667,10 +667,10 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
667667
return p(pathArgument, options);
668668
};
669669

670-
const { readFileSync } = fs;
671-
fs.readFileSync = function (pathArgument: string, options: any) {
672-
const pathInfo = splitPath(pathArgument);
673-
if (!pathInfo.isAsar) return readFileSync.apply(this, arguments);
670+
function readFileFromArchiveSync (
671+
pathInfo: { asarPath: string; filePath: string },
672+
options: any
673+
): ReturnType<typeof readFileSync> {
674674
const { asarPath, filePath } = pathInfo;
675675

676676
const archive = getOrCreateArchive(asarPath);
@@ -704,6 +704,14 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
704704
fs.readSync(fd, buffer, 0, info.size, info.offset);
705705
validateBufferIntegrity(buffer, info.integrity);
706706
return (encoding) ? buffer.toString(encoding) : buffer;
707+
}
708+
709+
const { readFileSync } = fs;
710+
fs.readFileSync = function (pathArgument: string, options: any) {
711+
const pathInfo = splitPath(pathArgument);
712+
if (!pathInfo.isAsar) return readFileSync.apply(this, arguments);
713+
714+
return readFileFromArchiveSync(pathInfo, options);
707715
};
708716

709717
type ReaddirOptions = { encoding: BufferEncoding | null; withFileTypes?: false, recursive?: false } | undefined | null;
@@ -980,25 +988,19 @@ export const wrapFsWithAsar = (fs: Record<string, any>) => {
980988
};
981989

982990
const modBinding = internalBinding('modules');
983-
const { readPackageJSON } = modBinding;
984-
internalBinding('modules').readPackageJSON = (
985-
jsonPath: string,
986-
isESM: boolean,
987-
base: undefined | string,
988-
specifier: undefined | string
989-
) => {
991+
modBinding.overrideReadFileSync((jsonPath: string): Buffer | false | undefined => {
990992
const pathInfo = splitPath(jsonPath);
991-
if (!pathInfo.isAsar) return readPackageJSON(jsonPath, isESM, base, specifier);
992-
const { asarPath, filePath } = pathInfo;
993993

994-
const archive = getOrCreateArchive(asarPath);
995-
if (!archive) return undefined;
994+
// Fallback to Node.js internal implementation
995+
if (!pathInfo.isAsar) return undefined;
996996

997-
const realPath = archive.copyFileOut(filePath);
998-
if (!realPath) return undefined;
999-
1000-
return readPackageJSON(realPath, isESM, base, specifier);
1001-
};
997+
try {
998+
return readFileFromArchiveSync(pathInfo, undefined);
999+
} catch {
1000+
// Not found
1001+
return false;
1002+
}
1003+
});
10021004

10031005
const { internalModuleStat } = binding;
10041006
internalBinding('fs').internalModuleStat = (receiver: unknown, pathArgument: string) => {

patches/node/.patches

+1
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,4 @@ build_option_to_use_custom_inspector_protocol_path.patch
4747
feat_add_oom_error_callback_in_node_isolatesettings.patch
4848
fix_ensure_traverseparent_bails_on_resource_path_exit.patch
4949
zlib_fix_pointer_alignment.patch
50+
fix_expose_readfilesync_override_for_modules.patch
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
2+
From: Fedor Indutny <[email protected]>
3+
Date: Mon, 31 Mar 2025 11:21:29 -0700
4+
Subject: fix: expose ReadFileSync override for modules
5+
6+
To avoid copying out `package.json` files out of the ASAR file we need
7+
an API override to replace the native `ReadFileSync` in the `modules`
8+
binding.
9+
10+
diff --git a/src/env_properties.h b/src/env_properties.h
11+
index 9f89823170782242093bc5ee0df6a2a2ef5b919f..b9374ee1acceb3d0aab51c6c5ae6a79be1cc71a9 100644
12+
--- a/src/env_properties.h
13+
+++ b/src/env_properties.h
14+
@@ -478,6 +478,7 @@
15+
V(maybe_cache_generated_source_map, v8::Function) \
16+
V(messaging_deserialize_create_object, v8::Function) \
17+
V(message_port, v8::Object) \
18+
+ V(modules_read_file_sync, v8::Function) \
19+
V(builtin_module_require, v8::Function) \
20+
V(performance_entry_callback, v8::Function) \
21+
V(prepare_stack_trace_callback, v8::Function) \
22+
diff --git a/src/node_modules.cc b/src/node_modules.cc
23+
index 4e9f70a1c41b44d2a1863b778d4f1e37279178d9..c56a32885b8debbd5b95a2c11f3838d4088e05ac 100644
24+
--- a/src/node_modules.cc
25+
+++ b/src/node_modules.cc
26+
@@ -21,6 +21,7 @@ namespace modules {
27+
28+
using v8::Array;
29+
using v8::Context;
30+
+using v8::Function;
31+
using v8::FunctionCallbackInfo;
32+
using v8::HandleScope;
33+
using v8::Isolate;
34+
@@ -88,6 +89,7 @@ Local<Array> BindingData::PackageConfig::Serialize(Realm* realm) const {
35+
36+
const BindingData::PackageConfig* BindingData::GetPackageJSON(
37+
Realm* realm, std::string_view path, ErrorContext* error_context) {
38+
+ auto isolate = realm->isolate();
39+
auto binding_data = realm->GetBindingData<BindingData>();
40+
41+
auto cache_entry = binding_data->package_configs_.find(path.data());
42+
@@ -97,8 +99,36 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
43+
44+
PackageConfig package_config{};
45+
package_config.file_path = path;
46+
+
47+
+ Local<Function> modules_read_file_sync = realm->modules_read_file_sync();
48+
+
49+
+ int read_err;
50+
// No need to exclude BOM since simdjson will skip it.
51+
- if (ReadFileSync(&package_config.raw_json, path.data()) < 0) {
52+
+ if (modules_read_file_sync.IsEmpty()) {
53+
+ read_err = ReadFileSync(&package_config.raw_json, path.data());
54+
+ } else {
55+
+ Local<Value> args[] = {
56+
+ v8::String::NewFromUtf8(isolate, path.data()).ToLocalChecked(),
57+
+ };
58+
+ Local<Value> result = modules_read_file_sync->Call(
59+
+ realm->context(),
60+
+ Undefined(isolate),
61+
+ arraysize(args),
62+
+ args).ToLocalChecked();
63+
+
64+
+ if (result->IsUndefined()) {
65+
+ // Fallback
66+
+ read_err = ReadFileSync(&package_config.raw_json, path.data());
67+
+ } else if (result->IsFalse()) {
68+
+ // Not found
69+
+ read_err = -1;
70+
+ } else {
71+
+ BufferValue data(isolate, result);
72+
+ package_config.raw_json = data.ToString();
73+
+ read_err = 0;
74+
+ }
75+
+ }
76+
+ if (read_err < 0) {
77+
return nullptr;
78+
}
79+
// In some systems, std::string is annotated to generate an
80+
@@ -248,6 +278,12 @@ const BindingData::PackageConfig* BindingData::GetPackageJSON(
81+
return &cached.first->second;
82+
}
83+
84+
+void BindingData::OverrideReadFileSync(const FunctionCallbackInfo<Value>& args) {
85+
+ Realm* realm = Realm::GetCurrent(args);
86+
+ CHECK(args[0]->IsFunction());
87+
+ realm->set_modules_read_file_sync(args[0].As<Function>());
88+
+}
89+
+
90+
void BindingData::ReadPackageJSON(const FunctionCallbackInfo<Value>& args) {
91+
CHECK_GE(args.Length(), 1); // path, [is_esm, base, specifier]
92+
CHECK(args[0]->IsString()); // path
93+
@@ -556,6 +592,8 @@ void GetCompileCacheDir(const FunctionCallbackInfo<Value>& args) {
94+
void BindingData::CreatePerIsolateProperties(IsolateData* isolate_data,
95+
Local<ObjectTemplate> target) {
96+
Isolate* isolate = isolate_data->isolate();
97+
+ SetMethod(isolate, target, "overrideReadFileSync", OverrideReadFileSync);
98+
+
99+
SetMethod(isolate, target, "readPackageJSON", ReadPackageJSON);
100+
SetMethod(isolate,
101+
target,
102+
@@ -595,6 +633,8 @@ void BindingData::CreatePerContextProperties(Local<Object> target,
103+
104+
void BindingData::RegisterExternalReferences(
105+
ExternalReferenceRegistry* registry) {
106+
+ registry->Register(OverrideReadFileSync);
107+
+
108+
registry->Register(ReadPackageJSON);
109+
registry->Register(GetNearestParentPackageJSONType);
110+
registry->Register(GetNearestParentPackageJSON);
111+
diff --git a/src/node_modules.h b/src/node_modules.h
112+
index 17909b2270454b3275c7bf2e50d4b9b35673ecc8..3d5b0e3ac65524adfe221bfd6f85360dee1f0bee 100644
113+
--- a/src/node_modules.h
114+
+++ b/src/node_modules.h
115+
@@ -54,6 +54,8 @@ class BindingData : public SnapshotableObject {
116+
SET_SELF_SIZE(BindingData)
117+
SET_MEMORY_INFO_NAME(BindingData)
118+
119+
+ static void OverrideReadFileSync(
120+
+ const v8::FunctionCallbackInfo<v8::Value>& args);
121+
static void ReadPackageJSON(const v8::FunctionCallbackInfo<v8::Value>& args);
122+
static void GetNearestParentPackageJSON(
123+
const v8::FunctionCallbackInfo<v8::Value>& args);

0 commit comments

Comments
 (0)