Skip to content

Commit 9f2fb93

Browse files
authored
sea: mount vfsArchive at layer 0 and in workers
A single executable application built with "vfsArchive" mounted its archive at the next free layer, like any file system a program mounts itself, and only in the main thread. A worker had no mount at all, so a path into the archive, such as the main script's __filename, could not be loaded there. Mount the archive at the layer reserved for --vfs-load instead, so it is at the same mount point in every thread, and mount it in each worker as well, with the main script placed at its root just as in the main thread. The worker reads the main script through a new getMainCode() binding, since no main script is handed to it. Unlike the --vfs-load source, the archive needs no provider that a preload could register, so its mount in a worker is not deferred past --import. Since the archive takes that mount point, reject --vfs-load alongside it: --build-sea refuses it in "execArgv", and the executable refuses it at startup when it comes from --node-options or from the execArgv of a worker. Also fix the example in vfs.md, which showed a file system a program mounts itself at layer 0, the layer reserved for these mounts. Signed-off-by: Philipp Dunkel <pip@pipobscure.com> PR-URL: #66709 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
1 parent 2d908f4 commit 9f2fb93

15 files changed

Lines changed: 198 additions & 22 deletions

‎doc/api/cli.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3955,6 +3955,10 @@ comes from anywhere else, such as the real file system, needs nothing added.
39553955
the command line's decision, and the environment must not be able to redirect
39563956
it.
39573957

3958+
A [single executable application][] built with `"vfsArchive"` mounts that
3959+
archive at the mount point reserved for `--vfs-load`, so `--vfs-load` cannot be
3960+
used in it.
3961+
39583962
```console
39593963
$ node --vfs-load=app.zip
39603964
```

