Repository navigation
Cowork N2: guest side of shares (SH6) - #139
Merged
Merged
Conversation
…step, PID 1 inventory, mounts, mountinfo check, guidance alias, drain order); the nine shares transcripts completed
…uard mutants, the image's system gitconfig with an empty root-owned init.templateDir (R15), guest pins re-pinned
…ed mount point's descriptor; a test-build-only owner lets the unprivileged test reach the refused mount(2); a root CI step proves the step, the mountinfo reader and every check on real mounts (tmpfs, ext4 with dax=never, the refused virtiofs call); a source-derived virtio-fs mountinfo fixture; 48 guard mutants; init re-pinned
…cowork/n2-sh6-guest-shares
…tshare subcommands (dropped in the review fold), and a test that runs everywhere and fails when a subcommand the Linux tests use is missing from the driver
…the refused-before-mounting test counts mount(2) calls, exact-size payload truncations, two mountinfo parser cases, a root test of the alias owner, UMOUNT_NOFOLLOW on share unmounts, the named mutants (root-only ones run in CI's root step); init re-pinned
…cowork/n2-sh6-guest-shares # Conflicts: # .github/workflows/cowork-guest-boot.yml # apple/Cowork/WinterCoworkGuest/tests/linux_init_tool.c # scripts/cowork-guest-artifact/image.py # scripts/cowork-guest-artifact/pins.json
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
SH6 of the N2 extensions design (
docs/research/2026-10-18-n2-extensions-design.mdsections 2.1, 2.5, 2.6, R6, R8, R15, 4b.3, O14), against contract revision 3 (docs/cowork-native-worker.mdsection 12,native/protocol.ts). Guest only: no host, libkrun or SDK change. No VM was run.What the guest now does
After
mountedand beforeconversationsthe helper accepts one bootstrapsharesmessage:operationIdandnonce(asconversationsdoes), arrive before any conversation set, at most once, with nothing more from the host untilshares_mounted(checked before PID 1 is asked, and again for anything that arrives while PID 1 mounts). Any departure, and a broken or unbound share set, isshare_mount_failedwith nothing mounted. A set that follows a share step that did not end inshares_mountedisbad_conversationsand never reaches PID 1; PID 1 fail-stops on one as a helper fault (defence in depth).<sys>/fs/virtiofs/<n>/tagmust equal the declared tags exactly (no extra, missing or repeated tag), and the virtio bus must hold exactly as many type-26 devices, so a virtio-fs device that published no tag is an extra device too. Otherwiseshare_inventory./run/winter/shared-instructions,/workspace/.winter/memory,/workspace/outputs) and are never made here. A folder's/workspace/folders/<grantId>is created only directly below the skeleton's/workspace/folders, root's, mode 0700 (PID 1's umask). Every component is openedO_NOFOLLOWand must be root's; the mount point must sit on its parent's filesystem.mount(tag, /proc/self/fd/<n>, "virtiofs", MS_NOSUID|MS_NODEV [|MS_RDONLY] [|MS_NOEXEC on guidance, memory, outputs], "dax=never"); folder sources keep exec. The target is the descriptor of the mount point the walk checked, held open from the check to the mount, so the path is never resolved a second time./, fstypevirtiofs, source the tag,roexactly when read-only,nosuidandnodev,noexecexactly on a Winter share,dax=neveras the only DAX option, and the tag mounted nowhere else. Any mismatch isshare_mount_failed./workspace/.winter/WINTER.mdis a root-owned symlink to/run/winter/shared-instructions/WINTER.md; one left by an earlier boot is kept only if it is exactly that link.shares_mounted. Any failure after a mount unmounts what the step mounted (last first); what could not be unmounted is handed to the drain.Never chown or chmod. The share step contains no chown, chmod, xattr, truncate, unlink or write open (a test reads the source); a behavioural test compares every owner and mode in the tree before and after the step.
Drain. The shares are unmounted first, last mounted first; the ext4 volumes are unmounted only once no share is mounted.
haltingstill means every filesystem this boot mounted is unmounted: a share that stays mounted withholds it (fault afterdrain_ack, as for a failed ext4 unmount).Image (R15). Integration's #140 ships the system gitconfig: the pinned
scripts/cowork-guest-artifact/environment/gitconfigat the base's/etc/gitconfig, withinit.templateDir = /usr/share/winter/git-empty-template, an empty root-owned directory, checked byverify.check_gitconfig_textandcheck_environment_gitconfig. SDK SH6b mounts that file read-only into the process worker. This branch's own gitconfig was dropped in the merge in favour of integration's; the two would have clashed at the same path.What the browse uid can and cannot see on a share (O14)
st_dev(4b.3 step 2) and walks withRESOLVE_NO_XDEV. Every share root is a virtio-fs mount with its own device, so a share root, or a share mounted below a data-volume root, answersfile_unavailable. PID 1 makes no share a data root (/workspace/folders/*,/workspace/outputs,/workspace/.winter/memoryare skipped, Cowork N2: guest side of the file channel (FC3), gated on file-channel-v1 #122) and binds no share into any compartment view.root:workspaceGid 02770data roots (FC3), with no capability (SDK FC4). A file a tool made 0600 stays unreadable (accepted, O14).LinuxSimplifiedwith no xattrs (R7): guest uid, gid and mode bits on a share are a simplified view, not an access boundary. A group-only rule, a setgid root or a chmod there would promise nothing. So PID 1 never chowns or chmods a share mount point or anything on a share. The boundaries for shares are the mount view, the root-device check, and the host side (the confined server and its Seatbelt profile). Mac readers read shares directly on the host, so the channel loses nothing.Decisions
file-channel-v1speaks. All PID 1 code is underWG_FILE_CHANNEL_V1(newinit/shares.{c,h}, built only intoinit-file-channel). The production programs' preprocessed sources are unchanged, so their binary pins are kept; their source pins and the helper's script pin are re-pinned (PinTestspasses).share_mount_failedwith nothing mounted; an inventory mismatch isshare_inventory.dax=never. This kernel hasCONFIG_FUSE_DAX=y, so it does. The optionalCONFIG_FUSE_DAX=nfragment would stop it printing, and must change this one rule in the same change.The nine
sharestranscriptsbootstrap-order.json's nine share transcripts that #122 left waiting are now completed by the guest like the other fifteen (24 of 25;plan-before-hellocannot involve it), with exact outputs pinned for each, and a test that the share set reaches PID 1 only in the three transcripts where the order allows it.Review fold (REJECT at 0c61494)
op_mount_sharespassed owner 0, so on the unprivileged runner the walk refused the test's own directories beforemkdiratormount.init_linux.cnow hasSHARES_OWNER. It defaults to 0, onlytests/linux_init_tool.cbuilds pass another value, andbuild.pypasses none. A guard test holds that, and two mutants of it are killed. The test now reaches the realmount(2)and asserts its refusal:errno=EPERM, nothing left mounted, and only the folder's mount point made (0700).cowork-guest-boot.ymlruns therealmounttests. Each runs in its own private mount namespace, withWG_REQUIRE_REALMOUNT=1so a test that cannot run fails rather than skips:nr_inodes=4096in place of virtiofs anddax=never.EINVAL"tag not found", orENODEV) because this runner has no virtio-fs device, with the step undone.dax=neveron a loop device: a realdax=neverline, judged by the same DAX rule.dax=neverline.tests/virtiofs-mountinfo-v6.18.jsonis derived, not captured: no VM may run in this lane. Each line is assembled from the Linux v6.18 source of the functions that print it (show_mountinfo/show_vfsmnt_opts,fuse_show_options,virtio_fs_init_fs_context/virtio_fs_get_tree,fuse_dax_conn_alloc), each recorded with its URL and SHA-256. That source shows a virtio-fs mount prints the tag as its source andro|rw,dax=neveras its super options: the context is zeroed, so no legacy options. Every share passes against it and fails with any other DAX option. G7 replaces it with a captured live table.shares.ccomment on the exactly-one-entry rule now says what it does: none means not mounted; two or more means stacked on, or under, another mount.Second fold (ACCEPT at 5654174, lows)
WG_REQUIRE_REALMOUNT=1,test_pid1_linuxraises insetUpModulewhen it lacks Linux, a compiler, root or unshare(1). The class decorators would otherwise skip every test first. A test that runs everywhere pins this.mount(2)calls, and the test assertsmounts=0(the mountinfo rollback masked this before).UMOUNT_NOFOLLOWon the share rollback and drain unmounts.st_devcheck and the alias owner check). CI's root step runs those two throughRootMutantTests, which also refuses to skip.Tests
SharesPid1Testsas the runner user (4: the inventory and real-sysfs refusals, the realmount(2)EPERM undoing the step, the runner's own mountinfo), and as root in the new step (6realmounttests: the whole step on tmpfs, a mount point that is already a mount, the refused virtio-fs call, every check on real output, ext4 withdax=never, a tmpfs probe with a share's flags), plus the existing Linux-only suites.test_hygiene.py,guest-boot-anti-wiring.test.ts) covershares.c; a test pins that.NEEDS
/workspace/.winter/memory(LINUX_MEMORY_ROOT, and the supervisor'sMEMORY_ROOT), and the process worker mounts the base's/etc/gitconfig.expectedArtifact) are stale, because the helper changed in the cpio. A new owner-armed one-run image build re-pins them.dax=never) and replace the derived fixture with it. Also confirm the virtio device count with 12 shares: discovery refuses more than 16 virtio devices; 12 shares plus the console and two disks is 15.