detached b72d29f

fix(vm): scope an unjailed Firecracker by its --api-sock

A Firecracker with --api-sock was ours on its id alone. Now the socket must
be an absolute path below <state>/vm/<id>/ (cleaned, compared lexically; the
resolved state path is accepted too), else it is someone else's. Tests: our
dir, socket elsewhere, another id, a .. escape, a relative path, a missing
value, a prefix sibling.
commit
b72d29f9e6b01e7bbb2740da6d6f2c6ab6fc2de6
author
brice · · 2026-10-12 22:15 UTC
parent
fd4fcdb
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +46 -9
M internal/sandbox/vm/proc.go +36 -6
@@ -37,6 +37,7 @@ type vmmArgs struct {
3737 id string
3838 markers map[string]bool
3939 chrootBase string // the jailer's --chroot-base-dir
40+ apiSock string // Firecracker's --api-sock value
4041 }
4142
4243 func parseVMMArgs(args []string) vmmArgs {
@@ -53,6 +54,9 @@ func parseVMMArgs(args []string) vmmArgs {
5354 }
5455 case slices.Contains(VMMMarkers, args[i]):
5556 a.markers[args[i]] = true
57+ if args[i] == MarkerAPISock && i+1 < len(args) {
58+ a.apiSock = args[i+1]
59+ }
5660 }
5761 }
5862 return a
@@ -73,17 +77,40 @@ func IDFromCmdline(args []string) string {
7377 // (or have been started for) its own jailer chroot base, <state dir>/jail.
7478 type scope struct {
7579 jailBases []string // the base as configured, and with symlinks resolved
80+ vmBases []string // <state dir>/vm (unjailed VMs), likewise
7681 }
7782
7883 func newScope(stateDir string) scope {
79- base := jailBase(stateDir)
80- sc := scope{jailBases: []string{base}}
81- if r, err := filepath.EvalSymlinks(base); err == nil && r != base {
82- sc.jailBases = append(sc.jailBases, r)
84+ sc := scope{}
85+ for _, b := range []struct {
86+ dir string
87+ to *[]string
88+ }{{jailBase(stateDir), &sc.jailBases}, {filepath.Join(stateDir, "vm"), &sc.vmBases}} {
89+ *b.to = []string{b.dir}
90+ if r, err := filepath.EvalSymlinks(b.dir); err == nil && r != b.dir {
91+ *b.to = append(*b.to, r)
92+ }
8393 }
8494 return sc
8595 }
8696
97+// apiSockIsOurs: an unjailed Firecracker's --api-sock must be a path below
98+// <state dir>/vm/<id>/ (compared lexically, after cleaning), the directory the
99+// driver puts it in. Another Firecracker with an id of our shape and a socket
100+// elsewhere is someone else's.
101+func (sc scope) apiSockIsOurs(sock, id string) bool {
102+ if sock == "" || !filepath.IsAbs(sock) {
103+ return false
104+ }
105+ sock = filepath.Clean(sock)
106+ for _, b := range sc.vmBases {
107+ if strings.HasPrefix(sock, filepath.Join(b, id)+"/") {
108+ return true
109+ }
110+ }
111+ return false
112+}
113+
87114 func (sc scope) isBase(p string) bool {
88115 return slices.Contains(sc.jailBases, filepath.Clean(p))
89116 }
@@ -105,7 +132,8 @@ func (sc scope) under(p string) bool {
105132 // below our jail base (rootIsOurs). Anything else with that flag and an id
106133 // of our shape belongs to someone else's jailer and is never touched.
107134 //
108-// A Firecracker started with --api-sock (no jailer) is ours by its id alone.
135+// A Firecracker started with --api-sock (no jailer) is ours when the socket is
136+// in <state dir>/vm/<id>/ (apiSockIsOurs).
109137 func (sc scope) match(dir string, args []string) string {
110138 a := parseVMMArgs(args)
111139 if a.id == "" || len(a.markers) == 0 {
@@ -113,7 +141,9 @@ func (sc scope) match(dir string, args []string) string {
113141 }
114142 switch {
115143 case a.markers[MarkerAPISock]:
116- return a.id
144+ if sc.apiSockIsOurs(a.apiSock, a.id) {
145+ return a.id
146+ }
117147 case a.markers[MarkerExecFile]:
118148 if sc.isBase(a.chrootBase) {
119149 return a.id
@@ -37,6 +37,7 @@ type vmmArgs struct {
37 id string37 id string
38 markers map[string]bool38 markers map[string]bool
39 chrootBase string // the jailer's --chroot-base-dir39 chrootBase string // the jailer's --chroot-base-dir
40+ apiSock string // Firecracker's --api-sock value
40 }41 }
41 42
42 func parseVMMArgs(args []string) vmmArgs {43 func parseVMMArgs(args []string) vmmArgs {
@@ -53,6 +54,9 @@ func parseVMMArgs(args []string) vmmArgs {
53 }54 }
54 case slices.Contains(VMMMarkers, args[i]):55 case slices.Contains(VMMMarkers, args[i]):
55 a.markers[args[i]] = true56 a.markers[args[i]] = true
57+ if args[i] == MarkerAPISock && i+1 < len(args) {
58+ a.apiSock = args[i+1]
59+ }
56 }60 }
57 }61 }
58 return a62 return a
@@ -73,17 +77,40 @@ func IDFromCmdline(args []string) string {
73 // (or have been started for) its own jailer chroot base, <state dir>/jail.77 // (or have been started for) its own jailer chroot base, <state dir>/jail.
74 type scope struct {78 type scope struct {
75 jailBases []string // the base as configured, and with symlinks resolved79 jailBases []string // the base as configured, and with symlinks resolved
80+ vmBases []string // <state dir>/vm (unjailed VMs), likewise
76 }81 }
77 82
78 func newScope(stateDir string) scope {83 func newScope(stateDir string) scope {
79- base := jailBase(stateDir)84+ sc := scope{}
80- sc := scope{jailBases: []string{base}}85+ for _, b := range []struct {
81- if r, err := filepath.EvalSymlinks(base); err == nil && r != base {86+ dir string
82- sc.jailBases = append(sc.jailBases, r)87+ to *[]string
88+ }{{jailBase(stateDir), &sc.jailBases}, {filepath.Join(stateDir, "vm"), &sc.vmBases}} {
89+ *b.to = []string{b.dir}
90+ if r, err := filepath.EvalSymlinks(b.dir); err == nil && r != b.dir {
91+ *b.to = append(*b.to, r)
92+ }
83 }93 }
84 return sc94 return sc
85 }95 }
86 96
97+// apiSockIsOurs: an unjailed Firecracker's --api-sock must be a path below
98+// <state dir>/vm/<id>/ (compared lexically, after cleaning), the directory the
99+// driver puts it in. Another Firecracker with an id of our shape and a socket
100+// elsewhere is someone else's.
101+func (sc scope) apiSockIsOurs(sock, id string) bool {
102+ if sock == "" || !filepath.IsAbs(sock) {
103+ return false
104+ }
105+ sock = filepath.Clean(sock)
106+ for _, b := range sc.vmBases {
107+ if strings.HasPrefix(sock, filepath.Join(b, id)+"/") {
108+ return true
109+ }
110+ }
111+ return false
112+}
113+
87 func (sc scope) isBase(p string) bool {114 func (sc scope) isBase(p string) bool {
88 return slices.Contains(sc.jailBases, filepath.Clean(p))115 return slices.Contains(sc.jailBases, filepath.Clean(p))
89 }116 }
@@ -105,7 +132,8 @@ func (sc scope) under(p string) bool {
105 // below our jail base (rootIsOurs). Anything else with that flag and an id132 // below our jail base (rootIsOurs). Anything else with that flag and an id
106 // of our shape belongs to someone else's jailer and is never touched.133 // of our shape belongs to someone else's jailer and is never touched.
107 //134 //
108-// A Firecracker started with --api-sock (no jailer) is ours by its id alone.135+// A Firecracker started with --api-sock (no jailer) is ours when the socket is
136+// in <state dir>/vm/<id>/ (apiSockIsOurs).
109 func (sc scope) match(dir string, args []string) string {137 func (sc scope) match(dir string, args []string) string {
110 a := parseVMMArgs(args)138 a := parseVMMArgs(args)
111 if a.id == "" || len(a.markers) == 0 {139 if a.id == "" || len(a.markers) == 0 {
@@ -113,7 +141,9 @@ func (sc scope) match(dir string, args []string) string {
113 }141 }
114 switch {142 switch {
115 case a.markers[MarkerAPISock]:143 case a.markers[MarkerAPISock]:
116- return a.id144+ if sc.apiSockIsOurs(a.apiSock, a.id) {
145+ return a.id
146+ }
117 case a.markers[MarkerExecFile]:147 case a.markers[MarkerExecFile]:
118 if sc.isBase(a.chrootBase) {148 if sc.isBase(a.chrootBase) {
119 return a.id149 return a.id
M internal/sandbox/vm/vm_test.go +10 -3
@@ -652,8 +652,8 @@ func TestFirecrackerProcsScan(t *testing.T) {
652652 _ = os.MkdirAll(filepath.Join(proc, pid), 0o755)
653653 _ = os.WriteFile(filepath.Join(proc, pid, "cmdline"), []byte(cmdline), 0o644)
654654 }
655- mk("100", "firecracker\x00--id\x00er00112233\x00--api-sock\x00/x\x00")
656- mk("101", "/usr/bin/firecracker\x00--api-sock\x00/x\x00--id\x00er44556677\x00")
655+ mk("100", "firecracker\x00--id\x00er00112233\x00--api-sock\x00/var/lib/er/vm/er00112233/api.sock\x00")
656+ mk("101", "/usr/bin/firecracker\x00--api-sock\x00/var/lib/er/vm/er44556677/api.sock\x00--id\x00er44556677\x00")
657657 mk("102", "firecracker\x00--id\x00other-tool-vm\x00") // someone else's VM
658658 mk("103", "bash\x00--id\x00er00112233\x00") // not firecracker
659659 mk("1", "firecracker\x00--id\x00er99999999\x00") // pid 1 is never touched
@@ -1210,7 +1210,14 @@ func TestScanIsScopedToOurJailBase(t *testing.T) {
12101210 mk("105", "/", "jailer", "--id", "er22222222", "--exec-file", "/fc", "--chroot-base-dir", filepath.Join(foreign, "jail"))
12111211 mk("106", "/", "jailer", "--id", "er33333333", "--exec-file", "/fc")
12121212 mk("107", ours+"/../../../../x", "/firecracker", "--id", "er55555555", "--start-time-us", "1") // lexical escape
1213- mk("108", "/", "firecracker", "--id", "er66666666", "--api-sock", "/x") // unjailed: by id
1213+ vmDir := filepath.Join(state, "vm", "er66666666")
1214+ mk("108", "/", "firecracker", "--id", "er66666666", "--api-sock", filepath.Join(vmDir, "api.sock")) // unjailed, in our dir
1215+ mk("111", "/", "firecracker", "--id", "er99999999", "--api-sock", "/x") // socket elsewhere
1216+ mk("112", "/", "firecracker", "--id", "erabababab", "--api-sock", filepath.Join(vmDir, "api.sock")) // our dir, another id
1217+ mk("113", "/", "firecracker", "--id", "er66666666", "--api-sock", vmDir+"/../../x/api.sock") // escapes by ..
1218+ mk("114", "/", "firecracker", "--id", "er66666666", "--api-sock", "vm/er66666666/api.sock") // relative
1219+ mk("115", "/", "firecracker", "--id", "er66666666", "--api-sock") // no value
1220+ mk("116", "/", "firecracker", "--id", "er66666666", "--api-sock", vmDir+"x/api.sock") // prefix sibling
12141221 got := vmmProcs(proc, sc)
12151222 want := map[int]string{100: "er00112233", 104: "er11111111", 108: "er66666666", 109: "er00112233"}
12161223 if len(got) != len(want) {
@@ -652,8 +652,8 @@ func TestFirecrackerProcsScan(t *testing.T) {
652 _ = os.MkdirAll(filepath.Join(proc, pid), 0o755)652 _ = os.MkdirAll(filepath.Join(proc, pid), 0o755)
653 _ = os.WriteFile(filepath.Join(proc, pid, "cmdline"), []byte(cmdline), 0o644)653 _ = os.WriteFile(filepath.Join(proc, pid, "cmdline"), []byte(cmdline), 0o644)
654 }654 }
655- mk("100", "firecracker\x00--id\x00er00112233\x00--api-sock\x00/x\x00")655+ mk("100", "firecracker\x00--id\x00er00112233\x00--api-sock\x00/var/lib/er/vm/er00112233/api.sock\x00")
656- mk("101", "/usr/bin/firecracker\x00--api-sock\x00/x\x00--id\x00er44556677\x00")656+ mk("101", "/usr/bin/firecracker\x00--api-sock\x00/var/lib/er/vm/er44556677/api.sock\x00--id\x00er44556677\x00")
657 mk("102", "firecracker\x00--id\x00other-tool-vm\x00") // someone else's VM657 mk("102", "firecracker\x00--id\x00other-tool-vm\x00") // someone else's VM
658 mk("103", "bash\x00--id\x00er00112233\x00") // not firecracker658 mk("103", "bash\x00--id\x00er00112233\x00") // not firecracker
659 mk("1", "firecracker\x00--id\x00er99999999\x00") // pid 1 is never touched659 mk("1", "firecracker\x00--id\x00er99999999\x00") // pid 1 is never touched
@@ -1210,7 +1210,14 @@ func TestScanIsScopedToOurJailBase(t *testing.T) {
1210 mk("105", "/", "jailer", "--id", "er22222222", "--exec-file", "/fc", "--chroot-base-dir", filepath.Join(foreign, "jail"))1210 mk("105", "/", "jailer", "--id", "er22222222", "--exec-file", "/fc", "--chroot-base-dir", filepath.Join(foreign, "jail"))
1211 mk("106", "/", "jailer", "--id", "er33333333", "--exec-file", "/fc")1211 mk("106", "/", "jailer", "--id", "er33333333", "--exec-file", "/fc")
1212 mk("107", ours+"/../../../../x", "/firecracker", "--id", "er55555555", "--start-time-us", "1") // lexical escape1212 mk("107", ours+"/../../../../x", "/firecracker", "--id", "er55555555", "--start-time-us", "1") // lexical escape
1213- mk("108", "/", "firecracker", "--id", "er66666666", "--api-sock", "/x") // unjailed: by id1213+ vmDir := filepath.Join(state, "vm", "er66666666")
1214+ mk("108", "/", "firecracker", "--id", "er66666666", "--api-sock", filepath.Join(vmDir, "api.sock")) // unjailed, in our dir
1215+ mk("111", "/", "firecracker", "--id", "er99999999", "--api-sock", "/x") // socket elsewhere
1216+ mk("112", "/", "firecracker", "--id", "erabababab", "--api-sock", filepath.Join(vmDir, "api.sock")) // our dir, another id
1217+ mk("113", "/", "firecracker", "--id", "er66666666", "--api-sock", vmDir+"/../../x/api.sock") // escapes by ..
1218+ mk("114", "/", "firecracker", "--id", "er66666666", "--api-sock", "vm/er66666666/api.sock") // relative
1219+ mk("115", "/", "firecracker", "--id", "er66666666", "--api-sock") // no value
1220+ mk("116", "/", "firecracker", "--id", "er66666666", "--api-sock", vmDir+"x/api.sock") // prefix sibling
1214 got := vmmProcs(proc, sc)1221 got := vmmProcs(proc, sc)
1215 want := map[int]string{100: "er00112233", 104: "er11111111", 108: "er66666666", 109: "er00112233"}1222 want := map[int]string{100: "er00112233", 104: "er11111111", 108: "er66666666", 109: "er00112233"}
1216 if len(got) != len(want) {1223 if len(got) != len(want) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 4

Tags 11

↑↓ moveenter openesc close