// 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}`);