Skip to content
Closed
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
31 changes: 31 additions & 0 deletions .github/workflows/boot-init.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# sandbox-init is PID 1 in every guest, but its own suite only ran as part of
# the image chain on main — a PR could not fail on it. This gate is the
# source-level counterpart to silkd.yml.
name: boot-init

on:
push:
branches: [main]
paths:
- "boot/init/**"
pull_request:
paths:
- "boot/init/**"
workflow_dispatch:

permissions:
contents: read

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v7

- name: fmt + clippy + test
working-directory: boot/init
run: |
cargo fmt --check
cargo clippy --all-targets -- -D warnings
cargo test
5 changes: 5 additions & 0 deletions .github/workflows/build-os-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,10 @@ jobs:
- name: Checkout
uses: actions/checkout@v7

- name: Read versions
id: ver
run: echo "silkd=$(sed -n 's/^version = "\(.*\)"/\1/p' silkd/Cargo.toml | head -1)" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v4

Expand Down Expand Up @@ -321,6 +325,7 @@ jobs:
# single base:24.04 tag; revisit when a second base tag appears.
build-args: |
BASE_IMAGE=ghcr.io/${{ github.repository }}/base:24.04
SILKD_IMAGE=${{ inputs.silkd_image != '' && inputs.silkd_image || format('ghcr.io/{0}/silkd:{1}', github.repository, steps.ver.outputs.silkd) }}
INPUTS_HASH=${{ steps.inputs.outputs.hash }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Expand Down
20 changes: 15 additions & 5 deletions sdk/python/cocoonsandbox/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,13 @@ def run(self, argv: list[str], cwd: str = "", env: dict | None = None,
with self._dial() as conn:
conn.send("exec", argv=argv, cwd=cwd or None, env=env,
user=user or None, session=session or None)
if stdin:
_send_chunks(conn, stdin, op="stdin")
conn.send("stdin_close")
# The guest blocks writing output once its stdout buffer fills, and
# stops draining stdin while it does, so feeding stdin to completion
# before reading deadlocks on any payload past the socket buffers.
pump = threading.Thread(target=_feed_stdin, args=(conn, stdin), daemon=True)
pump.start()
code = _pump_stdio(conn, on_stdout, on_stderr)
pump.join()
if code is None:
raise ProtocolError("exec stream ended without an exit frame")
return code
Expand All @@ -90,7 +93,7 @@ def ps(self) -> list[dict]:
def kill(self, pid: int, signal: int | None = None) -> None:
"""Signals a tracked process (default SIGKILL); killing one that
already exited is a no-op success."""
self._done_rpc("kill", pid=pid, signal=signal)
self._done_rpc("kill", pid=pid, signal=signal or None)

def logs(self, pid: int, on_stdout: Callable[[bytes], object] | None = None,
on_stderr: Callable[[bytes], object] | None = None) -> int | None:
Expand Down Expand Up @@ -347,7 +350,7 @@ def pump_out():
break
guest.send(chunk)
guest.close_write()
except Exception:
finally:
guest.close()

def _call(self, op: str, expect: str, **fields) -> dict:
Expand Down Expand Up @@ -480,6 +483,13 @@ def _send_chunks(conn: Conn, data: bytes, op: str = "data", chunk: int = FS_CHUN
conn.send(op, data=view[off:off + chunk])


def _feed_stdin(conn: Conn, stdin: bytes) -> None:
with contextlib.suppress(Exception): # the reader reports the real failure
if stdin:
_send_chunks(conn, stdin, op="stdin")
conn.send("stdin_close")


def _pump_stdio(conn: Conn, on_stdout, on_stderr) -> int | None:
"""Streams stdout/stderr frames into the callbacks until the terminal
frame: the exit code, or None when the stream ends with done."""
Expand Down
33 changes: 33 additions & 0 deletions sdk/python/tests/test_proc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""Process-management verbs against a scripted fake conn: pure frame
plumbing, so the fake pins the framing and terminal semantics."""

import threading

from cocoonsandbox import Client, Sandbox
from cocoonsandbox.frames import FS_CHUNK


class FakeConn:
Expand Down Expand Up @@ -77,3 +80,33 @@ def test_attach_returns_exit_code(monkeypatch):
out = []
assert sb.attach(41, on_stdout=out.append) == 7
assert out == [b"late"]


class BlockingStdinConn(FakeConn):
"""A guest that stops draining stdin until its output is read: send blocks
past the buffer, exactly as the real socket pair does."""

def __init__(self, frames, buffer_frames):
super().__init__(frames)
self._room = threading.Semaphore(buffer_frames)
self._read = False

def send(self, op, **fields):
if op == "stdin" and not self._read and not self._room.acquire(blocking=False):
assert self._room.acquire(timeout=5), "stdin send deadlocked"
super().send(op, **fields)

def recv(self):
self._read = True
self._room.release()
return super().recv()


def test_run_pumps_stdin_while_reading_output(monkeypatch):
sb, conn = fake_sandbox(monkeypatch, [{"type": "exit", "code": 0}])
blocking = BlockingStdinConn([{"type": "exit", "code": 0}], buffer_frames=1)
monkeypatch.setattr(sb, "_dial", lambda: blocking)

assert sb.run(["cat"], stdin=b"x" * (FS_CHUNK * 3)) == 0
assert [op for op, _ in blocking.sent].count("stdin") == 3
assert blocking.sent[-1][0] == "stdin_close"
4 changes: 2 additions & 2 deletions silkd/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ RUN case "$TARGETARCH" in \
WORKDIR /silkd
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release && strip target/release/silkd
RUN cargo build --release --target "$(cat /musl-target)" && \
RUN cargo build --release --locked && strip target/release/silkd
RUN cargo build --release --locked --target "$(cat /musl-target)" && \
strip "target/$(cat /musl-target)/release/silkd" && \
cp "target/$(cat /musl-target)/release/silkd" /silkd-static

Expand Down