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
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: DeterminateSystems/nix-installer-action@v22
with:
determinate: false
diagnostic-endpoint: ''
- name: Use systemstart Cachix cache
uses: cachix/cachix-action@v17
with:
name: systemstart
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
- run: nix flake check --impure
- run: nix build --impure .#claude
- run: nix build --impure .#gemini
Expand All @@ -25,3 +34,4 @@ jobs:
uses: ./.github/workflows/release.yml
permissions:
contents: write

9 changes: 9 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@ jobs:
with:
fetch-depth: 0
- uses: DeterminateSystems/nix-installer-action@v22
with:
determinate: false
diagnostic-endpoint: ''
- name: Use systemstart Cachix cache
uses: cachix/cachix-action@v17
with:
name: systemstart
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}
signingKey: ${{ secrets.CACHIX_SIGNING_KEY }}
- run: nix build --impure .#claude
- run: nix build --impure .#gemini
- run: nix build --impure .#codex
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/update-flake.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@v7
- uses: DeterminateSystems/nix-installer-action@v22
- uses: DeterminateSystems/update-flake-lock@main
- uses: DeterminateSystems/update-flake-lock@v28
with:
pr-title: "chore: nix flake update"
pr-body: |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ ENABLE_CRI=crun make claude.run
ENABLE_CRI=podman make claude.run
```

Container images and layers are stored on the host at `$AGENT_HOME/cri-storage/` via a dedicated virtiofs share at `/var/lib/containers`, so they persist across VM restarts and don't consume the VM's RAM-backed root filesystem.
Container images and layers are stored on a dedicated ext4 disk image at `$AGENT_HOME-cri/cri-storage.img` (sparse, up to 30 GiB), mounted at `/var/lib/containers`, so they persist across VM restarts and don't consume the VM's RAM-backed root filesystem. The image is kept in a host-only sibling directory next to agent home rather than inside it, so it is never exported through the agent-home virtiofs share and the guest cannot read or tamper with its own raw storage backing file. A real block-backed filesystem is required here rather than a virtiofs share: image unpack must `lchown` extracted layers to UID 0, which the host's rootless virtiofsd cannot do (it has a single-ID uid map). On a share, `docker info` reports `Backing Filesystem: fuse` and overlay2/KinD layer extraction fails with `lchown … operation not permitted`; on the ext4 volume it reports `extfs` and works.

#### Available runtimes

Expand Down
20 changes: 10 additions & 10 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 30 additions & 45 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -101,18 +101,25 @@
fi
AGENT_TEMP=""

# --- CRI storage share (virtiofsd) ---
CRI_SOCK="$RUNTIME/$VM_ID-virtiofs-$ID-cri-storage.sock"
CRI_UNIT="$VM_ID-virtiofsd-$ID-cri-storage"
CRI_STATE="$RUNTIME/$VM_ID-virtiofsd-$ID-cri-storage.dir"
CRI_DIR="$AGENT_DIR/cri-storage"
mkdir -p "$CRI_DIR"
# --- CRI storage volume (ext4 block image) ---
# Container runtimes need a real block-backed fs that permits lchown to
# arbitrary UIDs during image unpack — a virtiofs share cannot (rootless
# virtiofsd has a single-ID uid map). The image is a sparse ext4 disk
# created on first boot by microvm.nix's createVolumesScript. It must NOT
# live inside $AGENT_DIR: that directory is exported into the guest via
# the agent-home virtiofs share, so the guest could read or tamper with
# its own raw storage backing file. Keep it in a host-only sibling state
# dir that persists across runs but is never shared into the guest. The
# CRI module declares the image as "cri-storage.img"; rewrite it to this
# absolute path below.
CRI_STATE_DIR="$AGENT_DIR-cri"
mkdir -p "$CRI_STATE_DIR"
CRI_IMG="$CRI_STATE_DIR/cri-storage.img"

cleanup() {
${pkgs.systemd}/bin/systemctl --user stop "$UNIT" 2>/dev/null || true
${pkgs.systemd}/bin/systemctl --user stop "$AGENT_UNIT" 2>/dev/null || true
${pkgs.systemd}/bin/systemctl --user stop "$CRI_UNIT" 2>/dev/null || true
rm -f "$SOCK" "$AGENT_SOCK" "$STATE" "$AGENT_STATE" "$CRI_SOCK" "$CRI_STATE"
rm -f "$SOCK" "$AGENT_SOCK" "$STATE" "$AGENT_STATE"
if [ -n "$AGENT_TEMP" ]; then
rm -rf "$AGENT_TEMP"
fi
Expand Down Expand Up @@ -152,41 +159,6 @@
[ -S "$AGENT_SOCK" ] || { echo "error: agent-home virtiofsd socket did not appear"; exit 1; }
fi

# --- CRI storage virtiofsd ---
CRI_NEED_START=1
if ${pkgs.systemd}/bin/systemctl --user is-active "$CRI_UNIT" &>/dev/null; then
if [ -f "$CRI_STATE" ] && [ "$(cat "$CRI_STATE")" = "$CRI_DIR" ] && [ -S "$CRI_SOCK" ]; then
CRI_NEED_START=0
else
${pkgs.systemd}/bin/systemctl --user stop "$CRI_UNIT" 2>/dev/null || true
fi
fi

if [ "$CRI_NEED_START" = "1" ]; then
rm -f "$CRI_SOCK"

${pkgs.systemd}/bin/systemd-run --user --unit="$CRI_UNIT" --collect \
-- ${virtiofsd}/bin/virtiofsd \
--socket-path="$CRI_SOCK" \
--shared-dir="$CRI_DIR" \
--sandbox=namespace \
--uid-map ":0:$(id -u):1:" \
--gid-map ":0:$(id -g):1:" \
--translate-uid "map:1000:0:1" \
--translate-gid "map:1000:0:1" \
--socket-group="$(id -gn)" \
--xattr \
--xattrmap ":prefix:all:trusted.:user.virtiofs.::prefix:all:security.:user.virtiofs.::ok:all:::"

echo "$CRI_DIR" > "$CRI_STATE"

for i in $(seq 1 50); do
[ -S "$CRI_SOCK" ] && break
sleep 0.1
done
[ -S "$CRI_SOCK" ] || { echo "error: cri-storage virtiofsd socket did not appear"; exit 1; }
fi

# Write host env vars for the VM
echo "DIRENV_ALLOW=''${DIRENV_ALLOW:-0}" > "$AGENT_DIR/.microvm-env"
echo "ENABLE_CRI=''${ENABLE_CRI:-}" >> "$AGENT_DIR/.microvm-env"
Expand Down Expand Up @@ -249,6 +221,11 @@
|| rm -f "$AGENT_DIR/.microvm-nix-db.sqlite"
fi

# $CRI_IMG is used as sed replacement text below. Escape characters
# that are special on the replacement side (\, &) and the | delimiter
# so a path with such characters can't corrupt the generated command.
_CRI_IMG_ESC=$(printf '%s' "$CRI_IMG" | ${pkgs.gnused}/bin/sed -e 's/[\\&|]/\\&/g')

# Build sed arguments for QEMU runner
_SED_ARGS=(
# Process and QEMU name: inject project basename
Expand All @@ -259,8 +236,16 @@
-e "s|${hostname}-virtiofs-work.sock|$SOCK|g"
-e "s|/tmp/${hostname}-home|$AGENT_DIR|g"
-e "s|${hostname}-virtiofs-agent-home.sock|$AGENT_SOCK|g"
-e "s|/tmp/${hostname}-cri-storage|$CRI_DIR|g"
-e "s|${hostname}-virtiofs-cri-storage.sock|$CRI_SOCK|g"
# CRI storage volume image: microvm.nix emits the relative path
# "cri-storage.img" in both the createVolumesScript and the QEMU
# -drive; point both at the persistent image in agent home.
-e "s|cri-storage.img|$_CRI_IMG_ESC|g"
# microvm.nix unconditionally sets cache=none (O_DIRECT) on volume
# drives, which fails when agent home is on a filesystem without
# O_DIRECT support (tmpfs, some network/virtiofs mounts). The CRI
# store is a rebuildable cache, so writeback (host page cache) is the
# portable choice and works on every backing filesystem.
-e "s|,cache=none|,cache=writeback|g"
# Runtime mem/vcpu override (VM_MEM / VM_VCPU env vars)
-e "s| -m ${toString defaultMem} | -m $VM_MEM |g"
-e "s| -smp ${toString defaultVcpu} | -smp $VM_VCPU |g"
Expand Down
17 changes: 13 additions & 4 deletions modules/cri.nix
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,21 @@ CRIEOF
export CONTAINER_HOST=unix:///run/podman/podman.sock
'';

microvm.shares = [
# Container storage must live on a real block-backed filesystem, NOT a
# virtiofs share. The host virtiofsd runs rootless with a single-ID uid/gid
# map, so it cannot lchown extracted layer files to UID 0 — overlay2/KinD
# image unpack fails with "lchown /bin: operation not permitted" and
# `docker info` reports `Backing Filesystem: fuse`. A dedicated ext4 volume
# gives the guest a genuine root-capable fs (Backing Filesystem: extfs).
# The sparse image is created on the host and persists in agent home; the
# runner script (flake.nix) rewrites the `cri-storage.img` path accordingly.
microvm.volumes = [
{
tag = "cri-storage";
source = "/tmp/${config.networking.hostName}-cri-storage";
image = "cri-storage.img";
label = "cri-storage";
mountPoint = "/var/lib/containers";
proto = "virtiofs";
size = 30720; # MiB, sparse — max size, only consumes what is used
fsType = "ext4";
}
];

Expand Down
Loading