Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
33a3608
silkd: a client hang-up reaps an attached language server
CMGS Nov 3, 2026
790b8d9
silkd: git_branch list names an unborn branch
CMGS Nov 3, 2026
4d96d04
review: silkd docs: attach never ends in done, a dropped port_forward…
CMGS Nov 3, 2026
e1705a7
review: silkd and sandbox-init borrow what they only read
CMGS Nov 3, 2026
19ffac1
review: silkd drops code that cannot run
CMGS Nov 3, 2026
c335561
review: silkd statics and test helpers follow the file layout
CMGS Nov 3, 2026
61f10fe
review: silkd comments shrink to what the code cannot carry
CMGS Nov 3, 2026
6737982
review: silkd tests use the shared request and assert_error helpers
CMGS Nov 3, 2026
2a3d04f
sdk/python: ship the PEP 561 py.typed marker
CMGS Nov 3, 2026
9349206
review: one-line docstrings where several lines restated the signatur…
CMGS Nov 3, 2026
2f8d047
review: drop duplicate test fixtures and a subset test; share FakeCon…
CMGS Nov 3, 2026
beefef6
review: tests before helpers in test_stream and test_client
CMGS Nov 3, 2026
ed07c9c
review: adapters floor the SDK at 0.1.13, where the in-guest kill aft…
CMGS Nov 3, 2026
3f487b4
review: silkdtest's default arm calls serveStateless
CMGS Nov 3, 2026
0e786e7
review: trim multi-line comments on unexported decls, capitalise own-…
CMGS Nov 3, 2026
59d3666
review: guestserver keeps its report utility below the halfCloseSink …
CMGS Nov 3, 2026
a37e958
review: volumesmoke rejects non-local probes with filepath.IsLocal
CMGS Nov 3, 2026
329fd96
review: harness owns the guest proxy address, RelayConn and a package…
CMGS Nov 3, 2026
abfc8a5
docs: deploy's upgrade notes name the release each starts from
CMGS Nov 3, 2026
b273f0e
docs: deploy and egress samples load and name a pulled image by its f…
CMGS Nov 3, 2026
b4be3f7
docs: deploy states what the code requires of a failed claim, a golde…
CMGS Nov 3, 2026
20f56ff
docs: deploy gives the archive registry's DDL
CMGS Nov 3, 2026
94c8f64
docs: deploy keeps the S3 layout and the replica bound in one place each
CMGS Nov 3, 2026
cde9191
docs: cluster lists registered volumes and the TTL, and says when the…
CMGS Nov 3, 2026
6ef720b
docs: security names the TLS edge, the re-admitted prefix and the sha…
CMGS Nov 3, 2026
8a4aeba
docs: benchmarks and performance keep each caveat once
CMGS Nov 3, 2026
e008046
docs: api states redirects, operator tokens, fork env and the checkpo…
CMGS Nov 3, 2026
a0c91b9
docs: api lists the errors of a call that reaches a guest once
CMGS Nov 3, 2026
2b3013b
docs: sdk, silkd, langchain and android match what the code does
CMGS Nov 3, 2026
a7737b4
docs: the sdk pages link the volume and deadline rules the api page owns
CMGS Nov 3, 2026
992a289
sandboxd: a preview listener that cannot bind is fatal
CMGS Nov 3, 2026
35f6d4c
tenants: the contract replaces the set with no tenants
CMGS Nov 3, 2026
0edfb80
review: one owner for the max fork default, the routable-host test, t…
CMGS Nov 3, 2026
9e339c6
review: store package docs fit the comment budget, constant errors us…
CMGS Nov 3, 2026
1e7a6e0
pool: a fenced node captures no replicated claim
CMGS Nov 3, 2026
833d720
pool: consumeArchive reads the row epoch under the manager lock
CMGS Nov 3, 2026
199d228
review: pool comments name what the code does now
CMGS Nov 3, 2026
a33459b
review: checkpoint.go keeps const and var above its types
CMGS Nov 3, 2026
2b0404a
review: fold pool helpers with one caller
CMGS Nov 3, 2026
4ead9f1
review: one owner for pool facts stated twice
CMGS Nov 3, 2026
bafaa4d
review: pool sweeps and wakes carry what they have
CMGS Nov 3, 2026
8343183
review: types.go splits into env.go and volume.go
CMGS Nov 3, 2026
71ffaf6
egress: an upstream dying mid-body truncates the guest's response
CMGS Nov 3, 2026
d56966f
outbound: ArmProxy closes a displaced door before it binds
CMGS Nov 3, 2026
6487307
engine: a restore gets the dump budget
CMGS Nov 3, 2026
0917a5e
review: drop checks and fields the code already guarantees
CMGS Nov 3, 2026
34dc83b
review: DialFunc's Router methods serve only tests
CMGS Nov 3, 2026
57b15c6
review: a CA key error names the key its caller parsed
CMGS Nov 3, 2026
d6cd718
review: cut a restating godoc and fold the engine package doc to one …
CMGS Nov 3, 2026
733e90c
review: layout: silkd helpers live in silkd.go, tcpAddrPort beside pr…
CMGS Nov 3, 2026
9f5b04f
docs: name the capture fence, the restore budget, a preview bind fail…
CMGS Nov 3, 2026
fe5e09f
review: VolumePatch keeps the SDK's omitzero tags, as the wire parity…
CMGS Nov 3, 2026
0823ea5
engine: a clone restores memory like a restore, so it gets the dump b…
CMGS Nov 3, 2026
85ac68a
review: silkd's language server feed needs no flag or channel, and on…
CMGS Nov 3, 2026
d815506
review: refill.go keeps the refill loop; golden builds go to golden.g…
CMGS Nov 3, 2026
e9b5c02
review: the reaper leaves claim.go for reap.go, since expiry handling…
CMGS Nov 3, 2026
8f5e3f1
review: claim and authed resolve a credential, so they sit in operato…
CMGS Nov 3, 2026
9d52157
review: the pool type joins its watermark methods in watermark.go, so…
CMGS Nov 3, 2026
e70b819
review: rewriteArchiveRow writes an archive row, so it sits in failov…
CMGS Nov 3, 2026
ca9aae8
review: NewManager delegates template and volume seeding to loadTempl…
CMGS Nov 3, 2026
314db7e
review: the catalog owns its entries and holders, so catalogVolume, c…
CMGS Nov 3, 2026
53d1f98
review: the seven single-flight sweep guards group into one sweeps fi…
CMGS Nov 3, 2026
1028de8
review: recordLocks owns the per-record lock maps and their mutex, an…
CMGS Nov 3, 2026
09f48f3
review: healVetoes owns the heal veto maps and their mutex, replacing…
CMGS Nov 3, 2026
0f3cacd
review: ckMarkers owns the archive delete markers, and the delete and…
CMGS Nov 3, 2026
427d1a2
pool: an uncordon during an evacuation's drain stops it
CMGS Nov 4, 2026
a2e8656
pool: a tier claim woken past its lease before the export gets a fres…
CMGS Nov 4, 2026
def8bdc
pool: a running claim that moved to a peer closes its egress doors
CMGS Nov 4, 2026
118accf
docs: a moved claim's token, its first wake, and the 503s an evacuati…
CMGS Nov 4, 2026
ac73905
sdk: Close follows a sandbox that moved off a retired node
CMGS Nov 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 16 additions & 27 deletions boot/init/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::fs;
use std::path::Path;
use std::time::{Duration, Instant};

