Skip to content

fix: use custom process uid/gid for Node permission checks#1251

Open
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-permission-checks-using-process
Open

fix: use custom process uid/gid for Node permission checks#1251
Copilot wants to merge 4 commits intomasterfrom
copilot/fix-permission-checks-using-process

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 23, 2026

Node.canRead/canWrite/canExecute default parameters captured the global process.getuid/getgid at module load time, ignoring the instance-level process option introduced in #1248. This meant accessSync(path, X_OK | R_OK | W_OK) and all file-creation permission checks silently fell back to the global process identity.

Changes

  • Superblock.ts — added private getUid()/getGid() helpers delegating to this.process; threaded them through all bare canRead/canWrite/canExecute calls in openLink, openFile, symlink, rename, mkdir, mkdirp, and rm
  • volume.ts_access() and _readdir() now extract uid/gid from this._core.process before calling node permission methods

Example (previously broken)

const { fs } = memfs({ '/exec.sh': '#!/bin/sh' }, {
  process: { getuid: () => 1000, getgid: () => 1000, cwd: () => '/', platform: 'linux', emitWarning: () => {}, env: {} },
});

fs.chmodSync('/exec.sh', 0o100); // user-execute only
fs.chownSync('/exec.sh', 1000, 1000);

// Previously threw EACCES; now resolves correctly
fs.accessSync('/exec.sh', fs.constants.X_OK);
Original prompt

This section details on the original issue you should resolve

<issue_title>Custom process object not used for Node permission checks (canRead/canWrite/canExecute)</issue_title>
<issue_description>## Summary

The process option added in #1248 is stored on the Superblock instance and used for
path resolution, but Node.canRead(), Node.canWrite(), and Node.canExecute() still use
the global process.getuid/process.getgid captured at module load time.

This means fs.accessSync() with X_OK, R_OK, or W_OK ignores the custom process
object for permission checks.

Reproduction

const { memfs } = require('memfs');

const { fs } = memfs({ '/exec.sh': '#!/bin/sh' }, {
  process: {
    getuid: () => 1000,
    getgid: () => 1000,
    cwd: () => '/',
    platform: 'linux',
    emitWarning: () => {},
    env: {},
  },
});

fs.chmodSync('/exec.sh', 0o100); // user-execute only
fs.chownSync('/exec.sh', 1000, 1000);

// Expected: no error (uid 1000 owns the file and has execute permission)
// Actual: throws EACCES
fs.accessSync('/exec.sh', 1); // X_OK

Root cause

In @jsonjoy.com/fs-core/lib/Node.js:

const getuid = () => process_1.default.getuid?.() ?? 0;
const getgid = () => process_1.default.getgid?.() ?? 0;

// ...
canExecute(uid = getuid(), gid = getgid()) { ... }
canRead(uid = getuid(), gid = getgid()) { ... }
canWrite(uid = getuid(), gid = getgid()) { ... }

These use the global process default, not the instance-level this.process from the
Superblock.

Meanwhile Superblock.js correctly uses this.process.getuid?.() for its own path
resolution checks (lines ~350, ~378), so there's an inconsistency.

Expected behavior

accessSync/access should use the custom process object passed via memfs() options
or Volume.fromJSON() for all permission checks, including canRead, canWrite, and
canExecute on Node.

Versions

  • memfs: 4.57.1
  • @jsonjoy.com/fs-core: (bundled with memfs 4.57.1)</issue_description>

Comments on the Issue (you are @copilot in this section)


📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.

…anWrite/canExecute)

Co-authored-by: streamich <9773803+streamich@users.noreply.github.com>
Agent-Logs-Url: https://github.com/streamich/memfs/sessions/24107010-37ec-4ee4-9f28-f2cf1f4f0501
Copilot AI changed the title [WIP] Fix custom process object usage in Node permission checks fix: use custom process uid/gid for Node permission checks Mar 23, 2026
Copilot AI requested a review from streamich March 23, 2026 12:42
@streamich streamich marked this pull request as ready for review March 25, 2026 10:19
Copilot AI review requested due to automatic review settings March 25, 2026 10:19
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes permission checks to respect the instance-level injected process (uid/gid) instead of falling back to the global process identity, aligning permission behavior with the process option introduced in #1248.

Changes:

  • Threaded uid/gid from the injected process through Node.canRead/canWrite/canExecute call sites in Superblock and Volume.
  • Added regression coverage ensuring accessSync and core operations (e.g., open, mkdir, rm, rename) honor custom uid/gid.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
