package guesthook import ( "context" "io" "os" "regexp" "testing" ) // recordingRunner is a fake proxmox.Runner that records every call and snapshots the content of the // install SOURCE file at call time (the deferred os.Remove would erase it before the test can look). type recordingRunner struct { calls [][]string srcContent []string } func (r *recordingRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) { r.calls = append(r.calls, append([]string{name}, args...)) if name == "install" && len(args) > 0 { src := args[len(args)-2] b, _ := os.ReadFile(src) r.srcContent = append(r.srcContent, string(b)) } return nil, nil, nil } func (r *recordingRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) { return r.Run(ctx, name, args...) } // TestInstallSnippet_RandomTempName is the audit-B1 negative test: the staged install SOURCE must be a // RANDOM os.CreateTemp name (felhom-guest-hook-.sh), never the fixed, pre-creatable // /tmp/felhom-guest-hook.sh (a local TOCTOU into a root-executed hookscript), and two consecutive // installs must stage through DIFFERENT paths. func TestInstallSnippet_RandomTempName(t *testing.T) { r := &recordingRunner{} if err := InstallSnippet(context.Background(), r); err != nil { t.Fatalf("InstallSnippet #1: %v", err) } if err := InstallSnippet(context.Background(), r); err != nil { t.Fatalf("InstallSnippet #2: %v", err) } if len(r.calls) != 2 { t.Fatalf("expected 2 install calls, got %d: %v", len(r.calls), r.calls) } randomName := regexp.MustCompile(`felhom-guest-hook-[^/\\]+\.sh$`) fixedName := regexp.MustCompile(`felhom-guest-hook\.sh$`) var srcs []string for i, call := range r.calls { // install -m 0755 -- if call[0] != "install" || len(call) != 6 { t.Fatalf("call %d: unexpected vector %v", i, call) } src, dest := call[4], call[5] if dest != SnippetPath { t.Errorf("call %d: dest = %q, want %q", i, dest, SnippetPath) } if !randomName.MatchString(src) { t.Errorf("call %d: source %q does not match the random felhom-guest-hook-*.sh pattern", i, src) } if fixedName.MatchString(src) { t.Errorf("call %d: source %q is the FIXED predictable temp name (B1 TOCTOU)", i, src) } srcs = append(srcs, src) } if srcs[0] == srcs[1] { t.Errorf("two consecutive installs staged through the SAME source path %q — must be random per call", srcs[0]) } // Non-hollow: the staged file must actually carry the snippet body at install time. for i, c := range r.srcContent { if c != snippetBody { t.Errorf("call %d: staged content is not the snippet body (got %d bytes)", i, len(c)) } } // And the temp is cleaned up after. for _, src := range srcs { if _, err := os.Stat(src); err == nil { t.Errorf("staged temp %q left behind (defer os.Remove missing)", src) } } }