Repository navigation
SubOS architecture: isolation, roots, carriers and Luban (#640) - #641
Merged
Merged
Conversation
Sunrisepeak
added a commit
to openxlings/xim-pkgindex
that referenced
this pull request
Oct 6, 2026
* feat: Luban editions and the packages a SubOS root is made of For xlings SubOS design part 2 (openxlings/xlings#641): a SubOS presented as /, exported as an image, booted by a kernel -- Luban. - subos:luban-tiny / luban-core / luban-desktop: edition templates (no download; install() writes them): the packages a root is made of, its init, its factory /etc, sysusers. core is `from` tiny, desktop from core. - bash 5.2.37, coreutils 9.5: new, built against xim:glibc 2.44.3 with xim:gcc 16.1.0 (form X; libc.so.6 is their only NEEDED). - linux-kernel 6.8.0-71: Ubuntu noble's generic vmlinuz, repackaged at lib/modules/<ver>/vmlinuz; virtio, ext4 and the serial console are built in, so a root on /dev/vda boots with no initramfs. - busybox (revision 1): the payload's bin/ carries a relative link per applet, which is what a root's /usr/bin is made of. Only `busybox` is registered, so a home's PATH is unchanged. - perl (revision 1): 29 scripts (perldoc, pod2man, prove, cpan...) said `#!/build/stage/bin/perl` and started on no machine but the build's. - binutils 2.42.1 (revision 1): the ld wrapper takes its directory with ${0%/*} instead of dirname, so it runs in a root without coreutils. * luban-tiny: init's restart re-execs stage-0 (subos boot --now) * luban-core: no curl (it has no linux build); xlings fetches * fix: spec -- subos-type packages are registered by xlings' default config; linux-kernel registers its name; the ld wrapper's note moves out of its lines * revert the index cache a local run rewrote
added 27 commits
October 8, 2026 08:06
The design consolidates the earlier SubOS isolation documents into one: deployment and run modes, policy presets and the decide function, enter and exec, the human/agent contract, the platform abstraction layer, observability, module layout, and the test/CI architecture. The implementation plan cuts it into the checkpoints this PR lands as commits, with the dependency graph and the scope decisions. docs/design/subos-isolation.md: image is a storage mode, not an isolation level; bwrap runs in user-namespace mode (the setuid chmod silently fails without sudo); the known #640 issues are listed. AGENTS.md: core and interaction surfaces are separate; the audience is declared with --agent / XLINGS_AGENT_MODE.
modules/testkit answers the four questions every end-to-end test used to answer for itself, differently each time: - which binary: XLINGS_BIN when it names a file, else the newest target/**/bin/xlings (the shell profile exports XLINGS_BIN as a directory, so a developer's shell already gives it another meaning); - which home: Home::isolated() under the system temp dir, removed unless the test failed, in which case its config/logs/state are kept as artefacts; - which environment: run() starts from nothing and adds what the test names; inheriting XLINGS_ACTIVE_SUBOS is what made a unit test see the developer's subos; - why it did not run: XTEST metadata declares required capabilities; missing is a skip on a developer machine and a failure on a lane that declared it in XDEV_LANE_CAPS. The runner covers POSIX (pipes, process groups, pty with nothing typed) and Windows (CreateProcess, job object). XTEST_META_OUT and XTEST_RESULTS_OUT write NDJSON for the report. The proof case ports subos_cmd_contract_test.sh and .ps1 to one C++ test. The macOS shell copy never asserted anything: bash 3.2 does not trip set -e on a failing [[ ]], and the last main run printed 'No such file' for the path it checked and then 'ok'.
apps/xdev is the project's development tool, its own workspace member:
the root build does not build it, so the product is unchanged
(mcpp build -p xdev).
- xdev test runs mcpp test --message-format json with the XTEST outputs
wired, then the legacy suites from tests/suites.toml through an adapter
(one record per command: exit code, duration, log tail);
- xdev report renders one report from any number of run directories:
pass/fail/skip per test binary, XTEST case and script, failures with
output, skips grouped by reason, slowest tests, lane capabilities; to
the terminal, report.{md,json}, and the GitHub step summary;
- xdev doctor lists what this machine can test.
tests/suites.toml carries the contract scripts and lint checks CI runs
inline today. tests/README.md is rewritten (it still described
test_main.cpp and linux_usability_test.sh).
test_interface_protocol no longer inherits XLINGS_ACTIVE_SUBOS from the
shell that runs it; inside a subos it failed locally and never in CI.
xlings-ci-linux-e2e.yml built the same commit a second time from source (E2E-00) and a third time as a release, to run a 3.6-minute suite. It is now the e2e job of xlings-ci-linux.yml and takes build-and-test's release artifact; the binary under test is the one extracted from it. - unit-asan (the slowest job, ~27 min) runs on push to main/release and on PRs labelled ci:asan; E2E-00 (fresh mcpp home builds xlings) moved there too, on push. - Tests run through xdev on Linux, macOS and Windows. Each lane declares its capabilities (XDEV_LANE_CAPS); the Linux lane installs bubblewrap and lifts the AppArmor userns restriction for itself, so its isolation tests run instead of skipping. - run_all.sh writes one record per test when XDEV_RECORDS is set; a report job merges the Linux lanes into the step summary. xdev is built static (musl) so the e2e and report jobs can run the same binary. BMIs stay uncached: restored BMI sets fail with 'CRC mismatch', as the cache step documents.
tests/requirements.toml declares every behaviour the SubOS design promises, by ID: the findings it fixes (F1-F16), the exit codes, the agent contract, policy, isolation, sessions, permissions, observability, home and compatibility. Each carries a status: - required: built, must be covered; - planned: its checkpoint has not landed; the commit that lands it flips the status, so the gate grows with the PR; - deferred: not in this PR, with the reason (net=proxy, rootfs). An isolation ID is only covered by a test that runs a real sandbox (proves = "isolation"): the fake provider proves a path is wired, not that anything is isolated. xdev report --requirements ... --fail-uncovered fails on an uncovered required ID and on a test naming an undeclared ID; the Linux report job runs it over every lane's metadata. check_requirements() takes the lane's declarations explicitly too, and the lane rule that replaces skip-when-missing (F13) is tested with it.
Two things both the xlings core and the SubOS core need, and neither may own, become their own packages: - xlings.guard: the UserConfirmed token and the Asker port. ask() takes an Asker; the core never decides how a question is put. The token can still only come from ask(), so the rule that only a confirmed deletion may remove user data stays a compile error to break. - xlings.observe: the event model (ops, lifecycle, perm, exec, net, fs, destructive, trace), a journal that rotates by size and never fails its caller, redaction (names, never values) and XLINGS_TRACE. src/core/confirm and src/core/destructive_log keep their names and APIs as adapters (the EventStream is an Asker; the destructive record is bound to this home), so none of the 28 call sites changes. The destructive record is never rotated: it exists to attribute a loss after the fact. Behaviour is unchanged; the full unit suite passes.
The macOS leg's member output layout differs from Linux's, the path pattern matched nothing, and an empty $xdev ran as exit 127 after a successful build.
… in (C2) The SubOS core gets its own package. It depends on json, platform, guard and observe, and may not import xlings.core.*: what it needs from the package manager arrives through ports (next checkpoint), so a build that reaches for xim from here fails to compile. gpu, graphics and manifest already depended on nothing but std, json and platform; they move with history (git mv) and their modules are renamed xlings.core.subos.* -> xlings.subos.*. Namespaces are unchanged, so the only edits outside the move are import lines. No behaviour change.
…re (C3) - xlings.subos.home_view: where a SubOS's things live -- the instance, and, outside it, its policy (config/subos/<n>), its audit (logs/subos/<n>), its sockets (run/subos/<n>) and the host capability cache (state/). The SubOS core gets this from xlings core instead of reading Config. - xlings.subos.ports: what the SubOS core needs from xlings (install a backend, recognise a shim's owner), received as functions. - src/core/subos/ports: the adapter that builds both from this home and xim / xvm. - userdata moves with history; delete_subos takes the HomeView and a guard::UserConfirmed, and records through observe. The remove_all lint now covers modules/ and treats all of modules/subos as lifecycle code. keeper is not moved: the session model replaces it (C9), and moving dead code first would only move it twice. No behaviour change: unit tests and subos_user_data_test.sh pass.
The rules that turn a typed name into an instance -- exact, unique case-insensitive, unique prefix, ambiguous, not_found with suggestions ranked by relation and edit distance -- are pure functions over names in xlings.subos.model. src/core/subos.cpp keeps the adapter half: reading the registry, counting commands and packages through xvm, and mapping the model's answer back to instances. Every surface that resolves a name (use, and next exec / start / the interface) shares one implementation. No behaviour change: subos_use_candidates_test.sh passes; the rules are pinned by unit tests without a home.
src/core/home (xlings.core.home) is the one answer to which home, in which deployment mode, at which layout: - Config records how it chose the home (anchored, XLINGS_HOME, self-contained, default); Config::home_context() adds what the marker declares. - .xlings-home gains mode (user, custom, portable, system, multi) and layout. self init declares both at creation; a marker that predates modes is inferred once from where the home is and then declared. Writes keep every key this client does not know; the layout only rises. - layout 2 adds config/subos, logs/subos, run/subos and state -- only directories, so older clients are unaffected. - A home at a layout this client does not know is read and never written: mutating commands refuse and name the home's own client. Tests: unit tests for describe/declare/read-only commands, and e2e that a first command declares the home and that a newer layout refuses 'subos new' while 'subos list' works.
… it (C6)
home::read_json_for_update is the writer rule in one place: a missing
file is a new, empty document; a file that exists and does not parse as
an object is an error, and the writer does not write. Writers update the
object they read, so keys they do not know survive.
Two writers broke it:
- Config::save_versions turned an unparseable home .xlings.json into
{versions}, erasing the mirror, the subos registry and known projects;
- ensure_subos_info_ (subos new on an existing directory) rebuilt an
unparseable manifest from {}, writing a blank workspace over the
subos's real one.
Both now leave the file untouched and say so, as the workspace writer
already did. The SubOS policy file gets its place outside the instance
(HomeView::policy_file, config/subos/<n>/policy.json) for the same
reason: a document the sandbox could rewrite would govern nothing.
Policy + HomeView + Caps -> SandboxSpec -> provider (design §16-§17): - xlings.subos.policy: the policy model as data (presets, net, fetch, observe, identity, env pass-list, mounts, named grants). Legacy is the policy of an instance nobody declared one for. - xlings.subos.caps: backends located and probed, with the probe's raw output kept as evidence; the SubOS core finds them through HomeView and asks Ports whether a host binary is another home's shim. - xlings.subos.spec: SandboxSpec is pure data -- mounts in order, namespaces, the whole environment, the command -- and compile() is the only place a security default lives. A requirement the host cannot meet is a structured refusal (dimension, reason, fix, must/should). - xlings.subos.provider: bwrap and proot argv and the process environment, translated from the spec and deciding nothing. - xlings.subos.gates: the eight platform interfaces' probes (FsGate, ProcessScope, NetGate, DeviceGate, IdentityShim, ExecTracer, SessionHost, RootfsRuntime): supported, kernel or advisory, and the route where not. The platform matrix becomes a measurement. src/core/subos/sandbox.cpp keeps what belongs to xlings (directory setup, image mounts, the index check, backend auto-install) and builds the sandbox from the compiled spec. The backend is now exec'd with an explicit environment. No behaviour change: goldens pin the Legacy bwrap and proot argv to what the old builders produced, byte for byte.
Five workflows queued every push's full matrix behind the last one's; a PR with several checkpoint commits waited on runs nobody would read. main and release branches keep every run.
… terminal, probe text (C11) Applied by the compiler to every bwrap sandbox, declared or not (the S0 fixes are not opt-in): - F3: the environment is an allow-list. A base set (TERM, LANG, LC_*, TZ, XLINGS_AGENT_MODE, ...) plus, for dev and undeclared instances, proxies, editor, pager and CA paths. Tokens, SSH_AUTH_SOCK, the D-Bus address and XAUTHORITY no longer enter; a policy can name more. - F4: pid, ipc and uts namespaces; --die-with-parent. - F8: a non-interactive command runs without a controlling terminal (--new-session); an interactive shell keeps job control and carries a seccomp filter refusing ioctl(TIOCSTI/TIOCLINUX) with EPERM, compared on the low 32 bits so a high-bit spelling does not pass. The filter is hand-written classic BPF (x86_64 incl. x32 and i386, aarch64). - F12: a failed bwrap probe quotes bwrap, names the restriction in force from the kernel's own sysctl, and lists remedies least-privilege first (self doctor --isolation --fix, then proot). It no longer advises turning the restriction off for the whole machine. proot cannot do any of this; the spec records it as degraded. Verified from inside real sandboxes (proves = isolation): no token in env, under ten pids in /proc, no controlling terminal for a redirected command, TIOCSTI -> EPERM on a terminal. The filter is also checked in a plain child process, including the high-bit bypass.
…oins with fd passing (C9) Entering a sandbox no longer replaces xlings with bwrap (F15). The xlings process that entered stays outside as the supervisor: - it forks the backend with session-init (this binary, read-only at /run/xlings/xlings, plus its loader and RUNPATH for a dynamic build) as the sandbox's first process, and waits; - it owns <home>/run/subos/<n>/exec.sock (owner-only). A later command for the same instance JOINS the session: it passes stdin/stdout/stderr with SCM_RIGHTS, the supervisor records the request and hands request and descriptors to session-init over a private SOCK_SEQPACKET pair, and the process inside uses the caller's terminal or pipes directly -- nothing relays the bytes. The exit status comes back the same way. Clients never talk to the sandbox, so it cannot answer for the supervisor; - a join with different isolation is refused (spec digest); - like system(), it ignores SIGINT while waiting and forwards SIGTERM/SIGHUP; a client's Ctrl-C is forwarded to its command; - it writes the audit to <home>/logs/subos/<n>/events.ndjson: lifecycle (session-start with the compiled spec, session-end with exit and duration, stop requests) and ops (each joined exec: program, argc, cwd, environment NAMES, exit, duration). Exit codes follow design §12.2: the command's own, 126/127 for cannot run / not found, 128+n for a signal, 125 for setup failures. New: subos ps [--json], subos log <n> [--kind] [--session] [-n] [-f] [--json]; subos stop ends the session (and clears keeper files left by older clients). macOS and Windows report SessionHost as not implemented yet. Tests: the supervisor's lifecycle records and exit passthrough, a second command joining (shared /tmp, exec audit), ps and stop, log filtering -- all against real sandboxes.
testkit writes XTEST metadata from a static destructor; on libc++ the function-local mutex it locked was destroyed first and every test binary aborted at exit with 'mutex lock failed: Invalid argument' (macOS CI). The registry, testkit's output mutex and the journal mutex are now allocated once and never freed.
…(C12) The way to run commands in an instance from outside it (design §12), for scripts, CI and agents: - subos exec <name> [--sandbox] [--cwd] [--env K=V] [--timeout T] [--json] -- <argv...>: argv, nothing to shell-escape. It joins the instance's running session; otherwise it starts one for the command (--sandbox) or runs it with the instance's environment. Exit codes: the command's own, 125 before it starts, 126/127, 124 on timeout, 128+n for a signal. --json prints the result on stderr. - exec --temp [--from SRC]: a throwaway instance, removed through the one deletion entry point with --temp as the confirmation; its audit stays in logs/. - subos start <name> [--ttl T]: a session without a terminal that later execs join (a hot environment, no startup per command). use --keep / --ttl now mean the same thing: the session outlives the shell. - subos cp <src> <name>:<dst> (and back): only the instance's own trees (its home and /tmp). - interface: subos_exec (output as subos_exec_output events, the exit code as the result), subos_start, subos_stop, subos_events. Also: platform::run_argv on every platform; '--' ends option parsing in the CLI spec; a sandbox keeps the variables its instance declares (#352's GL paths) through the environment allow-list; POSIX specs use POSIX paths when compiled on Windows; two shell e2e tests read manifest.cppm from its new place.
…othing waits (C13, T4) The audience is declared, never inferred (design §13): --agent for one command, XLINGS_AGENT_MODE=1 for a process tree, =0 for a human. The CLI exports it, and the sandbox's environment allow-list carries it, so the xlings an agent runs inside a SubOS keeps the same contract. Agent mode already refused to prompt (no confirmation and no selection without an explicit answer). What remained was the one entry that waits by design: subos use <name> without --cmd opens an interactive shell. In agent mode it now refuses with exit 2 and names subos exec / subos start instead -- decided at the interaction surface, not in the core. T4: test_agent_contract walks the CLI's own spec and runs every local command on a pseudo-terminal with nobody typing, in agent mode; each must finish. Plus: a missing confirmation exits 2 and deletes nothing; the environment declares the audience as --agent does; an unknown name is an error that lists the candidates; the declaration reaches a sandbox.
…subos config/status (C14, C20) What an instance may do is declared, compiled and reported (design §7, §10, §14, §19): - presets: dev (host files and other instances invisible, network as usual, missing items reported), private (+ a private network through pasta, neutral identity, fetch=ask; its isolation is required), locked (+ no network, fetch=deny, full observation, nested user namespaces refused, mounts read-only by default); - <home>/config/subos/<n>/policy.json, outside the instance. Parsing fails closed: a field or value this version cannot enforce refuses entry instead of being ignored. Writers keep x-* and comment keys; - a call names a stricter preset (--sandbox=private|locked) or tighten-only overrides (--net, --fetch, --allow within grants_allowed, --no-degrade); loosening is refused with the owner's command. A declared instance is entered under its policy however it is entered, with or without --sandbox; - policy::decide() is the one answer for fetch (ordered rules: glob, index, size), index updates, grants and policy changes; the policy changes only from outside the sandbox (E_PERMISSION, exit 13); - the compiler honours net=none (a namespace with only lo, no ARP neighbours), a neutral identity (user, the instance as host name, UTC, C.UTF-8, etc-neutral/ passwd, no host zone file), disable_userns, and must/should: a missing must-item refuses (125) in one shared format, a missing should-item enters and says what is not in effect; - subos config <n> [--sandbox=P --net --fetch --index-update --observe --allow --disallow --grants-allowed --env-pass --no-degrade --reset] writes the file and audits the diff; subos status <n> [--json] shows requested vs effective and the eight platform interfaces' probes. net=nat needs pasta; it is reported as missing until the network checkpoint. Verified in real sandboxes: a locked instance entered without --sandbox has user=user host=box tz=UTC, only lo, no ARP entries, and cannot create a user namespace.
…t sockets out of reach (C23) private's network (design §19): a namespace of its own with egress through pasta (passt), the host's interfaces, ARP neighbours and abstract unix sockets out of sight. pasta cannot join a namespace bwrap made (bwrap is not dumpable and owns its netns from an inner user namespace). So for net=nat the supervisor's child first unshares a user namespace mapping this user to itself and a network namespace, says it is ready, pasta attaches to it, and only then does the child exec bwrap, which runs in that network instead of making one. pasta's pid goes to run/subos/<n>/pasta.pid and is ended with the session; a pasta that fails is a setup failure (125). Safe defaults: nothing published (-t none -u none), no path from the sandbox to the host's own loopback (-T none -U none --no-map-gw). --publish HOST:SANDBOX publishes a TCP port; the host-loopback grant opens the host's local services. --publish without nat is reported. pasta is found as a payload, then at /usr/bin or /usr/local/bin, and is usable only with /dev/net/tun; missing, private refuses with the reason and the fix (install passt, or --net none). The Linux CI lane installs passt and declares the capability. F2 verified in real sandboxes: a host process listening on an abstract socket (what an X server does) is reachable from dev, which shares the host network by design, and not from locked.
…it (C10, C15, C7) #640 F1 and F6, the S0 escape: the whole xlings home was bound read-write into every sandbox -- the shims, the shell profile, the payloads and the home config, all code the host runs, plus every other instance's files. Now (design §16): --ro-bind , --tmpfs /subos, --bind /subos/<self>, --tmpfs /logs, /run, /state config/ stays readable, so the policy is readable inside and not writable. proot cannot bind read-only and reports it as degraded fs. The inventory (C7) of what the xlings inside then needs: reads work as they were (--version, list, info, use <pkg>, subos list); what writes the home goes to the broker (design §8, §9): - inside, install / update / remove / use <pkg> <ver> are sent as argv with the caller's stdio to run/subos/<n>/broker.sock, bound at /run/xlings/broker.sock; - the supervisor decides with the same policy::decide (the strictest target wins) and runs an allowed command on the host for this instance, writing to the caller's terminal directly; ask queues it (exit 75 with the request id); deny is E_PERMISSION (13) with the owner's command; - the owner's things -- self, subos changes, config, other instances (--subos other) -- are refused inside before anything is sent; - every decision, approval and result is a perm event in the audit. New: install --subos <name> (remove had it); subos requests / approve / deny for fetch=ask. A state lock that cannot be opened for writing now fails at once instead of waiting 600 s for a holder that does not exist. /run/xlings (the session's client) is the last PATH entry inside, so an instance always has an xlings. Verified in real sandboxes: bin/, .xlings.json, data/ and the policy file are read-only inside, the instance's own tree is writable, another instance and the audit are invisible; brokered remove returns the host command's code; deny/ask/requests/deny-request flow; with the network, install from inside (fetch=auto) and install --subos from outside.
clang deduces an unnamed stream's format string as an argument (the file's own header says so); a const char* loop variable fed to std::format instantiated the wchar_t formatter; kill/setpgid needed <signal.h> on macOS.
Nothing a sandbox withholds by default is a dead end; each is one explicit grant away (design §11, §21.2). - --mount <host>[:<inside>][:ro|rw] on use / exec / start, and subos config --mount / --unmount to keep it in the policy. docker -v syntax: a second segment that is exactly ro or rw is the mode (~/.gitconfig:ro). Default rw, read-only under locked (which refuses an explicit rw). Refused: a missing host path, the xlings home or a directory above it, the system's trees and the sandbox's own paths. - --allow display | audio | camera | ssh-agent | dbus | gpu | host-loopback, within the policy's grants_allowed. Each opens one thing: X11's socket directory and a read-only copy of the Xauthority, or the Wayland socket; the Pulse / PipeWire socket; the /dev/video* nodes; the agent socket; the session bus socket -- each bound to a fixed path with its variable pointed at it, never the host's whole runtime directory. A grant this host cannot satisfy (no display, an abstract-only bus) is reported, not silent. Verified in a real sandbox: a rw mount writes through to the host, a ro mount refuses writes, the ssh-agent grant brings in exactly the socket, and mapping the xlings home is refused (125).
… from the last line The static musl builds (release, aarch64 cross) do not get offsetof through <sys/un.h>. With the release binary, xlings' own notice precedes the command's output, so the isolation test takes the count from the last line. Every sandbox test passes against the static release binary.
…, then payload bwrap (C21) #640's comment: on Ubuntu 24.04 the xim bwrap fails its probe because AppArmor denies unconfined programs user namespaces, the recipe's setuid chmod silently does nothing without sudo, and the old hint told people to turn the restriction off for the whole machine. - Lookup order (design §20, F10): /usr/lib/xlings/bwrap when it is root-owned and not writable by others; then the system's bwrap when its probe passes (used, and reported as the host's); then the xim payload. xlings neither creates nor relies on setuid. - self doctor --isolation [--json]: the user-namespace sysctls, every bwrap found with its probe result, the chosen backend, pasta, and the eight platform interfaces. - --fix, only for the case it repairs (AppArmor restricting user namespaces): installs a root-owned copy of a bwrap at /usr/lib/xlings/bwrap and /etc/apparmor.d/xlings-bwrap, a profile that grants that one binary user namespaces and nothing else, and loads it. It prints every command it will run as root, asks (agent mode: -y or exit 2), and leaves the kernel setting alone. CI: a job on a stock ubuntu-24.04 runner (restriction left on) installs the payload bwrap, sees the doctor name the restriction without advising sysctl, runs --fix -y, and enters a sandbox through the root-owned bwrap. The bwrap recipe's setuid step is a separate xim-pkgindex change.
…t every link (C45) Fixes H2 and M2 from the #641 review (SubOS design part 3 §7.2). H2: commit and switch_to were atomic for readers but not durable -- after a power loss the pointer could name a generation whose records never reached the disk. Now the records and every directory the commit created are flushed (deepest first) before the generation is placed, root.gen after it, and the SubOS directory after the pointer moves. XLINGS_TRACE=durability prints the order; ROOT-GEN-DURABLE asserts it. M2: switch_to walked the whole generation and read every link (9.6 ms of a 10 ms budget at 300 payloads). A placed generation now has an inventory beside it (root.gen/.<k>.inventory): the change stamp -- inode and ctime, which nobody can set back -- of each of its directories and record files, and the payloads it links into. Equal stamps mean "as placed"; anything else falls back to the full walk, which remains the only proof that authorizes pruning. Measured locally: 0.9 ms at 300 payloads, 3.0 ms at 3000. A switch also refuses a generation whose payload is gone (the last line of defence behind C44), naming it. platform::change_stamp is the new primitive; Flush::Deferred lets the perf lane time the check apart from the device's flush latency (durable switch reported beside it) -- nothing in the product passes it.
… rights (C46) SubOS design part 3 §6.4. Before this, where a program came from was decided in four places (caps' three locators and root_cmd's PATH walk), several ran as shell lines (`sudo mount ...`, `cp -a '...'`, `truncate`, `mountpoint`), and an export ran the host's tar inside a user namespace only so the entries would read as root's. - xlings.subos.tools: the table -- root-owned / payload / runtimedir / system paths per tool, never another home's shim. caps' bwrap/proot/pasta, image mkfs, the fork's cp and the export's mkfs.ext4 resolve through it; `self doctor --isolation --json` reports each tool and its source (and the package that brings a missing one). - In-process: xim::write_tar_gz (libarchive) writes export and pack tarballs, root-owned for an image without a user namespace; image sizing is std::filesystem::resize_file; "is it mounted" reads mountinfo. - platform::run_elevated / is_elevated (sudo, or UAC "runas" on Windows) and xlings.subos.elevation, which records every elevated argv in logs/elevation.ndjson. Image mount/chown/umount and the isolation fix go through it; platform::priv_prefix (a "sudo " shell prefix) is gone. - Headers: the extract interface unit no longer includes libarchive. - tools/lint_tool_resolve.sh (TOOL-RESOLVE): no std::system, no argv that starts with a literal tool name, administrator argv only via elevation::run.
…(INTENT-EQ baseline) 872 cases (every host shape, backend preference, preset, network mode, storage, grant set, root or not) through spec::compile and the providers, recorded by the compiler as it is BEFORE C47 replaces its insides: the description, the backend argv, pasta's arguments and the process environment, hashed per case. C47 must reproduce every one byte for byte.
…nger dispatches backends (C47) SubOS design part 3 §6.1-6.2. spec::compile was one 700-line function that mixed what a policy asks for with how each backend does it, and src/core/subos/sandbox.cpp chose the argv builder by backend. - xlings.subos.intent: the policy and the call lowered into backend-free decisions -- identity, the network wished for, host paths mapped in (an ordered list in the shape of an openkal preopen, each with its refusal), named grants, the environment allow-list, storage and root. - modules/confine (new package): one Implementation per backend -- linux-bwrap, linux-proot, linux-landlock, home-redirect, fake -- each stating whether it runs here, its view, its process isolation, how a socket grant and a --mount reach it, its environment and command, and its rows of the platform matrix. The selector, the compile skeleton (the policy's Must/Should semantics, kept in one place), launch_argv and the matrix (strongest claim per interface, the route when none) live here. - xlings.subos.spec keeps only the types; provider and gates moved to xlings.confine.provider / xlings.confine.gates. - INTENT-EQ: all 872 recorded cases (previous commit) compile byte for byte to the same description, argv, pasta arguments and environment. GATE-CONFORM: the matrix is the implementations' claims.
macOS's bsdtar prints 'nlink uid gid' where GNU tar prints 'uid/gid'; the tarball itself was root-owned on both (C46).
…ugh one launcher and the NDJSON interface (C48) SubOS design part 3 §5.1, §5.2, §5.6. - modules/carrier (new package): a Carrier probes this machine, ensures an endpoint (a launcher prefix and the xlings there), stops it, and grants a host directory into it. No new protocol: `carrier::control` is one `xlings interface <capability>` call there, `carrier::terminal` is `xlings <args>` there with this terminal attached. `local` is this machine's kernel. - carrier::choose (§5.6): Linux has one kernel; on Windows / macOS a SubOS of the host's own programs stays local and a Linux one, a root, or one whose policy needs a boundary this kernel cannot give goes to the platform's guest carrier (wsl2 / vz) -- or is refused with the route that brings it. A carrier asked for by name is honoured or refused, never replaced. - `subos new --carrier --abi`: recorded additively in instance.json (`carrier`, `abi`; absent = local/native, which older clients assume). A SubOS on another carrier: `new` and every name-taking subcommand run there; `remove` then drops the name here. - `subos list` and the interface's `list_subos` are one emitter now (the capability had its own copy that lacked `kind`); entries carry carrier, abi and view (overlay / root / machine). - CARRIER-LOCAL, CARRIER-UNAVAILABLE.
…and stats payloads once The Linux-root lane (a dev build) measured the 3000-payload check at 11.1 ms of 10. string_view + from_chars for the inventory, one lstat per payload root: 3.0 ms unoptimized, 1.9 ms optimized, 0.2 ms at 300. The static lane counts three generation cases now (ROOT-SWITCH-SCALE).
…the user entering WSL (C49)
SubOS design part 3 §5.3. On Windows, `xlings subos new dev --abi linux`
(or --rootfs, or --carrier wsl2) makes the SubOS in a WSL2 distribution of
this home's own and every later `xlings subos ...` for it runs there.
- One distribution per home ("xlings-" + a hash of the home's path, under
<home>\carriers\wsl2), imported once from an image this xlings writes
in-process: the Linux build of this release at /xlings/bin/xlings (static:
it runs before anything else is there; self-contained, so /xlings is its
home) and the machine files the carrier declares -- /etc/wsl.conf with no
automount and no interop, root's passwd, mount.drvfs -> /init. Written from
a description (xim::write_tar_gz_entries), so Windows needs no symlinks.
- The Linux build comes from the index's xim:xlings linux artifact at this
version (the latest published one for a build not in the index yet), or
XLINGS_CARRIER_GUEST_XLINGS.
- `wsl.exe -d <name> -u root --exec /xlings/bin/xlings __carrier-env K=V --
<args>`: the guest has no env(1), so its xlings carries the variables.
`__carrier-grant` mounts one granted Windows directory under /grant/.
- A carrier SubOS is registered here by name (home config `subos`, and
instance.json `carrier`), so list/use/remove find it; its content is there.
- `self doctor --isolation --json` reports the carriers this platform has.
- Tests: a stand-in wsl.exe (XLINGS_WSL_EXE) runs the whole lifecycle on
Linux CI -- real image, real guest xlings -- and found the env(1)
assumption. tests/e2e/windows_carrier_wsl2_test.ps1 runs on the Windows
lane: WSL2's lifecycle and interop-off where it exists, the refusal with
its route (exit 125, native SubOS unaffected) where it does not.
VIEW-CARRIER-EQ is deferred: it needs a real guest kernel.
…loads exist here The static lane's RootExport case: an exported image's generation links name payloads at the image home's (logical) path, which exists where the image is used, not at export time -- and C45's 'a payload this generation links into is gone' refused the commit's own switch. Choosing an existing generation (rollback) still proves its payloads; commit and the restore of a prior generation after a failure prove the tree (rootfs::Verify).
…Windows in a Job Object (C50) SubOS design part 3 §6.3. Until now a sandboxed native SubOS on macOS and Windows bypassed the supervisor entirely: environment variables set, a shell run -- no join, no --timeout, no exit-code table, no audit. - Transport: macOS has no SOCK_SEQPACKET for AF_UNIX; xlings.platform's message sockets are SOCK_STREAM there, each message framed by a 4-byte length with its descriptors on the frame's header. A socket path longer than macOS's sun_path goes through a per-user /tmp link to its directory (Linux keeps /proc/self/fd). Linux is unchanged. - Session host: kSessions is Linux and macOS; the home redirect launches as this binary's __session-init (as Landlock does), its default command the user's shell. The macOS path is the Linux one -- join, start/stop, --timeout (124), signals (128+n), 125/126/127, the audit. - Windows (no fork, no descriptor-passing sockets yet): run_argv_with_timeout is the process scope -- a Job Object with kill-on-close, the whole tree ended at the deadline (124), the command's own exit code. Joining a running session there needs a CreateProcess supervisor: stated in SESSION-NATIVE-SUPERVISED, not claimed. - tests/e2e/test_subos_native_session.cpp runs on macOS and Windows; the Linux report counts that requirement as gated by those workflows.
…act (C51) SubOS design part 3 §5.4: the wsl2 carrier's shape on a Mac -- one Linux VM per home, a Luban machine with /xlings as its home, the same NDJSON interface and __carrier-env launcher. Virtualization.framework needs a binary signed with its entitlement, which xlings is not; the carrier drives a helper, `xlings-vm` (a payload in the tool table), through one contract: probe, status (0 running, 3 stopped, 4 absent), create from the carrier image, start, stop, share (virtiofs; the path inside), exec (vsock). The VM is created on first need, started when stopped, reused while running. Verified against a stand-in helper (tests/fixtures/carrier/fake-vz-helper.sh) on Linux CI: created once, started twice across a stop, a grant answered by the helper, remove there. The helper itself is a separate signed package -- CARRIER-VZ-HELPER is deferred with that reason; until it is published the carrier is refused with `xlings install xlings-vm` as its route.
…-init (C52) SubOS design part 3 §8. - luban/ (package luban, modules luban.*): luban.boot (boot.json: default, fallback, a trial), luban.stage0 (a machine's first process), luban.machine (the machine's /etc: factory files, sysusers -- taken out of xlings.subos.rootfs). It depends on modules/ and never on the frontend; the layer lint enforces both directions. - apps/luban-init: stage-0 as its own package and binary. A package's binaries link every source of that package, so as a target of the root package it was the whole client (6812 frontend symbols); as its own it links luban and modules/ only (0). The Linux release builds and ships bin/luban-init (static, checked); an exported root and the projection carry boot/luban-init and /usr/bin/luban-init beside xlings-init, which machines already booting init=<home>/boot/xlings-init keep using. - Linking modules/ without the frontend found a latent defect: manifest's DEFAULT_RUNTIME_FALLBACK was `inline constexpr` in a module interface, which GCC emits only where it is used -- every binary until now got it from the frontend. - LUBAN-INIT: tests/e2e/luban_init_test.sh against the release tarball (shipped, static, a fraction of the client's size, refuses outside PID 1).
…isolation design map the new layout C53 (SubOS design part 3 §4.3): both questions answered with evidence. - openkal-linux 0.16.1 links and runs beside libstdc++ under glibc and the static musl release target (tests/openkal; the Linux CI builds and runs both on every PR). kal::write bypasses stdio: flush before mixing. - Passing a handle between processes is outside openkal 0.15 (SPEC §11/9): Transport stays in modules/platform. - Nothing of xlings links openkal in this PR: no portable piece behaves better as kal_*, and the first adoption that does -- an openkal program started with exactly its grants -- belongs with the Luban kernel. Part 3 §19 records C43-C53 with what verified each. AGENTS.md: the layout (store, confine, carrier, luban/, apps/luban-init) and the rules that came with part 3 (one tool table, one elevation door, layers import downward, a retained generation is a GC root, the INTENT-EQ golden).
macOS's home redirect starts its session-init as this binary (C50), and host_exe_ read /proc/self/exe -- Linux's alone -- falling back to a bare 'xlings' that exec could not find (125: 'cannot run xlings').
…dependent of SubOS scopes) The client was pinned to #47's fix commit on its PR branch; that branch is merged and gone, so the pin moves to the squash commit on libxpkg's main. Same code (the version bump to 0.0.62 is the only addition).
mcpp test compiles every tests/**/*.cpp as a test of the root package, which does not depend on openkal; the macOS lane failed compiling the probe.
2026.10.8.2 was a candidate and never published; its policy-schema floor (kPolicyMinClient) and the interface 1.6 note move to the release that carries them.
A route that names a package the index does not have is a dead end that looks like a step.
…t taken as local Self-review against AGENTS.md's "could not read is not empty": a WSL2 SubOS with a damaged instance.json was treated as local, so `subos remove` would drop its name here while its content lived on in the carrier.
…session host The macOS lane ran C50's exit-code table under the supervisor (3, 127, 124, 128+15 all correct); `subos start` was still refused by a "needs Linux" check that predates macOS sessions.
The check stats each payload root once so a rollback cannot land on a deleted payload; that grows with payloads. A loaded root-lane runner measured 11.9 ms (1.9 ms optimized, 3.0 ms unoptimized locally). The 300-payload budget (PERF-GEN-SWITCH, 10 ms) is unchanged -- 0.2 ms now. The 3000 case is a budget this PR introduced: ten times the payloads within twice the budget. .agents/docs/2026-10-09-pr-641-self-review.md: part 1/2/3 together, by angle; the fourteen defects this round found and what fixed each; what is not done and why (the signed VZ helper, Windows join, openkal in product).
…mance lane Lanes that run the suite in parallel measure their own contention: the same binary read 11.9 ms and 25.3 ms on two runs of the Linux-root lane. The static performance step runs each case alone and now sets XLINGS_PERF_BUDGETS=1; elsewhere the case validates its workload, prints its timing and skips the assertion, saying why.
tests/scripts/test_release_candidate_gate.py: a PowerShell test that runs a command it expects to fail must not leak that $LASTEXITCODE into the step.
…all.sh run_all.sh's orphan guard: a test nobody runs reports what a passing one does. It passed in its own line of the suite (the release tarball).
The Linux report found SESSION-NATIVE-SUPERVISED undeclared: every case in its file needed macOS or Windows, so xdev never ran the binary on Linux. A platform-independent case now checks what the macOS supervisor stands on -- a home redirect starts as this binary's __session-init with its command after it. The platform behaviour stays gated by the macOS and Windows workflows.
Sunrisepeak
marked this pull request as ready for review
October 9, 2026 03:15
Sunrisepeak
added a commit
that referenced
this pull request
Oct 9, 2026
…used install (2026.10.9.2) (#649) * fix(xvm): an asset the payload does not ship is not placed, not a refused install (2026.10.9.2) The fresh-install gcc suite on the published 2026.10.9.1: `xlings install gcc@15.1.0` failed on Linux and CentOS 7 with "sysroot update refused before metadata changed: .../gcc/15.1.0/lib64/libasan.so: asset source is missing or unreadable". gcc's recipe declares its library assets across versions; the 15.1.0 build ships no libasan.so. Before #641 such an asset was skipped ("asset source missing, not placed"); #641's materializer refused the whole update over it. An absent source (not found, a dangling link included) is skipped again with the same debug line; a source that exists and cannot be read still refuses. Regression: XvmMaterialize.AnAssetThePayloadDoesNotShipIsNotPlacedAndDoesNot RefuseTheRest. * fix(xdev): a lane that does not name its execution is reported, never sampled The hand-written lane.json of the linux-xdev lane carried no "run", so the report used the lane's directory as the execution -- the same string in every CI run. The first PR after main saved trend history read main's sample of LoopbackHttp.DownloadsExactBytesAndAttributesADestinationFailure as the same execution with another duration and refused the report. Every hand-written lane now names its execution (run id and attempt), and a lane that still does not is kept out of the trend history. macos-xdev also declared platform "macosx", which the history does not accept.
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.
Implement the SubOS architecture in Part 1 and Part 2: shared policy/provider/session/broker/audit contracts, owned prefix domains, checked source scopes, exact root views, atomic generations and rollback, root export/boot, and a unified xdev acceptance runner. Preserve user data and fail closed when ownership, state, or isolation cannot be established.
Plans and evidence:
Current status: Draft; not ready to merge. Head
24cb3ae9b2fa4cd8deccb78cb3b86fe1ec783c31adds only F12 test/CI evidence wiring and its progress record; C++ product source is unchanged fromb3dd5009. All five workflow groups are rerunning on this head.The preceding complete product head
b3dd5009passed Linux main, ASAN, macOS, Windows, ARM64, Linux root, legacy E2E, Arch, isolation and all three distro jobs. Linux main: 107 test programs pass; XTEST 216 pass/0 fail/5 skip. All three additional static domain/source/export cases and all six static performance cases actually pass, with no skips and every hard-gate outcome success. The originally reported DomainProducer case passes in 16416ms, including unconfirmed removal exit 2 and host data preservation. ASAN: all 109 programs pass; 1905.30s total. macOS XTEST154 pass/0 fail/2 skip; Windows111 pass/0 fail/45 skip.The full rootfs fixture passes all nine stages, including core/cache, 144 executable closures, no-shell shim, fetch=layer and desktop rendering. The full image fixture passes all seven stages, including multi layout, actual HTTP self-update from published .1 to candidate .2, generation 2 to 3, rollback to .1/generation2, and unchanged static stage0 bytes/inode. Root UID0 controller trials retained capabilities and were rejected; the original UID1000 boundary is retained.
The requirement report verified 143 of 146 behaviors. Windows quoting is covered by its mandatory Windows gate; WSL1 is the explicit best-effort exception. The only remaining hard blocker was F12: the dedicated repair script really ran and passed, but lacked coverage metadata. The appended fix adds F12/DOC-ISOLATION coverage, requires metadata in that CI suite, refuses an unsupported prerequisite instead of reporting exit-zero success, and always checks the raw probe error. Replaying real execution evidence proves the missing linkage before the fix and a passing F12 linkage after it; the new head must still pass CI.
Dependency: libxpkg #47 removes the installing SubOS path from shared payload RPATH. Its regression fails before the fix and passes afterward. The 0.0.62 version candidate also passes upstream CI. The client pins immutable commit
7c202104625a072b5e6553603cc18859c3e29b47for reproducible candidate validation. The upstream PR/source-tag release/mirror/index update remain pending review; nothing has been merged or released.Keep all 142 PR commits: append commits and ordinary pushes only. Once the technical acceptance criteria are met, report to the maintainer for review and a merge decision. Do not merge or release automatically.
Refs: #640
Part 3 (2026-10-09): content × view × carrier — C43–C53
Design:
.agents/docs/2026-10-09-subos-architecture-design-part3.md(§16 plan, §19 record). Self-review:.agents/docs/2026-10-09-pr-641-self-review.md.#if→if constexpr; layer / branch lints, everytools/lint_*.shin CImodules/store: a retained generation is a GC root; keep 5 + release (fixes H1, M1)std::systemgonemodules/confineregistry; core no longer dispatches backends; matrix from the implementationsmodules/carrier:subos new --carrier --abi, chosen by what the SubOS is, forwarded through one launcher + the NDJSON interfaceluban/(boot, stage0, machine) besidemodules/;luban-initits own static binary (0 frontend symbols, 4.7 MB)Also: libxpkg #47 reviewed and merged (
912720f), client pinned to it. Release version 2026.10.9.1.Not done, said so: the signed
xlings-vmVZ helper; joining a running session on Windows; openkal in product code (first meaningful use is with the Luban kernel).