packages/fs-node/src/volume.ts Uses this._core.process uid/gid for access/readdir permission checks.
packages/fs-core/src/Superblock.ts Adds getUid/getGid helpers and passes explicit uid/gid to node permission checks throughout core ops.
packages/fs-node/src/tests/volume/access-custom-process.test.ts New tests validating Volume.accessSync respects custom uid/gid across R/W/X and group/world cases.
packages/fs-core/src/tests/Superblock.process.test.ts Adds tests ensuring core operations’ permission checks use the custom process uid/gid.

@@ -0,0 +1,73 @@
import { Volume } from '../..';
import type { IProcess } from '@jsonjoy.com/fs-core/lib/process';
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

The test imports IProcess from a deep @jsonjoy.com/fs-core/lib/process path, which couples the test to the build output layout and can break in workspace/ts-jest runs where lib/ isn’t present. Import IProcess from the package public entry point (@jsonjoy.com/fs-core) like other tests in this repo (e.g. volume.process.test.ts).

Suggested change
import type { IProcess } from '@jsonjoy.com/fs-core/lib/process';
import type { IProcess } from '@jsonjoy.com/fs-core';

Copilot uses AI. Check for mistakes.
Comment on lines 221 to 225
let node: Node = curr.getNode();
// Check access permissions if current link is a directory
if (node.isDirectory()) {
if (checkAccess && !node.canExecute(uid, gid)) {
if (checkAccess && !node.canExecute(this.getUid(), this.getGid())) {
return Err(createStatError(ERROR_CODE.EACCES, funcName, filename));
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

walk() now calls this.getUid()/this.getGid() inside the traversal loop for every path segment. Since these delegate to process.getuid/getgid, this adds repeated calls and could yield inconsistent permission decisions if a custom process implementation returns changing values. Capture uid/gid once at the start of walk() (when checkAccess is true) and reuse them in the loop.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot address this comment

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.

Fixed in a203327. uid and gid are now captured once before the loop (only when checkAccess is true to avoid unnecessary calls), and the cached values are reused for each directory execute-permission check within the traversal.

// Check that the place we create the new file is actually a directory and that we are allowed to do so:
if (!dirNode.isDirectory()) throw createError(ERROR_CODE.ENOTDIR, 'open', filename);
if (!dirNode.canExecute() || !dirNode.canWrite()) throw createError(ERROR_CODE.EACCES, 'open', filename);
if (!dirNode.canExecute(this.getUid(), this.getGid()) || !dirNode.canWrite(this.getUid(), this.getGid()))
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

Several permission checks call this.getUid()/this.getGid() multiple times within a single condition (e.g. execute + write checks here), causing redundant process.getuid/getgid invocations and risking mismatched uid/gid pairs if the custom process is dynamic. Fetch uid/gid once per operation (or at least once per check block) and pass the cached values to all can* calls.

Suggested change
if (!dirNode.canExecute(this.getUid(), this.getGid()) || !dirNode.canWrite(this.getUid(), this.getGid()))
const uid = this.getUid();
const gid = this.getGid();
if (!dirNode.canExecute(uid, gid) || !dirNode.canWrite(uid, gid))

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot address this comment

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.

Fixed in c7aa9b5. All multi-call blocks in Superblock.ts now capture uid/gid once before the condition/check — openFile (read/write flags block), the O_CREAT branch, symlink, rename, mkdir, and the mkdirp loop.

Comment on lines 997 to +1000
if (!node.isDirectory()) throw createError(ERROR_CODE.ENOTDIR, 'scandir', filename);
// Check we have permissions
if (!node.canRead()) throw createError(ERROR_CODE.EACCES, 'scandir', filename);
if (!node.canRead(this._core.process.getuid?.() ?? 0, this._core.process.getgid?.() ?? 0))
throw createError(ERROR_CODE.EACCES, 'scandir', filename);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

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

In _readdir, uid/gid are read inline inside the canRead call. For consistency with _access() (and to avoid calling into process.getuid/getgid multiple times), consider extracting uid/gid once at the start of _readdir and reusing them for permission checks.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

@copilot address this comment

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.

Fixed in c7aa9b5. _readdir now extracts uid/gid once at the top of the permission check block, consistent with _access().

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Custom process object not used for Node permission checks (canRead/canWrite/canExecute)

3 participants