Skip to content

portal: add a sequential VFS read bridge (vfs_open/vfs_read/vfs_close) - #101

Merged
lacraig2 merged 1 commit into
mainfrom
vfs-read
Aug 21, 2026
Merged

portal: add a sequential VFS read bridge (vfs_open/vfs_read/vfs_close)#101
lacraig2 merged 1 commit into
mainfrom
vfs-read

Conversation

@lacraig2

Copy link
Copy Markdown
Contributor

Problem

handle_op_read_file is stateless: filp_open, read at an offset, filp_close — every call. Fine for a regular file, whose bytes live at stable offsets. It cannot work for a synthetic filesystem:

  • procfs/sysfs/debugfs generate contents at open time and hand them out through sequential reads of that one open file
  • those files report st_size 0
  • a seq_file's byte offset is not a stable cursor into a fixed object

So a chunked read re-generates content per chunk, and a caller that gets nothing cannot learn why.

Not theoretical: reading /proc through the existing op fails on all 15 penguin CI arches. The symptom was never an error — a process-tree cross-check reported matched=0 ("no processes") and a socket graph reported sockets=0 ("no sockets"), on a guest that reads /proc perfectly well itself (proc_self.yaml readlinks /proc/self/exe).

The ops

Appended at the end of PORTAL_OP_LIST, so no existing op number shifts:

op in out
vfs_open path {error, handle, fs_magic}
vfs_read handle, len {error, nbytes, eof} + payload
vfs_close handle {error}

The file stays open across reads and the kernel's own f_pos advances, so a seq_file is consumed exactly as a guest process cat-ing it would.

Errors are data, never absence

Each handler replies READ_OK with a result struct carrying a negative errno. Signalling failure by returning no payload is precisely what made an unreadable file indistinguishable from an empty one. An errno also makes the failure diagnosable-ENOENT (path missing) vs -EACCES (permissions) vs -EINVAL (a file the kernel won't read this way) — which is currently impossible, because every failure path in handle_op_read_file logs only via igloo_pr_debug.

fs_magic reports sb->s_magic, so the host can name the filesystem instead of guessing.

EOF is not an error: n == 0 returns eof=1, error=0. handle_op_read_file conflates the two today and reports a legitimate empty read as READ_FILE_FAIL.

Handle hygiene

Handle is (generation << 8) | (slot + 1):

  • 0 is never a valid handle, so a zeroed/unset value can't address slot 0
  • the generation makes a read-after-close fail loudly instead of silently hitting whatever file has since taken the slot
  • the 16-slot table reclaims its oldest entry when full, with a warning, so a host that leaks handles degrades into one stale-handle error rather than a permanent inability to read any file

Build

Built for armel/6.13 and armel/4.10 — the latter deliberately, to exercise the pre-4.14 kernel_read signature behind the version guard. Handlers confirmed linked into igloo.ko:

handle_op_vfs_open   488 bytes
handle_op_vfs_read   344 bytes
handle_op_vfs_close  148 bytes

What lands next

The penguin side needs a release to build against. Once this merges and cuts one, fs.read_file grows a synthetic-fs path that uses these ops (open once → read until eof → close), and processes.peers()'s /proc/net/* join starts resolving on a live guest. penguin#938 already makes the current failure loud and correctly typed, so the transition is observable rather than silent.

Worth noting for review: this PR does not delete or change handle_op_read_file. If the errno turns out to say the old op's failure was something trivially fixable, that fix is still worth having for regular files, and these ops remain the right answer for synthetic ones.

handle_op_read_file is stateless: filp_open, read at an offset, filp_close,
every call. That works for a regular file, whose bytes live at stable
offsets, and cannot work for a synthetic filesystem. procfs, sysfs and
debugfs generate their contents at open time and hand them out through
sequential reads of that one open file; the files report st_size 0, and a
seq_file's byte offset is not a stable cursor into a fixed object. So a
chunked read re-generates the content per chunk, and a caller that gets
nothing has no way to learn why.

That is not theoretical: reading /proc through the existing op fails on all
15 penguin CI arches, and the symptom was a process-tree cross-check
reporting "no processes" and a socket graph reporting "no sockets" on a
guest that reads /proc perfectly well itself.

Three ops, appended at the END of PORTAL_OP_LIST so no existing op number
shifts:

  vfs_open(path)            -> {error, handle, fs_magic}
  vfs_read(handle, len)     -> {error, nbytes, eof} + payload
  vfs_close(handle)         -> {error}

The file stays open across reads and the kernel's own f_pos advances, so a
seq_file is consumed exactly as a guest process cat-ing it would.

Errors are returned as DATA, never as absence: each handler replies
READ_OK with a result struct carrying a negative errno. Signalling failure
by returning no payload is what made an unreadable file indistinguishable
from an empty one; an errno also makes the failure diagnosable (-ENOENT
path missing, -EACCES permissions, -EINVAL a file the kernel will not read
this way). fs_magic reports sb->s_magic so the host can name the
filesystem instead of guessing. EOF (n == 0) is reported as eof=1 with
error=0 -- a successful read of nothing -- where read_file conflates it
with failure and returns READ_FILE_FAIL.

Handle hygiene: the handle is (generation << 8) | (slot + 1), so 0 is never
valid and a read after close fails loudly instead of hitting whatever file
has since taken the slot. The 16-slot table reclaims its oldest entry when
full, with a warning, so a host that leaks handles degrades into one stale
handle rather than a permanent inability to read any file.

Built for armel/6.13 and armel/4.10 (the latter exercises the pre-4.14
kernel_read signature); handlers confirmed linked into igloo.ko.
@lacraig2
lacraig2 merged commit 5abf0bb into main Aug 21, 2026
3 checks passed
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