use crate::cfg::{self, BootCfg};
use crate::cfg::{self, BootCfg, IpParam};
use crate::sys;

const LAYER_DIR: &str = "/l";
Expand Down Expand Up @@ -77,17 +77,12 @@ pub fn run() -> ! {
uptime(),
cfg.init
);
let err = sys::exec_init(&cfg.init);
let err = sys::exec_init(cfg.init);
sys::fatal(&format!("exec {}: {err}", cfg.init), cfg.debug)
}

fn assemble(cfg: &BootCfg, marks: &mut Marks) -> Result<(), String> {
let ids: Vec<&str> = cfg
.layers
.iter()
.map(String::as_str)
.chain([cfg.cow.as_str()])
.collect();
fn assemble(cfg: &BootCfg<'_>, marks: &mut Marks) -> Result<(), String> {
let ids: Vec<&str> = cfg.layers.iter().copied().chain([cfg.cow]).collect();
let mut devs = resolve_disks(&ids, cfg.timeout)?;
let cow_dev = devs
.pop()
Expand Down Expand Up @@ -119,7 +114,7 @@ fn assemble(cfg: &BootCfg, marks: &mut Marks) -> Result<(), String> {
)?;
marks.mark("overlay");

if let Some(hostname) = &cfg.hostname {
if let Some(hostname) = cfg.hostname {
sys::sethostname(hostname)?;
}
// empty machine-id => systemd generates a fresh one per VM (clone identity).
Expand All @@ -140,7 +135,7 @@ fn assemble(cfg: &BootCfg, marks: &mut Marks) -> Result<(), String> {
}

/// Persists kernel ip= params as MAC-matched networkd units in the new root.
fn persist_network(cfg: &BootCfg) {
fn persist_network(cfg: &BootCfg<'_>) {
if cfg.ips.is_empty() {
return;
}
Expand All @@ -149,8 +144,7 @@ fn persist_network(cfg: &BootCfg) {
eprintln!("sandbox-init: WARN: mkdir {dir}: {err}");
return;
}
let devices: Vec<&str> = cfg.ips.iter().map(|ip| ip.device.as_str()).collect();
for (ip, mac) in cfg.ips.iter().zip(wait_nic_macs(&devices, NIC_TIMEOUT)) {
for (ip, mac) in cfg.ips.iter().zip(wait_nic_macs(&cfg.ips, NIC_TIMEOUT)) {
let Some(mac) = mac else {
eprintln!(
"sandbox-init: WARN: NIC {} not found, static config skipped",
Expand All @@ -166,11 +160,11 @@ fn persist_network(cfg: &BootCfg) {
}

/// Resolves every NIC MAC against one shared deadline, so a missing NIC costs the timeout once.
fn wait_nic_macs(devices: &[&str], timeout: Duration) -> Vec<Option<String>> {
poll_slots(devices.len(), timeout, |found| {
for (slot, device) in found.iter_mut().zip(devices) {
fn wait_nic_macs(ips: &[IpParam<'_>], timeout: Duration) -> Vec<Option<String>> {
poll_slots(ips.len(), timeout, |found| {
for (slot, ip) in found.iter_mut().zip(ips) {
if slot.is_none() {
*slot = read_nic_mac(device);
*slot = read_nic_mac(ip.device);
}
}
})
Expand Down Expand Up @@ -231,12 +225,8 @@ fn scan_serials(ids: &[&str], found: &mut [Option<String>]) {
continue;
}
// the serial attribute location varies by kernel version.
let paths = [
format!("/sys/block/{name}/serial"),
format!("/sys/block/{name}/device/serial"),
];
for path in paths {
let Ok(serial) = fs::read_to_string(&path) else {
for sub in ["serial", "device/serial"] {
let Ok(serial) = fs::read_to_string(format!("/sys/block/{name}/{sub}")) else {
continue;
};
if serial.trim_end().is_empty() {
Expand Down Expand Up @@ -266,10 +256,9 @@ fn record_serial(ids: &[&str], found: &mut [Option<String>], serial: &str, devic
}

fn uptime() -> String {
fs::read_to_string("/proc/uptime")
.ok()
.and_then(|s| s.split_ascii_whitespace().next().map(String::from))
.unwrap_or_else(|| "?".into())
let mut s = fs::read_to_string("/proc/uptime").unwrap_or_else(|_| "?".into());
s.truncate(s.find(' ').unwrap_or(s.len()));
s
}

#[cfg(test)]
Expand Down
79 changes: 37 additions & 42 deletions boot/init/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,24 @@
use std::fmt::Write as _;
use std::time::Duration;

pub const DEFAULT_INIT: &str = "/sbin/init";
const DEFAULT_INIT: &str = "/sbin/init";
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
// cap the timeout: `Instant + Duration` panics on overflow, and a PID 1 panic is a kernel panic.
const MAX_TIMEOUT_SECS: u64 = 86_400;

#[derive(Debug, PartialEq, Eq)]
pub struct BootCfg {
pub struct BootCfg<'a> {
/// Layer disk serials in lowerdir order (first = uppermost lower layer).
pub layers: Vec<String>,
pub layers: Vec<&'a str>,
/// Writable ext4 COW disk ID (same resolution rules as layers).
pub cow: String,
pub cow: &'a str,
/// Wait budget for the whole disk set; NICs get a fixed 200 ms.
pub timeout: Duration,
pub hostname: Option<String>,
pub hostname: Option<&'a str>,
/// Static per-NIC config, persisted as networkd units and never applied in the initramfs.
pub ips: Vec<IpParam>,
pub ips: Vec<IpParam<'a>>,
/// Handoff target inside the assembled rootfs.
pub init: String,
pub init: &'a str,
/// Fatal errors drop to /bin/sh (debug initramfs) instead of poweroff.
pub debug: bool,
/// Emit one pre-handoff line with per-phase µs timings (sandbox.trace=1).
Expand All @@ -29,51 +29,46 @@ pub struct BootCfg {

/// One kernel `ip=` param, as cocoon emits it per NIC.
#[derive(Debug, PartialEq, Eq)]
pub struct IpParam {
pub addr: String,
pub struct IpParam<'a> {
pub addr: &'a str,
pub prefix: u8,
pub gateway: Option<String>,
pub dns: Vec<String>,
pub gateway: Option<&'a str>,
pub dns: Vec<&'a str>,
/// Initramfs-time device name, used only to look up the MAC the persisted unit matches on.
pub device: String,
pub device: &'a str,
}

pub fn parse(cmdline: &str) -> Result<BootCfg, String> {
pub fn parse(cmdline: &str) -> Result<BootCfg<'_>, String> {
let mut cfg = BootCfg {
layers: Vec::new(),
cow: String::new(),
cow: "",
timeout: DEFAULT_TIMEOUT,
hostname: None,
ips: Vec::new(),
init: DEFAULT_INIT.to_string(),
init: DEFAULT_INIT,
debug: false,
trace: false,
};
for (key, val) in params(cmdline) {
match key {
"cocoon.layers" => {
cfg.layers = val
.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
cfg.layers = val.split(',').filter(|s| !s.is_empty()).collect();
}
"cocoon.cow" => cfg.cow = val.to_string(),
// a junk value keeps the default, matching the old initramfs hook.
"cocoon.cow" => cfg.cow = val,
"cocoon.timeout" => {
if let Ok(secs) = val.parse::<u64>() {
cfg.timeout = Duration::from_secs(secs.min(MAX_TIMEOUT_SECS));
}
}
"cocoon.hostname" if !val.is_empty() => cfg.hostname = Some(val.to_string()),
"cocoon.hostname" if !val.is_empty() => cfg.hostname = Some(val),
"sandbox.debug" => cfg.debug = debug_token(val),
"sandbox.trace" => cfg.trace = debug_token(val),
"ip" => {
if let Some(param) = parse_ip_param(val) {
cfg.ips.push(param);
}
}
"sandbox.init" if !val.is_empty() => cfg.init = val.to_string(),
"sandbox.init" if !val.is_empty() => cfg.init = val,
_ => {}
}
}
Expand Down Expand Up @@ -128,24 +123,24 @@ fn debug_token(val: &str) -> bool {
}

/// Kernel ip= fields: client:server:gw:netmask:hostname:device:autoconf[:dns0[:dns1]]; anything else is ignored.
fn parse_ip_param(val: &str) -> Option<IpParam> {
fn parse_ip_param(val: &str) -> Option<IpParam<'_>> {
let f: Vec<&str> = val.split(':').collect();
if f.len() < 7 || f[0].is_empty() || f[5].is_empty() {
return None;
}
let prefix = mask_to_prefix(f[3])?;
let gateway = (!f[2].is_empty() && f[2] != "0.0.0.0").then(|| f[2].to_string());
let gateway = (!f[2].is_empty() && f[2] != "0.0.0.0").then_some(f[2]);
let dns = f[7..]
.iter()
.filter(|d| !d.is_empty() && **d != "0.0.0.0")
.map(|d| d.to_string())
.copied()
.filter(|d| !d.is_empty() && *d != "0.0.0.0")
.collect();
Some(IpParam {
addr: f[0].to_string(),
addr: f[0],
prefix,
gateway,
dns,
device: f[5].to_string(),
device: f[5],
})
}

Expand All @@ -171,7 +166,7 @@ mod tests {
assert_eq!(cfg.layers, vec!["a", "b"]);
assert_eq!(cfg.cow, "cow");
assert_eq!(cfg.timeout, Duration::from_secs(3));
assert_eq!(cfg.hostname.as_deref(), Some("vm1"));
assert_eq!(cfg.hostname, Some("vm1"));
assert_eq!(cfg.init, "/bin/systemd");
assert!(cfg.debug);
assert!(cfg.ips.is_empty());
Expand Down Expand Up @@ -213,11 +208,11 @@ mod tests {

#[test]
fn parse_huge_timeout_is_capped() {
let cfg = parse(&format!(
let cmdline = format!(
"cocoon.layers=l0 cocoon.cow=cow cocoon.timeout={}",
u64::MAX
))
.unwrap();
);
let cfg = parse(&cmdline).unwrap();
assert_eq!(cfg.timeout, Duration::from_secs(MAX_TIMEOUT_SECS));
}

Expand Down Expand Up @@ -277,11 +272,11 @@ mod tests {
assert_eq!(
cfg.ips,
vec![IpParam {
addr: "172.20.100.5".into(),
addr: "172.20.100.5",
prefix: 24,
gateway: Some("172.20.100.1".into()),
dns: vec!["8.8.8.8".into(), "8.8.4.4".into()],
device: "eth0".into(),
gateway: Some("172.20.100.1"),
dns: vec!["8.8.8.8", "8.8.4.4"],
device: "eth0",
}]
);
}
Expand Down Expand Up @@ -315,11 +310,11 @@ mod tests {
#[test]
fn network_unit_layout() {
let ip = IpParam {
addr: "172.20.100.5".into(),
addr: "172.20.100.5",
prefix: 24,
gateway: Some("172.20.100.1".into()),
dns: vec!["8.8.8.8".into()],
device: "eth0".into(),
gateway: Some("172.20.100.1"),
dns: vec!["8.8.8.8"],
device: "eth0",
};
assert_eq!(
network_unit(&ip, "aa:bb:cc:dd:ee:ff"),
Expand Down
5 changes: 3 additions & 2 deletions docs/android.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@ ln, err := sb.ProxyPort(ctx, "127.0.0.1:6555", 5555)

## What works, what differs

- `sb.Exec` runs `/system/bin/sh` — there is no bash and no glibc, so the
session verbs (persistent bash) are unavailable; prefix commands with
- There is no bash and no glibc, so pass `/system/bin/sh` as argv[0]
(`sb.Exec(ctx, "/system/bin/sh", "-c", ...)`), and the session verbs
(persistent bash) are unavailable; prefix commands with
`PATH=/system/bin:/system/xbin:$PATH` when calling tools directly.
- Files, port relay, and watch verbs work as on the Ubuntu flavors.
- Checkpoint/branch of a booted Android works: a branch answers `ps` and
Expand Down
5 changes: 2 additions & 3 deletions docs/benchmarks.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,13 @@ prints a markdown table stamped with the host evidence: virtualization
(`systemd-detect-virt`), CPU, kernel, cocoon version, image digest.
On exit it removes every `sbx-*` VM on the cocoon root, a serving
sandboxd's claims included, so run it only on a node that serves no claims.
The daemon also reclaims every `sbx-` VM at start.

Knobs (environment variables): `WARM`/`WARM_N` (warm-pool depth and burst
size — the burst must stay within the depth, or refill loses the race and
the tail measures clones), `CLONE_N`, `COLD_N`, `RPC_N`, `PULL_MB`,
`PULL_N`, `BURST_N` (concurrent clone-claim burst; 0 skips the stage),
`ADDR`/`TOKEN` (the throwaway daemon's listen address and token). Run the
harness on a node with no live sandboxd: the daemon reclaims every `sbx-` VM at
start, and the harness removes every `sbx-` VM at exit.
`ADDR`/`TOKEN` (the throwaway daemon's listen address and token).

Boot anatomy (where inside the cold tier the milliseconds go — kernel,
initramfs phases, rootfs handoff) has its own harness:
Expand Down
Loading
Loading