controllerswap: stdin tee write + narrow FELHOM_CONTROLLERSWAP grants (non-root, v0.45.0)
writeImage drops bash -c/printf for GuestExecStdin(img+\n -> tee /etc/felhom-controller-image); new Runner.RunStdin/GuestExecStdin route stdin through the fenced sudo -n runner. 5 narrow, auditable sudoers grants (no general pct exec, no bash -c) + capability manifest entries (Critical) so the self-probe watches them and the build-test asserts coverage (companion red-proof). No controller change; swap orchestration/rollback/state unchanged. Spike GO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EPZ4GJ8L5Jqf8UiPwbn1kt
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -39,6 +40,9 @@ func ValidControllerImage(ref string) bool { return controllerImageRe.MatchStrin
|
||||
// faked in tests. The single seam the swap composes over (no hand-rolled pct).
|
||||
type GuestExecutor interface {
|
||||
GuestExec(ctx context.Context, vmid int, args ...string) (string, error)
|
||||
// GuestExecStdin is GuestExec with the command's stdin fed from stdin — the swap write pipes the
|
||||
// image ref into an in-guest `tee` (no shell vector).
|
||||
GuestExecStdin(ctx context.Context, vmid int, stdin io.Reader, args ...string) (string, error)
|
||||
}
|
||||
|
||||
// ControllerSwapState is the durable record of a swap (crash-safety + status). Written before the swap
|
||||
@@ -133,10 +137,13 @@ func (c *ControllerSwapper) imagePresent(ctx context.Context, vmid int, image st
|
||||
}
|
||||
|
||||
func (c *ControllerSwapper) writeImage(ctx context.Context, vmid int, image string) error {
|
||||
// image is strict-validated (controllerImageRe) before we ever get here, so this single-quoted
|
||||
// interpolation cannot smuggle shell metacharacters.
|
||||
cmd := fmt.Sprintf("printf '%%s\\n' '%s' > %s", image, controllerImageFile)
|
||||
_, err := c.exec.GuestExec(ctx, vmid, "bash", "-c", cmd)
|
||||
// Non-root path: pipe the image ref into an in-guest `tee` over stdin — no shell, no
|
||||
// interpolation, no `bash -c` (the only swap vector that would have needed an arbitrary-exec
|
||||
// grant). The trailing "\n" makes the on-disk bytes byte-identical to the golden's
|
||||
// `printf '%s\n'`; the bootstrap reads `IMAGE=$(cat …)` so the newline is stripped on read
|
||||
// (spike SPIKE-controllerswap-narrow-grants-2026-06-29). image is strict-validated
|
||||
// (controllerImageRe) upstream in Swap; defence-in-depth, the stdin path can't smuggle anyway.
|
||||
_, err := c.exec.GuestExecStdin(ctx, vmid, strings.NewReader(image+"\n"), "tee", controllerImageFile)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user