‎doc/api/single-executable-applications.md‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,27 @@ executable only embeds the archive; read the files through the file system
296296
APIs instead. `"vfsArchive"` requires `"useVfs": true` and cannot be
297297
combined with `"assets"`.
298298
299+
The archive is mounted at the mount point reserved for [`--vfs-load`][], which
300+
is the same in every thread, and worker threads mount it as well. A path into
301+
the archive, such as the main script's `__filename`, can therefore be loaded
302+
in a worker:
303+
304+
```cjs
305+
const { Worker, isMainThread } = require('node:worker_threads');
306+
307+
if (isMainThread) {
308+
new Worker(__filename);
309+
} else {
310+
// The worker runs the same script, from the same archive.
311+
console.log(require('./lib/math.js').add(2, 3));
312+
}
313+
```
314+
315+
Since that mount point is taken by the archive, `--vfs-load` cannot be used
316+
with `"vfsArchive"`: `--build-sea` rejects it in `"execArgv"`, and the
317+
executable rejects it at startup when it comes from `--node-options` or from
318+
the `execArgv` of a worker.
319+
299320
#### Snapshot and code caching limitations
300321
301322
`"useVfs": true` cannot be used together with `"useSnapshot": true` or
@@ -786,6 +807,7 @@ to help us document them.
786807
[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script
787808
[VFS documentation]: vfs.md
788809
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
810+
[`--vfs-load`]: cli.md#--vfs-loadsource
789811
[`node:zlib`]: zlib.md
790812
[`process.execPath`]: process.md#processexecpath
791813
[`require()`]: modules.md#requireid

‎doc/api/vfs.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ const fs = require('node:fs');
225225
const myVfs = vfs.create();
226226
myVfs.writeFileSync('/data.txt', 'Hello');
227227
const mountPoint = myVfs.mount();
228-
// e.g. '/dev/null/vfs/0'
228+
// e.g. '/dev/null/vfs/1'
229229

230230
fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
231231
```

‎doc/node.1‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2013,6 +2013,9 @@ comes from anywhere else, such as the real file system, needs nothing added.
20132013
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
20142014
the command line's decision, and the environment must not be able to redirect
20152015
it.
2016+
A single executable application built with \fB"vfsArchive"\fR mounts that
2017+
archive at the mount point reserved for \fB--vfs-load\fR, so \fB--vfs-load\fR cannot be
2018+
used in it.
20162019
.Bd -literal
20172020
$ node --vfs-load=app.zip
20182021
.Ed

‎lib/internal/main/embedding.js‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ const {
1616
isExperimentalSeaWarningNeeded,
1717
isSea,
1818
isVfsEnabled,
19-
mainCodePath: seaMainCodePath,
2019
} = internalBinding('sea');
2120
const { emitExperimentalWarning } = require('internal/util');
2221
const { emitWarningSync } = require('internal/process/warning');
@@ -134,13 +133,12 @@ function embedderRunESM(content, filename) {
134133
* @returns {string|null} The VFS path of the main script
135134
*/
136135
function setUpSeaVfs(content) {
137-
const mainName = path.basename(seaMainCodePath || process.execPath);
138-
const { initSeaVfs } = require('internal/vfs/sea');
139-
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
136+
const { initSeaVfs, getSeaMainName } = require('internal/vfs/sea');
137+
const seaVfs = initSeaVfs(content);
140138
if (seaVfs === null) {
141139
return null;
142140
}
143-
return path.join(seaVfs.mountPoint, mainName);
141+
return path.join(seaVfs.mountPoint, getSeaMainName());
144142
}
145143
/* c8 ignore stop */
146144

‎lib/internal/main/worker_thread.js‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,11 @@ port.on('message', (message) => {
152152
if (getOptionValue('--import').length === 0) {
153153
finishVfsMounts();
154154
}
155+
// Unlike the --vfs-load source, a SEA archive needs no provider that a
156+
// preload could register, so its mount is never deferred.
157+
if (getOptionValue('[sea_vfs_archive]')) {
158+
require('internal/vfs/sea').initSeaVfsInWorker();
159+
}
155160
}
156161

157162
if (!hasStdin)

‎lib/internal/vfs/sea.js‎

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ const {
99
isVfsEnabled,
1010
isVfsArchiveEnabled,
1111
getAsset,
12+
getMainCode,
13+
mainCodePath,
1214
} = internalBinding('sea');
13-
const { kEmptyObject } = require('internal/util');
1415
const {
1516
codes: {
1617
ERR_INVALID_STATE,
@@ -29,14 +30,12 @@ let initialized = false;
2930
* SEA main script is placed at the mount point root so bundled code can
3031
* reach the assets through `__dirname`-relative paths and relative
3132
* `require()` calls.
32-
* @param {object} [options] Configuration options
33-
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
34-
* files to serve alongside the assets (used for the SEA main script)
33+
* @param {string|Buffer} mainCode The source of the SEA main script
3534
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
3635
* as a SEA or VFS is not enabled in the SEA configuration
3736
* @throws {ERR_INVALID_STATE} If already initialized
3837
*/
39-
function initSeaVfs(options = kEmptyObject) {
38+
function initSeaVfs(mainCode) {
4039
if (initialized) {
4140
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
4241
}
@@ -46,25 +45,51 @@ function initSeaVfs(options = kEmptyObject) {
4645
return null;
4746
}
4847

49-
const { VirtualFileSystem } = require('internal/vfs/file_system');
48+
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
5049

50+
const extraFiles = { __proto__: null, [getSeaMainName()]: mainCode };
51+
const isArchive = isVfsArchiveEnabled();
5152
let provider;
52-
if (isVfsArchiveEnabled()) {
53-
provider = createZipProvider(options.extraFiles);
53+
if (isArchive) {
54+
provider = createZipProvider(extraFiles);
5455
} else {
5556
const { SEAProvider } = require('internal/vfs/providers/sea');
56-
provider = new SEAProvider({ extraFiles: options.extraFiles });
57+
provider = new SEAProvider({ extraFiles });
5758
}
5859
// The SEA warning already covers the feature; don't emit the
5960
// VirtualFileSystem experimental warning for the implicit SEA mount.
61+
// An archive is mounted where --vfs-load mounts its source, which is the
62+
// same mount point in every thread, so a worker can reach the code in it.
6063
const vfs = new VirtualFileSystem(provider, {
64+
__proto__: null,
6165
emitExperimentalWarning: false,
66+
[kLoadLayer]: isArchive,
6267
});
6368
vfs.mount();
6469

6570
return vfs;
6671
}
6772

73+
/**
74+
* Mounts the SEA virtual file system in a worker, when it is backed by a
75+
* `"vfsArchive"`, so that a path into the mount in the main thread, such as
76+
* the main script's `__filename`, is also valid in the worker.
77+
*/
78+
function initSeaVfsInWorker() {
79+
if (isVfsArchiveEnabled()) {
80+
initSeaVfs(getMainCode());
81+
}
82+
}
83+
84+
/**
85+
* The name under which the SEA main script is placed at the mount point root.
86+
* @returns {string}
87+
*/
88+
function getSeaMainName() {
89+
const { basename } = require('path');
90+
return basename(mainCodePath || process.execPath);
91+
}
92+
6893
// The reserved asset key under which --build-sea stores the ZIP archive
6994
// named by "vfsArchive". Must match kVfsArchiveAssetName in src/node_sea.cc.
7095
const kVfsArchiveAssetName = 'node:sea:vfs.zip';
@@ -105,5 +130,7 @@ function createZipProvider(extraFiles) {
105130
/* c8 ignore stop */
106131

107132
module.exports = {
133+
getSeaMainName,
108134
initSeaVfs,
135+
initSeaVfsInWorker,
109136
};

‎src/node_options.cc‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,17 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
316316
errors->push_back("either --check or --eval can be used, not both");
317317
}
318318

319+
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
320+
sea_vfs_archive = sea::IsSingleExecutable() &&
321+
sea::FindSingleExecutableResource().use_vfs_archive();
322+
// The archive takes the mount point reserved for the --vfs-load source.
323+
if (!vfs_load_source.empty() && sea_vfs_archive) {
324+
errors->push_back(
325+
"--vfs-load cannot be used in a single executable application "
326+
"built with \"vfsArchive\"");
327+
}
328+
#endif // DISABLE_SINGLE_EXECUTABLE_APPLICATION
329+
319330
for (const std::string& pattern : permission::ParseEnvAllowList(allow_env)) {
320331
if (!permission::IsValidEnvAllowPattern(pattern)) {
321332
errors->push_back("--allow-env must be '*', a variable name, or a "
@@ -816,6 +827,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
816827
kDisallowedInEnvvar);
817828
AddOption("[vfs_load_set]", "", BOOL_FIELD(vfs_load));
818829
Implies("--vfs-load", "[vfs_load_set]");
830+
AddOption("[sea_vfs_archive]", "", BOOL_FIELD(sea_vfs_archive));
819831
AddOption("--experimental-quic",
820832
#ifndef OPENSSL_NO_QUIC
821833
"experimental QUIC support",

‎src/node_options.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,7 @@ class EnvironmentOptions : public Options {
217217
DEFINE_BOOL_FIELD(experimental_stream_iter) = EXPERIMENTALS_DEFAULT_VALUE;
218218
DEFINE_BOOL_FIELD(experimental_vfs) = true;
219219
DEFINE_BOOL_FIELD(vfs_load) = false;
220+
DEFINE_BOOL_FIELD(sea_vfs_archive) = false;
220221
DEFINE_BOOL_FIELD(webstorage) = HAVE_SQLITE;
221222
DEFINE_BOOL_FIELD(experimental_dtls) = EXPERIMENTALS_DEFAULT_VALUE;
222223
DEFINE_BOOL_FIELD(experimental_quic) = EXPERIMENTALS_DEFAULT_VALUE;

‎src/node_sea.cc‎

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,10 @@ bool SeaResource::use_code_cache() const {
250250
return static_cast<bool>(flags & SeaFlags::kUseCodeCache);
251251
}
252252

253+
bool SeaResource::use_vfs_archive() const {
254+
return static_cast<bool>(flags & SeaFlags::kVfsArchive);
255+
}
256+
253257
const SeaResource& FindSingleExecutableResource() {
254258
static const SeaResource sea_resource = []() -> SeaResource {
255259
std::string_view blob = FindSingleExecutableBlob();
@@ -277,12 +281,8 @@ void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
277281
}
278282

279283
void IsVfsArchiveEnabled(const FunctionCallbackInfo<Value>& args) {
280-
bool enabled = false;
281-
if (IsSingleExecutable()) {
282-
const SeaResource& sea_resource = FindSingleExecutableResource();
283-
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kVfsArchive);
284-
}
285-
args.GetReturnValue().Set(enabled);
284+
args.GetReturnValue().Set(IsSingleExecutable() &&
285+
FindSingleExecutableResource().use_vfs_archive());
286286
}
287287

288288
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
@@ -636,6 +636,15 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
636636
config_path);
637637
return std::nullopt;
638638
}
639+
// The archive takes the mount point reserved for the --vfs-load source.
640+
for (const std::string& arg : result.exec_argv) {
641+
if (arg == "--vfs-load" || arg.starts_with("--vfs-load=")) {
642+
FPrintF(stderr,
643+
"\"vfsArchive\" cannot be used together with --vfs-load in "
644+
"\"execArgv\"\n");
645+
return std::nullopt;
646+
}
647+
}
639648
// The archive is embedded as a single reserved asset.
640649
result.flags |= SeaFlags::kIncludeAssets;
641650
}
@@ -966,6 +975,20 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
966975
args.GetReturnValue().Set(result);
967976
}
968977

978+
// A worker has no main script handed to it, but places the SEA main script in
979+
// its virtual file system just as the main thread does.
980+
void GetMainCode(const FunctionCallbackInfo<Value>& args) {
981+
CHECK_EQ(args.Length(), 0);
982+
const SeaResource& sea_resource = FindSingleExecutableResource();
983+
CHECK(!sea_resource.use_snapshot());
984+
Local<Value> code;
985+
if (ToV8Value(args.GetIsolate()->GetCurrentContext(),
986+
sea_resource.main_code_or_snapshot)
987+
.ToLocal(&code)) {
988+
args.GetReturnValue().Set(code);
989+
}
990+
}
991+
969992
MaybeLocal<Value> LoadSingleExecutableApplication(
970993
const StartExecutionCallbackInfoWithModule& info) {
971994
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
@@ -1048,6 +1071,7 @@ void Initialize(Local<Object> target,
10481071
IsExperimentalSeaWarningNeeded);
10491072
SetMethod(context, target, "getAsset", GetAsset);
10501073
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
1074+
SetMethod(context, target, "getMainCode", GetMainCode);
10511075
}
10521076

10531077
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
@@ -1057,6 +1081,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
10571081
registry->Register(IsExperimentalSeaWarningNeeded);
10581082
registry->Register(GetAsset);
10591083
registry->Register(GetAssetKeys);
1084+
registry->Register(GetMainCode);
10601085
}
10611086

10621087
} // namespace sea

0 commit comments

Comments
 (0)