ce2441f312
---ci--- project: orca phase: 1 milestone: v0.12 status: execute ---/ci--- shellQuote the jobspec-supplied command string (cmdStr) before interpolating into SSH exec in podman.go (Start) and wasm.go (Start). Previously cmdStr was interpolated unquoted, allowing a malicious jobspec command with shell metacharacters (; | $() backticks newline > <) to inject commands on the peer. Fixes: - internal/runtime/runtime.go: add shellQuote helper (mirrors internal/sshpush.shellQuote; duplicated to avoid import cycle). - internal/runtime/podman.go: Start quotes name + cmdStr; Stop/rm/ inspect quote name (defense-in-depth). - internal/runtime/wasm.go: Start uses env 'ORCA_ALLOC_ID=<id>' (so the UUID-style alloc ID is safely assigned) and shellQuote(cmdStr). Tests: 21 new injection regression tests (10 podman + 9 wasm + 2 image) covering ; && | $() backticks newline $IFS > < (). All pass. Existing runtime tests still pass. go vet + gofmt clean.
143 lines
4.0 KiB
Go
143 lines
4.0 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/sshpush"
|
|
)
|
|
|
|
// PodmanRuntime implements Runtime for the "podman" one_of. It runs
|
|
// `podman` on the peer over the SSH-push transport (P01). The
|
|
// transport is injected via the constructor (dependency injection).
|
|
//
|
|
// Container lifecycle:
|
|
//
|
|
// - Prepare: `podman pull <image>`
|
|
// - Start: `podman run -d --name orca-<alloc-id> <image> <command>`
|
|
// - Stop: `podman stop <name>` then `podman rm <name>`
|
|
// - Status: `podman inspect --format '{{.State.Running}}' <name>`
|
|
//
|
|
// The container name is `orca-<alloc-id>` (sanitized to lowercase +
|
|
// alnum). The runtime keeps no in-process state — each call is a fresh
|
|
// SSH exec against the peer.
|
|
type PodmanRuntime struct {
|
|
transport *sshpush.Transport
|
|
}
|
|
|
|
// NewPodmanRuntime returns a PodmanRuntime backed by the given transport.
|
|
func NewPodmanRuntime(t *sshpush.Transport) *PodmanRuntime {
|
|
return &PodmanRuntime{transport: t}
|
|
}
|
|
|
|
// containerName returns the deterministic container name for an alloc.
|
|
func containerName(alloc *Alloc) string {
|
|
id := strings.ToLower(alloc.ID)
|
|
id = strings.Map(func(r rune) rune {
|
|
if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' || r == '-' || r == '_' {
|
|
return r
|
|
}
|
|
return '-'
|
|
}, id)
|
|
return "orca-" + id
|
|
}
|
|
|
|
// Prepare pulls the image on the peer.
|
|
func (p *PodmanRuntime) Prepare(ctx context.Context, alloc *Alloc) error {
|
|
image, err := imageFor(alloc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := fmt.Sprintf("podman pull %q", image)
|
|
if _, err := p.transport.Exec(ctx, alloc.Node, cmd); err != nil {
|
|
return fmt.Errorf("podman: pull: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Start runs `podman run -d --name <name> <image> <command>`.
|
|
func (p *PodmanRuntime) Start(ctx context.Context, alloc *Alloc) (int, error) {
|
|
image, err := imageFor(alloc)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
cmdStr, _ := commandFor(alloc)
|
|
name := containerName(alloc)
|
|
cmd := fmt.Sprintf("podman run -d --name %s %q %s", shellQuote(name), image, shellQuote(cmdStr))
|
|
out, err := p.transport.Exec(ctx, alloc.Node, cmd)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("podman: run: %w", err)
|
|
}
|
|
// The container ID is the first 12 chars of the printed hash. We
|
|
// don't keep it — Stop/Status use the name — but return a stable
|
|
// synthetic PID derived from the first 4 bytes of the hash for
|
|
// the interface contract.
|
|
cid := strings.TrimSpace(string(out))
|
|
return podmanCidToPID(cid), nil
|
|
}
|
|
|
|
// Stop stops and removes the container.
|
|
func (p *PodmanRuntime) Stop(ctx context.Context, alloc *Alloc) error {
|
|
name := containerName(alloc)
|
|
if _, err := p.transport.Exec(ctx, alloc.Node, fmt.Sprintf("podman stop %s", shellQuote(name))); err != nil {
|
|
return fmt.Errorf("podman: stop: %w", err)
|
|
}
|
|
if _, err := p.transport.Exec(ctx, alloc.Node, fmt.Sprintf("podman rm %s", shellQuote(name))); err != nil {
|
|
return fmt.Errorf("podman: rm: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Status inspects the container's running state.
|
|
func (p *PodmanRuntime) Status(ctx context.Context, alloc *Alloc) (State, error) {
|
|
name := containerName(alloc)
|
|
cmd := fmt.Sprintf("podman inspect --format '{{.State.Running}}' %s", shellQuote(name))
|
|
out, err := p.transport.Exec(ctx, alloc.Node, cmd)
|
|
if err != nil {
|
|
return StateFailed, fmt.Errorf("podman: inspect: %w", err)
|
|
}
|
|
v := strings.TrimSpace(string(out))
|
|
switch v {
|
|
case "true":
|
|
return StateRunning, nil
|
|
case "false":
|
|
return StateStopped, nil
|
|
default:
|
|
return StateFailed, fmt.Errorf("podman: unexpected inspect output %q", v)
|
|
}
|
|
}
|
|
|
|
// podmanCidToPID converts a container ID (hex hash) to a positive int
|
|
// PID for the interface contract. It reads up to 4 hex chars.
|
|
func podmanCidToPID(cid string) int {
|
|
if len(cid) < 1 {
|
|
return 1
|
|
}
|
|
n := len(cid)
|
|
if n > 4 {
|
|
n = 4
|
|
}
|
|
var pid int
|
|
for i := 0; i < n; i++ {
|
|
c := cid[i]
|
|
pid = (pid << 4) | int(hexVal(c))
|
|
}
|
|
if pid <= 0 {
|
|
pid = 1
|
|
}
|
|
return pid
|
|
}
|
|
|
|
func hexVal(c byte) byte {
|
|
switch {
|
|
case c >= '0' && c <= '9':
|
|
return c - '0'
|
|
case c >= 'a' && c <= 'f':
|
|
return c - 'a' + 10
|
|
case c >= 'A' && c <= 'F':
|
|
return c - 'A' + 10
|
|
}
|
|
return 0
|
|
}
|