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: https://github.com/nodejs/node/pull/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>
This commit is contained in:
Philipp Dunkel authored and GitHub committed 2026-10-16 12:44:35 +00:00
1 parent 2d908f48d9
commit 9f2fb935fd
15 files changed
+198 -22

No files matched your search

+4
View File
@@ -3955,6 +3955,10 @@ comes from anywhere else, such as the real file system, needs nothing added.
the command line's decision, and the environment must not be able to redirect
it.
A [single executable application][] built with `"vfsArchive"` mounts that
archive at the mount point reserved for `--vfs-load`, so `--vfs-load` cannot be
used in it.
```console
$ node --vfs-load=app.zip
```
+22
View File
@@ -296,6 +296,27 @@ executable only embeds the archive; read the files through the file system
APIs instead. `"vfsArchive"` requires `"useVfs": true` and cannot be
combined with `"assets"`.
The archive is mounted at the mount point reserved for [`--vfs-load`][], which
is the same in every thread, and worker threads mount it as well. A path into
the archive, such as the main script's `__filename`, can therefore be loaded
in a worker:
```cjs
const { Worker, isMainThread } = require('node:worker_threads');
if (isMainThread) {
new Worker(__filename);
} else {
// The worker runs the same script, from the same archive.
console.log(require('./lib/math.js').add(2, 3));
}
```
Since that mount point is taken by the archive, `--vfs-load` cannot be used
with `"vfsArchive"`: `--build-sea` rejects it in `"execArgv"`, and the
executable rejects it at startup when it comes from `--node-options` or from
the `execArgv` of a worker.
#### Snapshot and code caching limitations
`"useVfs": true` cannot be used together with `"useSnapshot": true` or
@@ -786,6 +807,7 @@ to help us document them.
[Using native addons in the injected main script]: #using-native-addons-in-the-injected-main-script
[VFS documentation]: vfs.md
[Windows SDK]: https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/
[`--vfs-load`]: cli.md#--vfs-loadsource
[`node:zlib`]: zlib.md
[`process.execPath`]: process.md#processexecpath
[`require()`]: modules.md#requireid
+1 -1
View File
@@ -225,7 +225,7 @@ const fs = require('node:fs');
const myVfs = vfs.create();
myVfs.writeFileSync('/data.txt', 'Hello');
const mountPoint = myVfs.mount();
// e.g. '/dev/null/vfs/0'
// e.g. '/dev/null/vfs/1'
fs.readFileSync(`${mountPoint}/data.txt`, 'utf8'); // 'Hello'
```
+3
View File
@@ -2013,6 +2013,9 @@ comes from anywhere else, such as the real file system, needs nothing added.
\fB--vfs-load\fR is not permitted in \fBNODE_OPTIONS\fR: which entry point runs is
the command line's decision, and the environment must not be able to redirect
it.
A single executable application built with \fB"vfsArchive"\fR mounts that
archive at the mount point reserved for \fB--vfs-load\fR, so \fB--vfs-load\fR cannot be
used in it.
.Bd -literal
$ node --vfs-load=app.zip
.Ed
+3 -5
View File
@@ -16,7 +16,6 @@ const {
isExperimentalSeaWarningNeeded,
isSea,
isVfsEnabled,
mainCodePath: seaMainCodePath,
} = internalBinding('sea');
const { emitExperimentalWarning } = require('internal/util');
const { emitWarningSync } = require('internal/process/warning');
@@ -134,13 +133,12 @@ function embedderRunESM(content, filename) {
* @returns {string|null} The VFS path of the main script
*/
function setUpSeaVfs(content) {
const mainName = path.basename(seaMainCodePath || process.execPath);
const { initSeaVfs } = require('internal/vfs/sea');
const seaVfs = initSeaVfs({ extraFiles: { [mainName]: content } });
const { initSeaVfs, getSeaMainName } = require('internal/vfs/sea');
const seaVfs = initSeaVfs(content);
if (seaVfs === null) {
return null;
}
return path.join(seaVfs.mountPoint, mainName);
return path.join(seaVfs.mountPoint, getSeaMainName());
}
/* c8 ignore stop */
+5
View File
@@ -152,6 +152,11 @@ port.on('message', (message) => {
if (getOptionValue('--import').length === 0) {
finishVfsMounts();
}
// Unlike the --vfs-load source, a SEA archive needs no provider that a
// preload could register, so its mount is never deferred.
if (getOptionValue('[sea_vfs_archive]')) {
require('internal/vfs/sea').initSeaVfsInWorker();
}
}
if (!hasStdin)
+36 -9
View File
@@ -9,8 +9,9 @@ const {
isVfsEnabled,
isVfsArchiveEnabled,
getAsset,
getMainCode,
mainCodePath,
} = internalBinding('sea');
const { kEmptyObject } = require('internal/util');
const {
codes: {
ERR_INVALID_STATE,
@@ -29,14 +30,12 @@ let initialized = false;
* SEA main script is placed at the mount point root so bundled code can
* reach the assets through `__dirname`-relative paths and relative
* `require()` calls.
* @param {object} [options] Configuration options
* @param {Record<string, string|Buffer>} [options.extraFiles] Additional
* files to serve alongside the assets (used for the SEA main script)
* @param {string|Buffer} mainCode The source of the SEA main script
* @returns {VirtualFileSystem|null} The mounted VFS, or null if not running
* as a SEA or VFS is not enabled in the SEA configuration
* @throws {ERR_INVALID_STATE} If already initialized
*/
function initSeaVfs(options = kEmptyObject) {
function initSeaVfs(mainCode) {
if (initialized) {
throw new ERR_INVALID_STATE('SEA VFS is already initialized');
}
@@ -46,25 +45,51 @@ function initSeaVfs(options = kEmptyObject) {
return null;
}
const { VirtualFileSystem } = require('internal/vfs/file_system');
const { VirtualFileSystem, kLoadLayer } = require('internal/vfs/file_system');
const extraFiles = { __proto__: null, [getSeaMainName()]: mainCode };
const isArchive = isVfsArchiveEnabled();
let provider;
if (isVfsArchiveEnabled()) {
provider = createZipProvider(options.extraFiles);
if (isArchive) {
provider = createZipProvider(extraFiles);
} else {
const { SEAProvider } = require('internal/vfs/providers/sea');
provider = new SEAProvider({ extraFiles: options.extraFiles });
provider = new SEAProvider({ extraFiles });
}
// The SEA warning already covers the feature; don't emit the
// VirtualFileSystem experimental warning for the implicit SEA mount.
// An archive is mounted where --vfs-load mounts its source, which is the
// same mount point in every thread, so a worker can reach the code in it.
const vfs = new VirtualFileSystem(provider, {
__proto__: null,
emitExperimentalWarning: false,
[kLoadLayer]: isArchive,
});
vfs.mount();
return vfs;
}
/**
* Mounts the SEA virtual file system in a worker, when it is backed by a
* `"vfsArchive"`, so that a path into the mount in the main thread, such as
* the main script's `__filename`, is also valid in the worker.
*/
function initSeaVfsInWorker() {
if (isVfsArchiveEnabled()) {
initSeaVfs(getMainCode());
}
}
/**
* The name under which the SEA main script is placed at the mount point root.
* @returns {string}
*/
function getSeaMainName() {
const { basename } = require('path');
return basename(mainCodePath || process.execPath);
}
// The reserved asset key under which --build-sea stores the ZIP archive
// named by "vfsArchive". Must match kVfsArchiveAssetName in src/node_sea.cc.
const kVfsArchiveAssetName = 'node:sea:vfs.zip';
@@ -105,5 +130,7 @@ function createZipProvider(extraFiles) {
/* c8 ignore stop */
module.exports = {
getSeaMainName,
initSeaVfs,
initSeaVfsInWorker,
};
+12
View File
@@ -316,6 +316,17 @@ void EnvironmentOptions::CheckOptions(std::vector<std::string>* errors,
errors->push_back("either --check or --eval can be used, not both");
}
#ifndef DISABLE_SINGLE_EXECUTABLE_APPLICATION
sea_vfs_archive = sea::IsSingleExecutable() &&
sea::FindSingleExecutableResource().use_vfs_archive();
// The archive takes the mount point reserved for the --vfs-load source.
if (!vfs_load_source.empty() && sea_vfs_archive) {
errors->push_back(
"--vfs-load cannot be used in a single executable application "
"built with \"vfsArchive\"");
}
#endif // DISABLE_SINGLE_EXECUTABLE_APPLICATION
for (const std::string& pattern : permission::ParseEnvAllowList(allow_env)) {
if (!permission::IsValidEnvAllowPattern(pattern)) {
errors->push_back("--allow-env must be '*', a variable name, or a "
@@ -816,6 +827,7 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
kDisallowedInEnvvar);
AddOption("[vfs_load_set]", "", BOOL_FIELD(vfs_load));
Implies("--vfs-load", "[vfs_load_set]");
AddOption("[sea_vfs_archive]", "", BOOL_FIELD(sea_vfs_archive));
AddOption("--experimental-quic",
#ifndef OPENSSL_NO_QUIC
"experimental QUIC support",
+1
View File
@@ -217,6 +217,7 @@ class EnvironmentOptions : public Options {
DEFINE_BOOL_FIELD(experimental_stream_iter) = EXPERIMENTALS_DEFAULT_VALUE;
DEFINE_BOOL_FIELD(experimental_vfs) = true;
DEFINE_BOOL_FIELD(vfs_load) = false;
DEFINE_BOOL_FIELD(sea_vfs_archive) = false;
DEFINE_BOOL_FIELD(webstorage) = HAVE_SQLITE;
DEFINE_BOOL_FIELD(experimental_dtls) = EXPERIMENTALS_DEFAULT_VALUE;
DEFINE_BOOL_FIELD(experimental_quic) = EXPERIMENTALS_DEFAULT_VALUE;
+31 -6
View File
@@ -250,6 +250,10 @@ bool SeaResource::use_code_cache() const {
return static_cast<bool>(flags & SeaFlags::kUseCodeCache);
}
bool SeaResource::use_vfs_archive() const {
return static_cast<bool>(flags & SeaFlags::kVfsArchive);
}
const SeaResource& FindSingleExecutableResource() {
static const SeaResource sea_resource = []() -> SeaResource {
std::string_view blob = FindSingleExecutableBlob();
@@ -277,12 +281,8 @@ void IsVfsEnabled(const FunctionCallbackInfo<Value>& args) {
}
void IsVfsArchiveEnabled(const FunctionCallbackInfo<Value>& args) {
bool enabled = false;
if (IsSingleExecutable()) {
const SeaResource& sea_resource = FindSingleExecutableResource();
enabled = static_cast<bool>(sea_resource.flags & SeaFlags::kVfsArchive);
}
args.GetReturnValue().Set(enabled);
args.GetReturnValue().Set(IsSingleExecutable() &&
FindSingleExecutableResource().use_vfs_archive());
}
void IsExperimentalSeaWarningNeeded(const FunctionCallbackInfo<Value>& args) {
@@ -636,6 +636,15 @@ std::optional<SeaConfig> ParseSingleExecutableConfig(
config_path);
return std::nullopt;
}
// The archive takes the mount point reserved for the --vfs-load source.
for (const std::string& arg : result.exec_argv) {
if (arg == "--vfs-load" || arg.starts_with("--vfs-load=")) {
FPrintF(stderr,
"\"vfsArchive\" cannot be used together with --vfs-load in "
"\"execArgv\"\n");
return std::nullopt;
}
}
// The archive is embedded as a single reserved asset.
result.flags |= SeaFlags::kIncludeAssets;
}
@@ -966,6 +975,20 @@ void GetAssetKeys(const FunctionCallbackInfo<Value>& args) {
args.GetReturnValue().Set(result);
}
// A worker has no main script handed to it, but places the SEA main script in
// its virtual file system just as the main thread does.
void GetMainCode(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 0);
const SeaResource& sea_resource = FindSingleExecutableResource();
CHECK(!sea_resource.use_snapshot());
Local<Value> code;
if (ToV8Value(args.GetIsolate()->GetCurrentContext(),
sea_resource.main_code_or_snapshot)
.ToLocal(&code)) {
args.GetReturnValue().Set(code);
}
}
MaybeLocal<Value> LoadSingleExecutableApplication(
const StartExecutionCallbackInfoWithModule& info) {
// Here we are currently relying on the fact that in NodeMainInstance::Run(),
@@ -1048,6 +1071,7 @@ void Initialize(Local<Object> target,
IsExperimentalSeaWarningNeeded);
SetMethod(context, target, "getAsset", GetAsset);
SetMethod(context, target, "getAssetKeys", GetAssetKeys);
SetMethod(context, target, "getMainCode", GetMainCode);
}
void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
@@ -1057,6 +1081,7 @@ void RegisterExternalReferences(ExternalReferenceRegistry* registry) {
registry->Register(IsExperimentalSeaWarningNeeded);
registry->Register(GetAsset);
registry->Register(GetAssetKeys);
registry->Register(GetMainCode);
}
} // namespace sea
+1
View File
@@ -64,6 +64,7 @@ struct SeaResource {
bool use_snapshot() const;
bool use_code_cache() const;
bool use_vfs_archive() const;
static constexpr size_t kHeaderSize = sizeof(kMagic) + sizeof(SeaFlags) +
sizeof(SeaExecArgvExtension) +
@@ -0,0 +1,7 @@
{
"main": "sea.js",
"output": "sea-vfs-load-rejected",
"useVfs": true,
"vfsArchive": "assets.zip",
"execArgvExtension": "cli"
}
+30 -1
View File
@@ -2,6 +2,19 @@
const fs = require('fs');
const path = require('path');
const assert = require('assert');
const os = require('os');
const { Worker, isMainThread, parentPort } = require('worker_threads');
if (!isMainThread) {
// A worker mounts the archive too, at the same mount point, so the main
// script and the code next to it can be loaded from there.
parentPort.postMessage({
dirname: __dirname,
greeting: fs.readFileSync(path.join(__dirname, 'data', 'greeting.txt'), 'utf8'),
sum: require('./modules/math.js').add(2, 3),
});
return;
}
// With "useVfsZip", the bundled assets are stored as a single ZIP archive
// and mounted through the ZipProvider. Everything is reachable through
@@ -13,6 +26,9 @@ assert.strictEqual(path.basename(__filename), 'sea.js');
assert.strictEqual(require.main, module);
console.log('main script runs from', __filename);
// The archive is mounted at the mount point reserved for --vfs-load.
assert.strictEqual(__dirname, path.join(os.devNull, 'vfs', '0'));
// The main script itself is readable through fs.
assert.strictEqual(fs.existsSync(__filename), true);
@@ -58,4 +74,17 @@ assert.notStrictEqual(a, b);
a[0] = 0;
assert.strictEqual(b.toString('utf8'), 'Hello from SEA VFS!');
console.log('All SEA VFS zip tests passed!');
// The mount point is taken by the archive, so a worker cannot --vfs-load.
assert.throws(() => new Worker(__filename, {
execArgv: ['--experimental-vfs', `--vfs-load=${__dirname}`],
}), { code: 'ERR_WORKER_INVALID_EXEC_ARGV' });
const worker = new Worker(__filename);
worker.once('message', (message) => {
assert.deepStrictEqual(message, {
dirname: __dirname,
greeting: 'Hello from SEA VFS!',
sum: 5,
});
console.log('All SEA VFS zip tests passed!');
});
@@ -141,3 +141,24 @@ skipIfBuildSEAIsNotSupported();
stderr: /"vfsArchive" field of .*vfs-archive-not-string\.json is not a string/,
});
}
// Test: "vfsArchive" combined with --vfs-load in "execArgv"
for (const arg of ['--vfs-load=assets.zip', '--vfs-load']) {
tmpdir.refresh();
const config = tmpdir.resolve('vfs-archive-vfs-load.json');
writeFileSync(config, JSON.stringify({
main: 'bundle.js',
output: 'sea',
useVfs: true,
vfsArchive: 'assets.zip',
execArgv: ['--experimental-vfs', arg],
}), 'utf8');
spawnSyncAndAssert(
process.execPath,
['--build-sea', config], {
cwd: tmpdir.path,
}, {
status: 1,
stderr: /"vfsArchive" cannot be used together with --vfs-load in "execArgv"/,
});
}
@@ -57,4 +57,25 @@ main().then(common.mustCall(() => {
},
},
);
// The archive is mounted where --vfs-load would mount its source, so the
// executable rejects --vfs-load at startup. --build-sea already refuses it
// in "execArgv", so pass it through --node-options instead.
const rejectedFile = buildSEA(fixtures.path('sea', 'vfs-zip'), {
configPath: 'sea-config-vfs-load-rejected.json',
});
spawnSyncAndAssert(
rejectedFile,
['--node-options=--experimental-vfs --vfs-load=assets.zip'],
{
env: {
...process.env,
NODE_DEBUG_NATIVE: undefined,
},
},
{
status: 9,
stderr: /--vfs-load cannot be used in a single executable application built with "vfsArchive"/,
},
);
}));