Skip to content

vfs: load native addons from a mounted file system - #65680

Open
pipobscure wants to merge 4 commits into
nodejs:mainfrom
pipobscure:vfsnative
Open

vfs: load native addons from a mounted file system#65680
pipobscure wants to merge 4 commits into
nodejs:mainfrom
pipobscure:vfsnative

Conversation

@pipobscure

Copy link
Copy Markdown
Contributor

A native addon inside a virtual file system could not be require()d: dlopen() and LoadLibrary() open a shared object by path, and a VFS path has no inode for them to open.

Read the addon's bytes from the VFS and load them from a private, self-cleaning image instead, using the smallest on-disk footprint each platform allows: on Linux an anonymous memfd loaded through /proc/self/fd, so the bytes never reach the file system; on other POSIX platforms a file in a 0700 mkdtemp() directory, unlinked right after loading, with the mapping keeping it alive; on Windows a temp file opened FILE_FLAG_DELETE_ON_CLOSE whose handle is held until exit.

This is internal. process.dlopen() keeps its documented (module, filename[, flags]) signature and ignores anything further; the bytes are passed to a dlopenBinary() reachable only through the process_methods binding, which the VFS hook calls for VFS paths. Addons on the real file system are untouched and load directly.

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. vfs Issues and PRs related to the virtual filesystem subsystem. labels Aug 31, 2026

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Comment thread lib/internal/vfs/setup.js
Comment on lines +920 to +921
originalDlopen = process.dlopen;
process.dlopen = function(module, filename, flags) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just reflecting... we need a better way to do this stuff. This kind of monkeypatching makes me sad.

@pipobscure pipobscure Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, but this hits only code loading when on VFS. Making it generic would cause it to be run on every module load, which I was just very hesitant to do. This way it's only a thing if VFS is in use.

Maybe the right answer is to make a note that this needs cleaning up (I volunteer) once VFS is out of experimental. Until then I think this is the right shape.

@mcollina mcollina added the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2026
assert.ok(!fs.existsSync(fakePath));

const m = { exports: {} };
dlopenBinary(m, fakePath, flags, bytes);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is inherently rather dangerous. I'd like to see a test that verifies this is protected by the permission system. I would expect that both the fs and addon permissions would need to be granted in order for this to run.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am personally not really a fan of permissions (but that's not an argument 😄)

That being said, I'm currently making sure that allow-fs is also checked (allow-addons already was) and adding tests to keep that verified. Push with that coming shortly.

@github-actions github-actions Bot removed the request-ci Add this label to start a Jenkins CI on a PR. label Aug 31, 2026
@nodejs-github-bot

Copy link
Copy Markdown
Collaborator

@codecov

codecov Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.00000% with 63 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.05%. Comparing base (a844473) to head (207aae4).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
src/node_binding.cc 45.13% 48 Missing and 14 partials ⚠️
lib/internal/vfs/setup.js 95.45% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #65680      +/-   ##
==========================================
- Coverage   90.07%   90.05%   -0.03%     
==========================================
  Files         754      754              
  Lines      256378   256514     +136     
  Branches    48498    48521      +23     
==========================================
+ Hits       230936   231002      +66     
- Misses      16572    16614      +42     
- Partials     8870     8898      +28     
Files with missing lines Coverage Δ
src/node_options.cc 79.66% <100.00%> (+0.03%) ⬆️
src/node_options.h 95.39% <100.00%> (+0.48%) ⬆️
src/node_process_methods.cc 88.60% <100.00%> (+0.04%) ⬆️
lib/internal/vfs/setup.js 87.52% <95.45%> (+0.18%) ⬆️
src/node_binding.cc 70.49% <45.13%> (-11.61%) ⬇️

... and 26 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pipobscure

Copy link
Copy Markdown
Contributor Author

Note: codecov level is only due to there being an #ifdef for Windows so a codecov run will always be missing half the file if run on a single architecture.

@pipobscure

Copy link
Copy Markdown
Contributor Author

971cc1c fs: fix recursive watch error handling

landed today and touches that failing test. Nothing I did should go near fs.watch so this is at least suspicious.

Anyone have any ideas?

@jasnell

jasnell commented Aug 31, 2026

Copy link
Copy Markdown
Member

Just need to rebase on main and run CI again.

A native addon inside a virtual file system could not be require()d:
dlopen() and LoadLibrary() open a shared object by path, and a VFS path
has no inode for them to open.

Read the addon's bytes from the VFS and load them from a private,
self-cleaning image instead, using the smallest on-disk footprint each
platform allows: on Linux an anonymous memfd loaded through
/proc/self/fd, so the bytes never reach the file system; on other POSIX
platforms a file in a 0700 mkdtemp() directory, unlinked right after
loading, with the mapping keeping it alive; on Windows a temp file
opened FILE_FLAG_DELETE_ON_CLOSE whose handle is held until exit.

This is internal. process.dlopen() keeps its documented
(module, filename[, flags]) signature and ignores anything further; the
bytes are passed to a dlopenBinary() reachable only through the
process_methods binding, which the VFS hook calls for VFS paths. Addons
on the real file system are untouched and load directly.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
Reflow the temporary-image open() call to what
`CLANG_FORMAT_START=$(git merge-base HEAD main) make format-cpp`
produces, so the C++ formatting check passes.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
dlopenBinary() materializes the addon's bytes into an image in the
temporary directory before handing it to the dynamic loader. Under the
permission model that write went unchecked: --allow-addons alone was
enough to have Node.js write an executable image out.

Require write access to the temporary directory as well, checked before
the bytes are materialized. The check does not depend on whether the
image reaches the file system on a given platform - Linux uses an
anonymous memfd - so that what a program must be granted stays the same
everywhere.

process.dlopen() is unaffected: it never takes bytes, so it never
reaches the check.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
Mounting a VFS under the permission model is refused with a message
telling the user to pass --allow-fs-vfs, but that option was never
registered, so `node --allow-fs-vfs` failed with "bad option" and a
virtual file system could not be mounted under --permission at all.

Register it alongside the other permission flags and document it.

Signed-off-by: Philipp Dunkel <pip@pipobscure.com>
@pipobscure

Copy link
Copy Markdown
Contributor Author

Rebased on main because CI was failing with an unrelated test.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c++ Issues and PRs that require attention from people who are familiar with C++. needs-ci PRs that need a full CI run. vfs Issues and PRs related to the virtual filesystem subsystem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants