Files
felhom-agent/internal/localapi/intermediary_install_test.go
T
admin 3f382bf762 v0.61.0: audit fixes B1 (random temp staging) + D1 (mkfs wrapper member/RO re-checks) + D2 (empty-lsblk fail-safe) + D3 (blank-format anti-retarget)
From AUDIT-blast-radius-hostroot-localapi-2026-07-02.md. Each fix ships with a
non-hollow test + a companion red-proof (shown failing on the pre-fix impl).
Sudoers install-source grants became globs — deploy the sudoers drop-in with
the binary. A1 (stale-lock pool-membership) deliberately excluded (spike).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
2026-07-03 07:25:50 +02:00

98 lines
3.5 KiB
Go

package localapi
import (
"context"
"io"
"os"
"regexp"
"testing"
)
// stagingRecorderRunner is a fake proxmox.Runner recording every call vector and snapshotting each
// install SOURCE file's content at call time (the deferred os.Remove erases it afterwards).
type stagingRecorderRunner struct {
calls [][]string
srcContent map[string]string // install dest → staged source content
}
func (r *stagingRecorderRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) {
r.calls = append(r.calls, append([]string{name}, args...))
if name == "install" && len(args) >= 2 {
src, dest := args[len(args)-2], args[len(args)-1]
if r.srcContent == nil {
r.srcContent = map[string]string{}
}
b, _ := os.ReadFile(src)
r.srcContent[dest] = string(b)
}
return nil, nil, nil
}
func (r *stagingRecorderRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) {
return r.Run(ctx, name, args...)
}
// installSources returns the install-call source paths keyed by destination.
func (r *stagingRecorderRunner) installSources() map[string][]string {
out := map[string][]string{}
for _, c := range r.calls {
if c[0] == "install" && len(c) >= 3 {
src, dest := c[len(c)-2], c[len(c)-1]
out[dest] = append(out[dest], src)
}
}
return out
}
// TestInstallSharedParent_RandomTempName is the audit-B1 negative test for the shared-parent boot
// persistence install: both staged install SOURCES (script + unit) must be RANDOM os.CreateTemp names
// (felhom-shared-parent-<random>.sh / .service), never the fixed, pre-creatable /tmp names (a local
// TOCTOU into a root-executed boot script), and two consecutive installs must use DIFFERENT paths.
func TestInstallSharedParent_RandomTempName(t *testing.T) {
r := &stagingRecorderRunner{}
b := NewGuestBinder(r, nil)
if err := b.installSharedParentUnit(context.Background()); err != nil {
t.Fatalf("installSharedParentUnit #1: %v", err)
}
if err := b.installSharedParentUnit(context.Background()); err != nil {
t.Fatalf("installSharedParentUnit #2: %v", err)
}
srcs := r.installSources()
cases := []struct {
dest string
random *regexp.Regexp
fixed *regexp.Regexp
content string
}{
{sharedParentScriptPath, regexp.MustCompile(`felhom-shared-parent-[^/\\]+\.sh$`),
regexp.MustCompile(`felhom-shared-parent\.sh$`), sharedParentScript},
{sharedParentUnitPath, regexp.MustCompile(`felhom-shared-parent-[^/\\]+\.service$`),
regexp.MustCompile(`felhom-shared-parent\.service$`), sharedParentUnit},
}
for _, tc := range cases {
got := srcs[tc.dest]
if len(got) != 2 {
t.Fatalf("dest %s: expected 2 install calls, got %d (%v)", tc.dest, len(got), got)
}
for i, src := range got {
if !tc.random.MatchString(src) {
t.Errorf("dest %s call %d: source %q does not match the random temp pattern", tc.dest, i, src)
}
if tc.fixed.MatchString(src) {
t.Errorf("dest %s call %d: source %q is the FIXED predictable temp name (B1 TOCTOU)", tc.dest, i, src)
}
if _, err := os.Stat(src); err == nil {
t.Errorf("dest %s: staged temp %q left behind (defer os.Remove missing)", tc.dest, src)
}
}
if got[0] == got[1] {
t.Errorf("dest %s: two consecutive installs staged through the SAME source %q — must be random per call", tc.dest, got[0])
}
// Non-hollow: the staged file carried the real content at install time.
if r.srcContent[tc.dest] != tc.content {
t.Errorf("dest %s: staged content mismatch (got %d bytes, want %d)", tc.dest, len(r.srcContent[tc.dest]), len(tc.content))
}
}
}