Skip to content

Commit 7308e0b

Browse files
lpcoxCopilot
andcommitted
feat: add Firecracker guest execution transport
Introduce the bounded vsock protocol, static guest supervisor, per-run workspace image staging and recovery-safe extraction, and reusable manager lifecycle primitives while keeping runtime dispatch fail-closed. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent fd0d9ae commit 7308e0b

19 files changed

Lines changed: 3603 additions & 19 deletions
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/sh
2+
set -eu
3+
4+
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
5+
GO_VERSION=go1.25.0
6+
VERSION=${VERSION:-dev}
7+
OUTPUT=${OUTPUT:-"$ROOT/firecracker-supervisor"}
8+
9+
actual=$(go env GOVERSION)
10+
if [ "$actual" != "$GO_VERSION" ]; then
11+
echo "required Go toolchain: $GO_VERSION (found $actual)" >&2
12+
exit 1
13+
fi
14+
15+
cd "$ROOT"
16+
CGO_ENABLED=0 GOOS=linux GOARCH="${GOARCH:-amd64}" \
17+
go build -trimpath -buildvcs=false -ldflags="-s -w -X main.version=$VERSION" -o "$OUTPUT" .
18+
if command -v sha256sum >/dev/null 2>&1; then
19+
sha256sum "$OUTPUT" > "$OUTPUT.sha256"
20+
else
21+
shasum -a 256 "$OUTPUT" > "$OUTPUT.sha256"
22+
fi
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net"
6+
"path/filepath"
7+
"strconv"
8+
"strings"
9+
)
10+
11+
type bootConfig struct {
12+
WorkspaceDevice string
13+
WorkspaceMount string
14+
VsockPort uint32
15+
GuestIP net.IP
16+
GuestPrefix int
17+
Gateway net.IP
18+
Interface string
19+
}
20+
21+
func parseBootConfig(cmdline string) (bootConfig, error) {
22+
values := make(map[string]string)
23+
for _, token := range strings.Fields(cmdline) {
24+
key, value, ok := strings.Cut(token, "=")
25+
if !ok || !strings.HasPrefix(key, "awf.") {
26+
continue
27+
}
28+
if _, duplicate := values[key]; duplicate {
29+
return bootConfig{}, fmt.Errorf("duplicate boot argument %q", key)
30+
}
31+
values[key] = value
32+
}
33+
required := []string{
34+
"awf.workspace-device", "awf.workspace-mount", "awf.vsock-port",
35+
"awf.guest-ip", "awf.guest-prefix", "awf.guest-gateway", "awf.guest-interface",
36+
}
37+
for _, key := range required {
38+
if values[key] == "" {
39+
return bootConfig{}, fmt.Errorf("missing required boot argument %q", key)
40+
}
41+
}
42+
port, err := strconv.ParseUint(values["awf.vsock-port"], 10, 32)
43+
if err != nil || port == 0 {
44+
return bootConfig{}, fmt.Errorf("invalid awf.vsock-port")
45+
}
46+
prefix, err := strconv.Atoi(values["awf.guest-prefix"])
47+
if err != nil || prefix < 0 || prefix > 32 {
48+
return bootConfig{}, fmt.Errorf("invalid awf.guest-prefix")
49+
}
50+
ip := net.ParseIP(values["awf.guest-ip"]).To4()
51+
gateway := net.ParseIP(values["awf.guest-gateway"]).To4()
52+
if ip == nil || gateway == nil {
53+
return bootConfig{}, fmt.Errorf("guest IP and gateway must be IPv4 addresses")
54+
}
55+
device := values["awf.workspace-device"]
56+
if !strings.HasPrefix(device, "/dev/") || filepath.Clean(device) != device || strings.Contains(device, "..") {
57+
return bootConfig{}, fmt.Errorf("invalid awf.workspace-device")
58+
}
59+
mount := values["awf.workspace-mount"]
60+
if !filepath.IsAbs(mount) || filepath.Clean(mount) != mount || mount == "/" {
61+
return bootConfig{}, fmt.Errorf("invalid awf.workspace-mount")
62+
}
63+
iface := values["awf.guest-interface"]
64+
if !validInterface(iface) {
65+
return bootConfig{}, fmt.Errorf("invalid awf.guest-interface")
66+
}
67+
return bootConfig{
68+
WorkspaceDevice: device, WorkspaceMount: mount, VsockPort: uint32(port),
69+
GuestIP: ip, GuestPrefix: prefix, Gateway: gateway, Interface: iface,
70+
}, nil
71+
}
72+
73+
func validInterface(name string) bool {
74+
if name == "" || len(name) > 15 {
75+
return false
76+
}
77+
for i, r := range name {
78+
if !(r == '-' || r == '_' || r == '.' || r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || (i > 0 && r >= '0' && r <= '9')) {
79+
return false
80+
}
81+
}
82+
return true
83+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package main
2+
3+
import "testing"
4+
5+
const validCmdline = "console=ttyS0 awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=1024 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0"
6+
7+
func TestParseBootConfig(t *testing.T) {
8+
config, err := parseBootConfig(validCmdline)
9+
if err != nil {
10+
t.Fatalf("parseBootConfig: %v", err)
11+
}
12+
if config.VsockPort != 1024 || config.Interface != "eth0" || config.GuestIP.String() != "192.0.2.2" {
13+
t.Fatalf("unexpected config: %#v", config)
14+
}
15+
}
16+
17+
func TestParseBootConfigRejectsUnsafeValues(t *testing.T) {
18+
cases := []string{
19+
"awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=0 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
20+
"awf.workspace-device=/dev/../etc/passwd awf.workspace-mount=/workspace awf.vsock-port=1 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
21+
"awf.workspace-device=/dev/vdb awf.workspace-mount=/ awf.vsock-port=1 awf.guest-ip=192.0.2.2 awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
22+
"awf.workspace-device=/dev/vdb awf.workspace-mount=/workspace awf.vsock-port=1 awf.guest-ip=bad awf.guest-prefix=24 awf.guest-gateway=192.0.2.1 awf.guest-interface=eth0",
23+
}
24+
for _, cmdline := range cases {
25+
if _, err := parseBootConfig(cmdline); err == nil {
26+
t.Errorf("unsafe command line accepted: %q", cmdline)
27+
}
28+
}
29+
}
30+
31+
func TestParseBootConfigRejectsDuplicateArguments(t *testing.T) {
32+
if _, err := parseBootConfig(validCmdline + " awf.vsock-port=1025"); err == nil {
33+
t.Fatal("duplicate argument accepted")
34+
}
35+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module github.com/github/gh-aw-firewall/firecracker-supervisor
2+
3+
go 1.24.0
4+
5+
toolchain go1.25.0
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// firecracker-supervisor is the minimal guest-side command supervisor.
2+
package main
3+
4+
import (
5+
"flag"
6+
"fmt"
7+
"os"
8+
)
9+
10+
var version = "dev"
11+
12+
func main() {
13+
showVersion := flag.Bool("version", false, "print version")
14+
flag.Parse()
15+
if *showVersion {
16+
fmt.Println(version)
17+
return
18+
}
19+
if err := runSupervisor(); err != nil {
20+
fmt.Fprintln(os.Stderr, "firecracker-supervisor:", err)
21+
os.Exit(1)
22+
}
23+
}

0 commit comments

Comments
 (0)