mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat: resident guest init (#333)
Get this thing going..
<!-- greptile_comment -->
<!-- greptile_summary -->
<h2><a
href="https://app.greptile.com/api/retrigger?id=63134761"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/RetriggerDark.svg?v=1"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"><img
alt="Retrigger"
src="https://greptile-static-assets.s3.amazonaws.com/badges/Retrigger.svg?v=1"
align="right"></picture></a>Confidence Score: 5/5</h2>
The PR appears safe to merge; all previous findings are resolved and the
latest readiness change introduces no established actionable regression.
<h3>Summary</h3>
- Establishes required guest filesystems, runtime directories, device
permissions, and service processes.
- Reports initialization and service deaths over the lifecycle channel.
- Supports launch, restart, and shutdown commands for a resident guest.
- Separates service and workload identities and configures per-launch
runtime environments.
- Removes the currently inactive nescope screenshot option and makes
capture-chain verification fail explicitly when compositor readback is
unavailable.
- Reworks the guest image around `nesinit` as PID 1 without a
distribution service manager.
<h3>Diagram</h3>
```mermaid
sequenceDiagram
participant Host
participant Init as nesinit
participant FS as Guest filesystems
participant Services as Service stack
participant Workload
Init->>Host: Ready(protocol version)
Host->>Init: Boot(mount descriptors)
Init->>FS: Establish and mount shares
Init->>Services: Spawn services in order
Services-->>Init: Required sockets ready
Init->>Host: Initialized(service names)
Host->>Init: Launch(id, exec, on_exit)
Init->>Workload: Spawn with isolated UID/runtime
Init->>Host: Started(id)
Workload-->>Init: Exit status
Init->>Host: WorkloadExited(id, status)
Host->>Init: Launch / Restart / Shutdown
```
<sub>Reviews (4) · Last reviewed commit: ["fix(nesinit): readiness is a
socket
that..."](731d34df9d)</sub>
<!-- /greptile_comment -->
---------
Co-authored-by: DatCaptainHorse <DatCaptainHorse@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
ec8b13d0c9
commit
8246aa5538
@@ -31,6 +31,12 @@ struct Early {
|
||||
/// so a device node or a setuid bit appearing in one did not come from us.
|
||||
const NOSUID_NODEV: libc::c_ulong = libc::MS_NOSUID | libc::MS_NODEV;
|
||||
|
||||
// Every tmpfs below is capped, and the caps are load-bearing rather than
|
||||
// tidiness. A tmpfs with no `size=` may grow to half of RAM, and the RAM in
|
||||
// question is the box's whole allowance — so an uncapped `/tmp` is a workload
|
||||
// that can OOM the box it runs in by writing files. The numbers are carried
|
||||
// over from the mount table this replaced, where they were already considered.
|
||||
|
||||
const EARLY: &[Early] = &[
|
||||
Early {
|
||||
source: "proc",
|
||||
@@ -41,6 +47,38 @@ const EARLY: &[Early] = &[
|
||||
cost: "this process cannot make itself ineligible for the OOM killer, \
|
||||
and nothing in the guest can read its own state",
|
||||
},
|
||||
// Usually already there: a kernel built with `CONFIG_DEVTMPFS_MOUNT` mounts
|
||||
// this before init runs. Listed anyway because the check below skips what
|
||||
// is already mounted, so the entry costs nothing when the kernel did it and
|
||||
// is the difference between a working box and one with no device nodes when
|
||||
// it did not. Without `nodev`, obviously — device nodes are the point.
|
||||
Early {
|
||||
source: "devtmpfs",
|
||||
target: "/dev",
|
||||
fstype: "devtmpfs",
|
||||
flags: libc::MS_NOSUID,
|
||||
data: "mode=755",
|
||||
cost: "there are no device nodes at all, so nothing can open the GPU",
|
||||
},
|
||||
Early {
|
||||
source: "devpts",
|
||||
target: "/dev/pts",
|
||||
fstype: "devpts",
|
||||
flags: NOSUID_NODEV | libc::MS_NOEXEC,
|
||||
data: "mode=620,gid=5,ptmxmode=666",
|
||||
cost: "nothing that wants a terminal can allocate one",
|
||||
},
|
||||
// The image creates this directory, and the mode is the load-bearing part:
|
||||
// a workload and the box's own services are different users, and shared
|
||||
// memory between them is how a Vulkan client hands buffers around.
|
||||
Early {
|
||||
source: "tmpfs",
|
||||
target: "/dev/shm",
|
||||
fstype: "tmpfs",
|
||||
flags: NOSUID_NODEV,
|
||||
data: "mode=1777,size=256m",
|
||||
cost: "anything using shared memory fails, which includes most graphics",
|
||||
},
|
||||
Early {
|
||||
source: "sysfs",
|
||||
target: "/sys",
|
||||
@@ -59,7 +97,7 @@ const EARLY: &[Early] = &[
|
||||
flags: NOSUID_NODEV,
|
||||
// The sticky bit, because the workload does not run as this process
|
||||
// does and what it binds here is its own.
|
||||
data: "mode=1777",
|
||||
data: "mode=1777,size=64m",
|
||||
cost: "whatever serves this session's address cannot bind its socket, \
|
||||
so the session never gets one",
|
||||
},
|
||||
@@ -74,20 +112,51 @@ const EARLY: &[Early] = &[
|
||||
// Octal, and without a leading zero on purpose: the kernel parses a
|
||||
// tmpfs mode as octal either way, and this is the spelling `mount`
|
||||
// itself documents.
|
||||
data: "mode=755",
|
||||
data: "mode=755,size=32m",
|
||||
cost: "there is nowhere for a runtime socket to live, so neither the \
|
||||
payload relay nor this session's address can be served",
|
||||
},
|
||||
// The tree a session's shares are mounted into.
|
||||
//
|
||||
// A share's target is named by the descriptor and may be any path under
|
||||
// here, so something has to create directories on a root that is read-only
|
||||
// by design. That is what this is: `workload::mount` calls `create_dir_all`
|
||||
// on each target, which fails with `EROFS` unless the tree it is creating
|
||||
// in is writable.
|
||||
//
|
||||
// **This entry used to be forbidden, and the reason it was forbidden is
|
||||
// gone.** A test here asserted that `/nestri` must never be mounted over,
|
||||
// because a fresh tmpfs would hide the install, the user state and the work
|
||||
// directory that the image had prepared underneath. That was true of the
|
||||
// image that shipped those directories and an `fstab` that mounted into
|
||||
// them. The image prepares nothing here now — the host names every share
|
||||
// and every target — so there is nothing left to hide, and the rule had
|
||||
// become a guard on a hazard that was deleted with the image that had it.
|
||||
// ref(d-0064)
|
||||
//
|
||||
// Small on purpose. Everything real is mounted *over* this, so what remains
|
||||
// is a handful of empty directories; the cap matters for the case where a
|
||||
// share fails to mount and a workload writes to the bare mount point
|
||||
// instead, which would otherwise be RAM the box cannot get back.
|
||||
Early {
|
||||
source: "tmpfs",
|
||||
target: "/nestri",
|
||||
fstype: "tmpfs",
|
||||
flags: NOSUID_NODEV,
|
||||
data: "mode=755,size=4m",
|
||||
cost: "no share can be mounted, because its target cannot be created on a read-only root",
|
||||
},
|
||||
// The relay's own directory, and it is deliberately **not** in the tree the
|
||||
// session's shares live in.
|
||||
//
|
||||
// It was, and that was wrong in a way no test here would have caught: a
|
||||
// fresh tmpfs over the share tree hides every directory the image prepared
|
||||
// underneath it — the install, the user state, the work directory, and the
|
||||
// mount point the log share is attached to from `fstab`. The box then has a
|
||||
// socket and none of the places its workload expects to find its files, and
|
||||
// the exact-path check below cannot notice, because what `fstab` mounts is
|
||||
// a directory *inside* that tree rather than the tree itself.
|
||||
// It was, and moving it out stays right for a reason that outlived the one
|
||||
// originally given. The first reason was that a tmpfs over the share tree
|
||||
// would hide what the image had prepared there; that image is gone and the
|
||||
// entry above now mounts that tree deliberately. The reason that remains is
|
||||
// ownership: this directory is written by this process and by nothing else,
|
||||
// which is what makes the socket in it unreplaceable. The share tree is
|
||||
// mounted into by the host's own shares, so a relay socket living there
|
||||
// would sit in a tree a workload's own share can be attached over.
|
||||
//
|
||||
// Owned by this process and writable by nothing else, which is what makes
|
||||
// the socket in it unreplaceable. The workload reaches it because the
|
||||
@@ -98,10 +167,22 @@ const EARLY: &[Early] = &[
|
||||
target: crate::payload::DIRECTORY,
|
||||
fstype: "tmpfs",
|
||||
flags: NOSUID_NODEV | libc::MS_NOEXEC,
|
||||
data: "mode=755",
|
||||
// One socket lives here, so this is as small as a tmpfs usefully gets.
|
||||
data: "mode=755,size=1m",
|
||||
cost: "the payload relay cannot bind, so nothing reaches the workload \
|
||||
over the channel",
|
||||
},
|
||||
// The root is read-only and some things write here whether or not anything
|
||||
// reads it back. A box's real logs leave over the control channel; this is
|
||||
// so that a library writing a file does not fail on `EROFS` instead.
|
||||
Early {
|
||||
source: "tmpfs",
|
||||
target: "/var/log",
|
||||
fstype: "tmpfs",
|
||||
flags: NOSUID_NODEV | libc::MS_NOEXEC,
|
||||
data: "mode=755,size=16m",
|
||||
cost: "anything that writes a log file fails on a read-only root",
|
||||
},
|
||||
];
|
||||
|
||||
/// Mount what the rest of this component assumes is already there.
|
||||
@@ -220,6 +301,25 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// **Every tmpfs is capped.** One without a `size=` may grow to half of RAM,
|
||||
/// and the RAM in question is the whole box's — so an uncapped `/tmp` hands
|
||||
/// a workload a way to OOM the box it is running in by writing files. The
|
||||
/// failure looks like a box that died under load rather than like a missing
|
||||
/// mount option, which is why this is a test.
|
||||
#[test]
|
||||
fn no_tmpfs_is_unbounded() {
|
||||
for early in EARLY {
|
||||
if early.fstype != "tmpfs" {
|
||||
continue;
|
||||
}
|
||||
assert!(
|
||||
early.data.contains("size="),
|
||||
"{} is an uncapped tmpfs",
|
||||
early.target
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `/proc` is mounted before `mountinfo` is read, so it has to be first.
|
||||
#[test]
|
||||
fn proc_is_the_first_entry() {
|
||||
@@ -243,20 +343,32 @@ mod tests {
|
||||
}
|
||||
|
||||
/// **Nothing here may be mounted over the tree the session's shares live
|
||||
/// in.** A fresh tmpfs there hides every directory the image prepared
|
||||
/// underneath — the install, the user state, the work directory, and the
|
||||
/// mount point the log share attaches to — and the exact-path check cannot
|
||||
/// notice, because what is mounted from `fstab` is a directory inside that
|
||||
/// tree rather than the tree itself. So a box would come up with a socket
|
||||
/// and without any of the places its workload looks for its files.
|
||||
/// in**, and the share tree itself is mounted so that targets under it can
|
||||
/// be created at all.
|
||||
///
|
||||
/// This replaces a test that asserted the exact opposite — that `/nestri`
|
||||
/// must never be mounted over — on the grounds that a tmpfs there would
|
||||
/// hide the install, the user state and the work directory the image had
|
||||
/// prepared. The image that prepared them no longer exists; the host names
|
||||
/// every share and every target now, and a read-only root cannot have a
|
||||
/// directory created on it. Measured 2026-09-11: without this entry the
|
||||
/// first real boot refused its own descriptor with
|
||||
/// `/nestri/payload: Read-only file system`.
|
||||
#[test]
|
||||
fn the_share_tree_is_never_mounted_over() {
|
||||
for early in EARLY {
|
||||
assert_ne!(
|
||||
early.target, "/nestri",
|
||||
"this hides the directories the image prepared for a session"
|
||||
);
|
||||
}
|
||||
fn the_share_tree_is_writable_and_the_relay_is_not_inside_it() {
|
||||
let tree = EARLY
|
||||
.iter()
|
||||
.find(|e| e.target == "/nestri")
|
||||
.expect("a share's target cannot be created without this");
|
||||
assert_eq!(tree.fstype, "tmpfs");
|
||||
assert!(
|
||||
tree.data.contains("size="),
|
||||
"an uncapped tmpfs here is RAM a box cannot get back"
|
||||
);
|
||||
assert!(
|
||||
!crate::payload::DIRECTORY.starts_with("/nestri/"),
|
||||
"the relay's socket would sit in a tree a share can be mounted over"
|
||||
);
|
||||
}
|
||||
|
||||
/// The relay's directory is the one this cannot hardcode: it belongs to
|
||||
|
||||
Reference in New Issue
Block a user