fix: use custom process uid/gid for Node permission checks#1251
fix: use custom process uid/gid for Node permission checks#1251
Conversation
…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
There was a problem hiding this comment.
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/gidfrom the injected process throughNode.canRead/canWrite/canExecutecall sites inSuperblockandVolume. - Added regression coverage ensuring
accessSyncand 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'; | |||
There was a problem hiding this comment.
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).
| import type { IProcess } from '@jsonjoy.com/fs-core/lib/process'; | |
| import type { IProcess } from '@jsonjoy.com/fs-core'; |
| 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)); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
packages/fs-core/src/Superblock.ts
Outdated
| // 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())) |
There was a problem hiding this comment.
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.
| 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)) |
There was a problem hiding this comment.
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.
| 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Fixed in c7aa9b5. _readdir now extracts uid/gid once at the top of the permission check block, consistent with _access().
…s calls Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> Agent-Logs-Url: https://github.com/streamich/memfs/sessions/b4e727ab-1a40-4b57-b51e-33292c392e26
Co-authored-by: streamich <9773803+streamich@users.noreply.github.com> Agent-Logs-Url: https://github.com/streamich/memfs/sessions/35ee794d-148a-45a7-a229-e07ade7dc856
Node.canRead/canWrite/canExecutedefault parameters captured the globalprocess.getuid/getgidat module load time, ignoring the instance-levelprocessoption introduced in #1248. This meantaccessSync(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 privategetUid()/getGid()helpers delegating tothis.process; threaded them through all barecanRead/canWrite/canExecutecalls inopenLink,openFile,symlink,rename,mkdir,mkdirp, andrmvolume.ts—_access()and_readdir()now extract uid/gid fromthis._core.processbefore calling node permission methodsExample (previously broken)
Original prompt
📱 Kick off Copilot coding agent tasks wherever you are with GitHub Mobile, available on iOS and Android.