mirror of
https://github.com/nestriness/nestri.git
synced 2026-09-19 17:25:19 +03:00
feat(nesinit): mount what the descriptor names, and relay the layer it cannot read
Builds on the previous change, which had PID 1, the channel and the trait but mounted nothing. The shares are mounted now: a tag names an export, the descriptor names where it lands, and every share goes on nosuid and nodev whether or not it is writable — a share is data handed to the guest, and no descriptor has a way to ask for a setuid binary or a device node in one. Mounting needs privileges a test does not have, so the arguments and flags are derived by a function the tests can assert, which is where the read-only decision lives. Progress is reported in two messages rather than one. A share that did not mount and a command that did not run are not the same incident, and each carries the reason the operating system gave and the path it happened on: a permission error on a named directory can be acted on, where "the share did not mount" cannot. The second layer is relayed and never read. Bytes arrive on the channel in an envelope, cross a unix socket to the workload, and come back the same way. The body is a string rather than nested JSON on purpose: a document this component can index into is a document it can grow to depend on, and then the layer is no longer opaque and the boundary it exists to draw is gone. An envelope is never logged — not the body, not truncated, not at debug level — and the channel name with a byte count is the whole of what may be said about one. The type's Debug is written by hand for the same reason, because a derived one puts the body one careless format string away from a log line. A write to a channel nobody is reading now ends the session the same way a closed read does. A caller that has stopped listening has also stopped being able to say stop, which is one situation and was two outcomes. The guest listens on the relay socket and the workload dials in, which is the convention the other guest sockets already use and removes the startup ordering problem: a workload that is not running yet has simply not connected yet.
This commit is contained in:
@@ -25,6 +25,8 @@ The guest dials out on a fixed vsock port and speaks first:
|
||||
```
|
||||
guest → { "type": "ready", "protocol_version": 2 }
|
||||
guest ← { "type": "boot", "exec": {...}, "mounts": [...], "geometry": {...}, "on_exit": {...} }
|
||||
guest → { "type": "mounted" }
|
||||
guest → { "type": "started" }
|
||||
guest → { "type": "workload_exited", "exit_code": 0 }
|
||||
```
|
||||
|
||||
@@ -42,6 +44,56 @@ The types are in [`nesprotocol::lifecycle`](../../crates/nesprotocol/src/lifecyc
|
||||
behind the `lifecycle` feature, so both ends of the channel read one definition
|
||||
and neither can drift from it silently.
|
||||
|
||||
`mounted` / `mount_failed` stay separate from `started` / `start_failed`
|
||||
because the two want different things looked at: a share that did not appear
|
||||
and a command that did not run are not the same incident. A failure carries the
|
||||
reason in the words the operating system used, and the path it happened on — a
|
||||
permission error on a named directory can be acted on, where "the share did not
|
||||
mount" cannot.
|
||||
|
||||
### Two layers, one channel
|
||||
|
||||
The channel carries a lifecycle layer, above, and a payload layer that nesinit
|
||||
relays and never reads:
|
||||
|
||||
```
|
||||
{ "type": "payload", "channel": "<name>", "body": "<opaque string>" }
|
||||
```
|
||||
|
||||
Both directions. Inside the guest an envelope crosses a unix socket at
|
||||
`/nestri/payload.sock`, which the guest listens on and the workload dials into.
|
||||
That socket is a mechanism and expected to change; the envelope is the boundary
|
||||
and is not.
|
||||
|
||||
`body` is a string rather than nested JSON, deliberately. A document nesinit
|
||||
can index into is a document nesinit can grow to depend on, and then the layer
|
||||
is no longer opaque and the boundary it exists to draw is gone.
|
||||
|
||||
**An envelope is never logged.** Not the body, not truncated, not at debug
|
||||
level. The channel name and the byte count are the whole of what may be said
|
||||
about one — what crosses here includes credentials meant for the workload and
|
||||
nothing else. `Payload`'s `Debug` is written by hand for the same reason: a
|
||||
derived one puts the body one careless `{:?}` away from a log line.
|
||||
|
||||
### The shares
|
||||
|
||||
Each `mounts` entry is a tag, a path to put it at, and whether it is read-only.
|
||||
The tag names an export and is never a path on the other side of the channel,
|
||||
so the guest learns nothing about the filesystem it is handed a piece of.
|
||||
Choosing *where* a share lands is the descriptor's job, not the guest's:
|
||||
deciding that means knowing what the workload expects to find there, which is
|
||||
exactly the knowledge a workload-independent init does not have.
|
||||
|
||||
Every share is mounted `nosuid` and `nodev`, whether or not it is writable. A
|
||||
share is data handed to the guest, and no descriptor has a way to ask for a
|
||||
setuid binary or a device node in one.
|
||||
|
||||
`uid` and `gid` in `exec` are load-bearing rather than hygiene. Whoever writes
|
||||
the descriptor also exported the writable share, so the two have to agree; when
|
||||
they do not, the first write is refused and the failure surfaces here as a
|
||||
permission error with a path, instead of as a workload that misbehaves much
|
||||
later for no visible reason.
|
||||
|
||||
### It reports; it does not supervise
|
||||
|
||||
When the workload ends, the exit goes up the channel and the session is over.
|
||||
@@ -54,9 +106,9 @@ A signalled workload is reported as signalled, with no exit code. Reporting
|
||||
|
||||
### What is not here yet
|
||||
|
||||
Mounting shares. The descriptor's `mounts` are refused rather than ignored — a
|
||||
workload started without the shares it was promised fails later, somewhere
|
||||
else, for a reason nobody can see from the guest.
|
||||
`geometry` is carried and parsed but nothing consumes it: nesinit does not
|
||||
start the guest's own services yet. `ticket` exists as a message with no
|
||||
producer wired to it.
|
||||
|
||||
### Testing
|
||||
|
||||
@@ -64,8 +116,10 @@ else, for a reason nobody can see from the guest.
|
||||
cargo test -p nesinit
|
||||
```
|
||||
|
||||
No VM required, and that is the point of the two seams. Reaping is tested
|
||||
against real forked children — `PR_SET_CHILD_SUBREAPER` makes a test process
|
||||
inherit orphans the same way PID 1 does — and the channel is tested over an
|
||||
in-memory pipe, because the transport contributes nothing to the protocol
|
||||
beyond ordering and framing.
|
||||
No VM required, and that is the point of the seams. Reaping is tested against
|
||||
real forked children — `PR_SET_CHILD_SUBREAPER` makes a test process inherit
|
||||
orphans the same way PID 1 does. The channel is tested over an in-memory pipe,
|
||||
because the transport contributes nothing to the protocol beyond ordering and
|
||||
framing. The relay is tested over a real unix socket. Mounting needs
|
||||
privileges a test does not have, so what is asserted is the arguments and flags
|
||||
the mount is given, which is where the read-only and `nosuid` decisions live.
|
||||
|
||||
Reference in New Issue
Block a user