Skip to content

wasi: a task's named grants as preopens, and the two faults that hid every preopen on Kore - #26

Open
SnowCheetos wants to merge 8 commits into
alphafrom
ft/stoic-davinci-mlhpjo
Open

SnowCheetos wants to merge 8 commits into
alphafrom
ft/stoic-davinci-mlhpjo

Conversation

@SnowCheetos

@SnowCheetos SnowCheetos commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lets an embedder offer a task's named grants to a WASI guest as preopens, and fixes two faults in kore-wasi's filesystem host that kept a guest from ever seeing its preopens on Kore. Together they let CPython 3.12, run by kore's wasm command as a wasi:cli/command component, find its stdlib beside its own executable. The kore half is MicroPerceptron/kore#1273, which pins this branch's head, 7afc2d0.

Changes

  • Named grants as preopens (90f5fa8). A guest's one preopen was ., the task root, walked a containment step at a time and never through a named grant, by design. On Kore the app store seeds programs and their data under shared, a grant the shell's walk resolves by name, so CPython launched as shared/wasm/python.wasm could stat neither itself nor shared/wasm/lib/python312.zip. Filesystem::with_named_grants lets the embedder opt in: each named grant becomes a preopen under its store name beside ., opened with the widest rights the service answers. The private root and unnamed grants are left out, a guest still never reaches a grant through ., and an embedder that says nothing offers nothing, as before.
  • A descriptor reopens from its own preopen (7010ac6). open-at of . reopens a directory by its path from the preopen, which was always the root; beneath a grant that named a different object, or none. A descriptor now carries its origin (the root or its grant) and reopens from there.
  • A refused preopen traps with the refusal (acad200). get-directories has no error to return. It answered an empty list when the root was refused and silently left out a refused grant, so a guest read "no files" where the service had said why. It now traps, naming what was refused.
  • A completed op's result is handed back; each preopen is opened once (d2fcddb). FsHost::exec drives the task's one filesystem op through a single slot. Its last arm, reached when an op installed on an earlier turn finishes, returned None unconditionally, dropping the result whenever the op had queued no view to close. That is always the case for get-directories, whose opens return their views owned, so every call answered an empty preopen list. It now hands back what the op produced. And get-directories reopened every preopen on each call (the preview1 adapter asks once at start and again per name); the preopens are now opened once and shared for the filesystem's life.
  • Regression tests for the slot driver (eb94cfe). Two tests drive FsHost::exec over a host with no face, with ops that park once: an op is answered with its result on the turn it finishes, and a caller dropped mid-op leaves its op to finish on the next caller's turn, which still gets its own result. Both fail against the old arm, which answered Ready(None).
  • A named grant of one file is not a preopen (daee213, from review). open_grant enters a granted file as readily as a subtree root, and every named grant opened was published as a directory preopen. A grant whose root is not a directory is now closed and left out. It is not refused, because a refused preopen traps the guest, and this guest asked for nothing it lacks.
  • alpha merged in (7afc2d0): Lua 5.5 (lua: Lua 5.5 as a crate, with io, os and kore.fs #23, lua: add bounded read-only worker policy #25) and the Wasmtime 48.0.3/48.0.5 RUSTSEC bumps. kore main had pinned 3f82da8, the head of lua: add bounded read-only worker policy #25, whose tree alpha holds as 7a76d19; with this merge one libkore revision carries both that and this branch's kore-wasi, so kore can pin kore-std, kore-wasi and kore-lua together. The merge changes nothing of alpha's: against alpha, this branch differs only in the three crates/wasi files above.

The two d2fcddb faults show only where a filesystem call parks and is driven to completion across turns, which never happens under a host Wasmtime's blocking calls; that is why no host test saw them. They were found on Kore under QEMU with a small C probe guest (clang wasm32-wasi, the preview1 adapter) that lists its preopens and stats what it is handed. The origin fix (7010ac6) and the file-grant rule (daee213) have no unit tests: each turns on an open_root/open_grant round trip that needs a real Kore face, and libkore does not mock its owners. The kore keytest below covers the directory-grant path end to end; no keytest holds a named file grant.

Verification

  • On 7afc2d0: cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo test --workspace --all-features (75 passed, alpha's Lua suite included), cargo doc --no-deps under -D warnings, check-owners and check-native: pass locally with Zig 0.16.0. CI was green on daee213.
  • Not run locally: miri (excludes kore-wasi), cross and bindings-check (they cover only kore-std, kore-ffi and kore-entry, which this branch's own commits do not touch).
  • On Kore under QEMU with 8 GiB, MicroPerceptron/kore#1273's keytest-python-x86 runs CPython importing sys, json and decimal through zipimport and requires the computed line kore 3 97, with libkore pinned at 7afc2d0: pass. Not yet run on real hardware.

Landing

Merge this first, with a merge commit (as #21 and #23 were), so 7afc2d0 stays reachable from alpha and kore's pin names an accepted alpha commit with no follow-up. If this is squashed instead, MicroPerceptron/kore#1273's pin has to move to the squash commit before it merges.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx

claude and others added 5 commits October 14, 2026 10:51
A guest's filesystem was one preopen, `.`, the task's root view, walked
one containment step at a time — deliberately never through the task's
named grants, since a guest opening `data/x` would reach authority its
embedder holds and never handed it. That left a guest unable to see
what its launcher sees: on kore the app store seeds programs and their
data under `shared`, a grant the shell's own walk resolves by name, and
CPython launched as `shared/wasm/python.wasm` could stat neither itself
nor `shared/wasm/lib/python312.zip` beside it (`getpath` then fell back
to a prefix of `/` and died for want of `encodings`).

`Filesystem::with_named_grants` lets the embedder offer them: each named
grant becomes a preopen of its own under its store name beside `.`,
opened with the widest rights the service answers (the root's own
ladder), the private root and unnamed grants left out. The guest walks
beneath a grant exactly as beneath `.` and still never reaches one
*through* `.`; an embedder that says nothing offers nothing, as before.
The listing is read whole before any grant is opened, since the next
request reuses the pair's window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
`open-at` of `.` reopens a directory by its path from the preopen, since
a view has no duplicate verb — and the preopen was always the task root.
With named grants offered as preopens of their own, a descriptor beneath
one reopened as `.` would have walked its path from the root instead and
named a different object, or none. A descriptor now carries its origin,
the root or the grant it descends from, and reopens from there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
`get-directories` answered an empty list when the task's root could not
be opened, and left out a named grant the service refused, so a guest
read "no files" where the service had said why. The interface has no
error to return; the refusal is now a trap naming what was refused.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
…n once

Two faults kept a guest from ever seeing its preopens on Kore, where a
filesystem call parks and is driven to completion across turns.

`FsHost::exec` drives the task's one filesystem op through a single slot:
it installs the op, polls it to `Ready`, and the op writes its result into
a shared cell. The loop's final arm — reached once the op has been taken
and the slot has drained — returned `None` unconditionally, throwing away
the result the op had just written whenever the op queued no view to close
(the common case: `get-directories`' opens return their views owned, so
nothing is orphaned). The one caller reads that `None` as a lost pair, so
every `get-directories` answered an empty preopen list and a program that
looks beside its executable — CPython for its stdlib — found nothing. The
arm now hands back what the op produced.

And `get-directories` reopened the root and every named grant on each call.
The preview1 adapter asks once at startup and again for each preopen name
it reads back, so a handful of fds became dozens of opens and immediate
closes. The preopens are now opened once, on the first call, and every
later call answers descriptors over the same shared views — one open per
preopen for the guest's life, one route held per grant, released with the
filesystem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
The slot driver's dropped result (d2fcddb) hid every preopen from a guest
on Kore and passed every host test, because nothing on the host parks an
op across turns. Two tests drive `FsHost::exec` over a host with no face,
with ops that park once, as every op with a request in flight does on
Kore: an op is answered with its result on the turn it finishes, and a
caller dropped mid-op leaves its op to finish on the next caller's turn,
which is still answered with its own. Both fail against the old arm,
which answered `Ready(None)`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
Copilot AI balanced review requested due to automatic review settings October 14, 2026 19:14

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

Named file grants can be published as directory preopens, violating the WASI interface contract.

1 open finding
What changed in this PR

Adds opt-in named-grant preopens to kore-wasi, enabling guests such as CPython to find files outside the task root while preserving containment.

Changes:

  • Exposes and caches named grants as preopens.
  • Preserves descriptor origins and reports preopen refusals as traps.
  • Fixes delayed filesystem results and adds regression tests.
File Description
crates/​wasi/​src/​filesystem/​walk.rs Documents containment for named-grant preopens.
crates/​wasi/​src/​filesystem/​mod.rs Tracks descriptor origins and publishes cached preopens.
crates/​wasi/​src/​filesystem/​host.rs Opens and caches grants; fixes and tests delayed results.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread crates/wasi/src/filesystem/host.rs Outdated
`Fs::open_grant` enters a granted file as readily as a subtree root, and
`get-directories` answered every named grant it opened as a directory
preopen, so a task holding a named file grant would hand its guest a
"directory" whose every walk fails with NotDirectory. A preopen is a
directory: a grant whose root is not one is now closed and left out. It is
not refused, since a refused preopen traps the guest (acad200) and this
guest asked for nothing it lacks. The open loop records what it opened
rather than inferring it from the last preopen's name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
Brings in Lua 5.5 (#23, #25) and the Wasmtime 48.0.3/48.0.5 RUSTSEC bumps,
so kore can pin one revision carrying both its kore-lua worker and this
branch's kore-wasi changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
`wasi:cli/terminal-stdin`, `-stdout` and `-stderr` answered "no terminal"
unconditionally, so a guest's `isatty` was always false and CPython never
opened its REPL without `-i` (and block-buffered its output on the shell).

Whether the far end of the standard streams is a terminal is something the
embedder knows and the streams do not: on Kore it is the profile the
launcher declares on the process stream page. `WasiCtx::with_terminal`
takes that declaration; with it the three getters hand the guest a
terminal handle (a resource with no methods: holding one is the answer),
and without it they keep answering `None`, as for redirected stdio. All
three streams share the one process stream, so they answer together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TmEMGSFqb56Tcsvuk9PQPx
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.

3 participants