Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,18 @@
and a liveness probe, so a guest whose sshd stops answering fails after ~90s
— the bound interactive sessions already had.

- **The guest hostname resolves, so `sudo` stops warning** — Instance creation
renamed the Firecracker guest to `claude-<name>` in `/etc/hostname` but left
the image's `127.0.1.1 claude-vm` entry in `/etc/hosts`, so every `sudo` in
the guest printed `sudo: unable to resolve host claude-<name>` before running.
Both files are now written together at create and restore, and the guest
hostname is clamped to fit the kernel's 64-byte hostname limit so long
instance names still get a resolvable name. No image rebuild is needed — the patch is
per-instance, and the image's own entry is what gets overwritten — but
`patch_guest_network` runs only on create and restore, so an existing VM
keeps the stale entry until `coop restore <vm> --image <image>` or a destroy
and recreate.

- **Fail closed on an unmanaged `CODEX_HOME` in ChatGPT auth mode** (#441) —
The guest wrapper now refuses an explicitly set `CODEX_HOME` when coop's
managed `~/.codex/config.toml` selects keyring storage. This prevents `codex
Expand Down
2 changes: 1 addition & 1 deletion docs/backends.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ All three steps are idempotent. If the artifact already exists and is up to date
Creating an instance (`coop up`) follows this sequence:

1. Copies the template rootfs to the instance directory using `cp --reflink=auto` for copy-on-write on supported filesystems.
2. Mounts the copy and patches the guest network config with the instance's unique IP address and hostname.
2. Mounts the copy and patches the guest network config with the instance's unique IP address, plus `/etc/hostname` and the matching `/etc/hosts` alias so the guest can resolve its own name.
3. Optionally resizes the rootfs if a larger disk was requested (truncate + e2fsck + resize2fs).
4. Writes a Firecracker JSON config specifying the kernel, rootfs drive, vCPU/memory allocation, network interface, and vsock device.
5. Creates and attaches a TAP device to the bridge (see TAP networking below).
Expand Down
12 changes: 12 additions & 0 deletions docs/trust-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,18 @@ user launched it.
`tar_pipe_pull` / `rsync_pull` bring guest-authored file contents, filenames,
and symlinks onto the host filesystem. This is the **widest guest→host
channel** and the primary place a path-traversal or symlink escape could land.
- **Rootfs files touched while loop-mounted during setup.** `setup.rs`
`patch_guest_network` reads and rewrites the guest's `/etc/hosts`, and `coop
commit` turns a guest-mutated rootfs into an image template — so the guest
authors both the contents and the directory entry at that path on every later
create/restore. Contents are read bounded and best-effort
(`bound_guest_hosts` degrades to a default rather than aborting the
lifecycle). Hosts-file operations use pinned directory descriptors, reject
symlinked `/etc`, and read only regular files checked through an `O_PATH`
descriptor. Replacement is atomic; permissions are set on the new file's
descriptor. Other paths remain **host** paths: `MountGuard::simple` is a
loop mount, not a chroot, so the traversal rule below still applies to the
hostname and network-config writes. Those paths are not currently validated.
- **Guest command output read by the host.** e.g. `check_guest_dirty` reads
`git status --porcelain` from the guest. Today this only gates control flow /
is printed to the user — it is never fed into `sh -c` on the host. Keep it
Expand Down
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ pub(crate) struct Cli {

#[derive(Subcommand)]
enum Commands {
/// Internal privileged helper for a mounted Firecracker rootfs.
#[cfg(target_os = "linux")]
#[command(name = "__patch-guest-hosts", hide = true)]
PatchGuestHosts { mount: PathBuf, hostname: String },

/// Ensure an environment for a project directory exists and is running.
///
/// Re-runnable: if an instance already exists for DIR it is reused
Expand Down Expand Up @@ -926,6 +931,15 @@ pub fn run() -> Result<()> {
let cli = Cli::parse();
init_tracing(cli.verbose);

#[cfg(target_os = "linux")]
if let Commands::PatchGuestHosts {
ref mount,
ref hostname,
} = cli.command
{
return setup::patch_guest_hosts(mount, hostname);
}

if let Commands::Completions { shell } = cli.command {
completions::emit_static(shell);
return Ok(());
Expand Down Expand Up @@ -997,6 +1011,8 @@ pub fn run() -> Result<()> {

let raw_args: Vec<String> = std::env::args().collect();
match cli.command {
#[cfg(target_os = "linux")]
Commands::PatchGuestHosts { .. } => unreachable!("handled before config loading"),
Commands::Up {
dir,
name,
Expand Down
Loading