current commit
260915.6 · ffi: load libraries from a mounted VFS
// Flags: --expose-internals
'use strict';

// Loading an addon from bytes materializes them into a private image so the
// dynamic loader has a real path to open. That image is transient and must not
// outlive the process that loaded it. How it is held differs by platform, so
// this checks both halves of the contract:
//
//   Linux:       an anonymous memfd loaded through /proc/self/fd - nothing ever
//                reaches the filesystem, and AfterOpen() closes the descriptor
//                once the load owns its mapping, so repeated loads must not
//                accumulate open descriptors.
//   other POSIX: a mkdtemp() directory unlinked and rmdir()ed right after the
//                load, so nothing is left even while the process runs.
//   Windows:     the loader maps the file by path for the DLL's lifetime, so
//                the image has to stay put; it is retained with the module it
//                loaded as, and both are released at process exit.

const common = require('../common');
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');
const tmpdir = require('../common/tmpdir');

const addonPath = path.join(
  __dirname, '..', 'addons', 'hello-world', 'build', 'Release', 'binding.node');
if (!fs.existsSync(addonPath)) common.skip('the hello-world addon is not built');

tmpdir.refresh();

// Where a temp-file image would land: GetTempPathW() reads TMP/TEMP and
// TempDir() reads TMPDIR, so pointing all three at a directory this test owns
// keeps any image the child writes somewhere it can inspect afterwards. Linux
// normally uses a memfd and never writes here at all.
const imageDir = tmpdir.resolve('addon-images');
fs.mkdirSync(imageDir, { recursive: true });

const child = `
  const fs = require('fs');
  const { internalBinding } = require('internal/test/binding');
  const { dlopenBinary } = internalBinding('process_methods');
  const bytes = fs.readFileSync(${JSON.stringify(addonPath)});
  // A path that does not exist on disk, as a VFS-resident addon would be, so
  // the load can only come from the bytes and their materialized image.
  const virtualPath = ${JSON.stringify(path.join(addonPath, '..', 'nowhere', 'binding.node'))};

  // On Linux the image is a descriptor rather than a file, so count them: each
  // load must hand its fd to the mapping and close it, leaving no growth.
  const fdDir = '/proc/self/fd';
  const countFds = () => {
    try { return fs.readdirSync(fdDir).length; } catch { return -1; }
  };
  const before = countFds();

  // Load repeatedly: each load materializes its own image, so a leak of an
  // image, a descriptor or a retained handle shows up as growth.
  for (let i = 0; i < 5; i++) {
    const m = { exports: {} };
    // flags undefined: keep the default dlopen(2) mode - 0 is not valid
    // everywhere (glibc rejects it with EINVAL).
    dlopenBinary(m, virtualPath, undefined, bytes);
    if (m.exports.hello() !== 'world') throw new Error('addon did not load');
  }

  const after = countFds();
  if (before !== -1 && after > before) {
    throw new Error(\`descriptor leak: \${before} -> \${after} after 5 loads\`);
  }
  process.exit(0);
`;

const res = spawnSync(process.execPath, ['--expose-internals', '-e', child], {
  env: { ...process.env, TMPDIR: imageDir, TMP: imageDir, TEMP: imageDir },
  encoding: 'utf8',
});

// The load itself must succeed. On Windows a retained writable handle makes the
// loader fail with ERROR_SHARING_VIOLATION ("The process cannot access the file
// because it is being used by another process").
assert.strictEqual(res.status, 0, `child failed:\n${res.stderr}`);

// Nothing an image left behind may outlive the process that created it. Match
// the shapes the two on-disk paths produce rather than requiring the directory
// to be empty, so an unrelated temp file cannot fail this.
const leftovers = fs.readdirSync(imageDir).filter(
  (name) => /^nod.*\.tmp$/i.test(name) || name.startsWith('node-addon-'));
assert.deepStrictEqual(
  leftovers, [],
  `materialized addon image outlived the process that loaded it: ${leftovers}`);