Skip to content

Preserve descriptor identity through devfs aliases - #876

Open
brandonpayton wants to merge 1 commit into
mainfrom
fix/devfs-inode-identity
Open

Preserve descriptor identity through devfs aliases#876
brandonpayton wants to merge 1 commit into
mainfrom
fix/devfs-inode-identity

Conversation

@brandonpayton

Copy link
Copy Markdown
Member

Contract

Make /dev/fd/N and /dev/std{in,out,err} truthful descriptor aliases. Path-based metadata that follows an alias must describe the same open file description as fstat(N); no-follow metadata must describe the devfs symlink itself.

Root cause

The Rust kernel intercepted these paths before host VFS delegation, but sys_stat and sys_fstatat returned a hard-coded character-device stat instead of consulting the process fd table. With captured stdio, the measured guest tuples were:

Operation st_dev st_ino st_mode
stat("/dev/stdin") before 5 0 020666
fstat(0) 0 0 010600

GNU coreutils' descriptor heuristic interpreted that false character device as stdin while evaluating /dev/null, then rejected an ordinary copy as replacing the source. host/src/vfs/device-fs.ts is not on this syscall path; the mismatch was entirely in the shared kernel.

Change

  • Route following stat, fstatat, and therefore statx calls through sys_fstat for the referenced descriptor.
  • Report stable devfs symlink metadata from lstat and AT_SYMLINK_NOFOLLOW; implement matching readlink targets and reject final-component aliases under O_NOFOLLOW.
  • Keep /dev, /dev/fd/N directory entries, and symlink stats in the same devfs device/inode namespace.
  • Add one C fixture exercised through both NodeKernelHost and real Chromium BrowserKernel paths.
  • Update the authoritative POSIX status documentation.

Node.js and browser use the same Rust kernel implementation. There are no host-specific branches in this fix.

ABI and artifacts

No syscall number, argument meaning, marshalled layout, channel field, export, custom section, or generated binding changed. This corrects an existing POSIX/devfs behavior contract for already-built programs, so ABI_VERSION is unchanged and no package or VFS artifact rebuild is required. The ABI snapshot and generated bindings remain byte-for-byte current.

Validation

  • ./scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh cargo-kernel - 968 passed, 0 failed.
  • ./scripts/dev-shell.sh bash -c 'rm -rf host/dist && cd host && npx vitest run test/dev-fd-stat.test.ts' - passed.
  • ./scripts/dev-shell.sh bash -c 'rm -rf host/dist && cd apps/browser-demos && CI=1 KANDELO_PLAYWRIGHT_PORT=5297 npx playwright test test/dev-fd-stat.spec.ts --project=chromium' - passed.
  • GNU coreutils cp /dev/null /tmp/empty through NodeKernelHost - exit 0, empty stdout/stderr.
  • ./scripts/dev-shell.sh bash scripts/check-abi-version.sh - snapshot, C header, TypeScript bindings, and ABI_VERSION consistent.
  • ./scripts/dev-shell.sh bash scripts/ci-run-test-suite.sh posix - 174 passed, 3 declared XFAIL, 2 unsupported, 0 unexpected failures.
  • ./scripts/dev-shell.sh bash scripts/run-libc-tests.sh functional stat - passed.
  • ./scripts/dev-shell.sh bash scripts/run-sortix-tests.sh basic sys_stat/stat sys_stat/lstat sys_stat/fstat sys_stat/fstatat - 4 passed.
  • git diff --check - passed.

Not run

  • Firefox and WebKit were not run locally; Chromium covers the real browser-worker path and CI will run the repository browser gates.
  • Full libc-test and Sortix matrices were not run; the directly relevant stat suites above were run, plus the full Open POSIX suite.
  • A broad Vitest attempt passed 97 test files and 758 tests. Four failures caused by a borrowed non-canonical rootfs were rerun successfully after building the canonical image; two unrelated wasm64 tests could not run because this worktree had no sysroot64/hello64.wasm. The full Vitest command was not rerun end-to-end.
  • cargo fmt is unavailable in the canonical dev shell (cargo: no such command: fmt); the Rust diff is manually rustfmt-shaped and git diff --check passes.

@brandonpayton

Copy link
Copy Markdown
Member Author

CI note: the red test-gate-prepare / dependent test-gate checks did not reach this PRs devfs tests. Fetch-only materialization failed on the existing ABI 16 release-index drift for four unrelated packages:

  • lamp: index 9f777974, downloaded archive cache key 64104909
  • node-vfs: index 5ca2294d, archive 7618c9c6
  • shell: index 2f4b693f, archive f7dccdb9
  • wordpress: index 6c52e8a5, archive 76133d30

This is the same external baseline failure documented on #875. No branch change is warranted; rerun the gate after #867 establishes the ABI 18 publication baseline.

Following stat and fstatat calls on /dev/fd/N and the stdio aliases now return the referenced descriptor's authoritative metadata. No-follow calls report stable devfs symlink metadata, readlink exposes the alias target, and O_NOFOLLOW rejects the final symlink.

Keep devfs directory, getdents, and lstat device/inode metadata coherent. Add a guest regression exercised through both NodeKernelHost and BrowserKernel. This fixes GNU coreutils mistaking /dev/null for stdin without changing the ABI.
@brandonpayton
brandonpayton force-pushed the fix/devfs-inode-identity branch from c832d4d to 35f0455 Compare July 12, 2026 03:43
@brandonpayton

Copy link
Copy Markdown
Member Author

ABI 18 rebase

Rebased the single semantic commit onto current ABI 18 main 2c083e143; new head is 35f04550738e8d429762cc2c73cac45aa9429fcf.

  • git range-diff reports an exact semantic match.
  • Old/new stable patch ID is identical: 5f67d9a38e8ce7a942f61bf8e3dfa02bba4e58ad.
  • git diff --check origin/main...HEAD passes.
  • The worktree is clean.

I did not rerun the local runtime suites for this history-only rebase. Hosted CI is now running against the ABI 18 merge base and the repaired ABI 18 binary ledger; the prior ABI 16 materialization failure is superseded.

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.

1 participant