detached ea344ad

fix(vm): refuse a state directory whose jailed API socket path exceeds sun_path

The jailed API socket is <state>/jail/firecracker/<id>/root/run/firecracker.socket.
Over 107 bytes the dial fails and the boot reports a 10 s 'API socket not
ready' timeout. Check the longest path at driver start and name the setting
to shorten, like the container driver does.
commit
ea344ada221614c95e39c6e2fe24fb700aedfd3d
author
brice · · 2026-10-12 21:16 UTC
parent
ed21f4f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +25 -0
M internal/sandbox/vm/driver.go +8 -0
@@ -126,6 +126,11 @@ func New(ctx context.Context, cfg sandbox.Config, opt Options) (*Driver, error)
126126 if err := os.MkdirAll(jailBase(cfg.StateDir), 0o700); err != nil {
127127 return fail(fmt.Errorf("jailer chroot base: %w", err))
128128 }
129+ // The API socket lives deep in the jail; a path over sun_path fails
130+ // as a 10 s "API not ready" timeout otherwise.
131+ if sock := newLayout(cfg.StateDir, filepath.Base(d.fcPath), "er00000000", true, 0).host("run/firecracker.socket"); len(sock) > maxSockPath {
132+ return fail(fmt.Errorf("firecracker API socket path %q is %d bytes, over the %d-byte limit of a Unix socket: use a shorter %s", sock, len(sock), maxSockPath, sandbox.EnvStateDir))
133+ }
129134 for _, p := range []string{cfg.KernelPath(), cfg.BaseImagePath()} {
130135 if err := checkWorldReadable(p); err != nil {
131136 return fail(err)
@@ -395,6 +400,9 @@ func (s *vmSandbox) bootVsock() (<-chan acceptResult, error) {
395400
396401 const udsName = "v.sock"
397402
403+// maxSockPath is the longest Unix socket path Linux accepts (108 with the NUL).
404+const maxSockPath = 107
405+
398406 func (s *vmSandbox) vmConfig(spec sandbox.JobSpec, netw netpolicy.Net, f vmFiles) vmConfig {
399407 cfg := s.d.cfg
400408 return vmConfig{
@@ -126,6 +126,11 @@ func New(ctx context.Context, cfg sandbox.Config, opt Options) (*Driver, error)
126 if err := os.MkdirAll(jailBase(cfg.StateDir), 0o700); err != nil {126 if err := os.MkdirAll(jailBase(cfg.StateDir), 0o700); err != nil {
127 return fail(fmt.Errorf("jailer chroot base: %w", err))127 return fail(fmt.Errorf("jailer chroot base: %w", err))
128 }128 }
129+ // The API socket lives deep in the jail; a path over sun_path fails
130+ // as a 10 s "API not ready" timeout otherwise.
131+ if sock := newLayout(cfg.StateDir, filepath.Base(d.fcPath), "er00000000", true, 0).host("run/firecracker.socket"); len(sock) > maxSockPath {
132+ return fail(fmt.Errorf("firecracker API socket path %q is %d bytes, over the %d-byte limit of a Unix socket: use a shorter %s", sock, len(sock), maxSockPath, sandbox.EnvStateDir))
133+ }
129 for _, p := range []string{cfg.KernelPath(), cfg.BaseImagePath()} {134 for _, p := range []string{cfg.KernelPath(), cfg.BaseImagePath()} {
130 if err := checkWorldReadable(p); err != nil {135 if err := checkWorldReadable(p); err != nil {
131 return fail(err)136 return fail(err)
@@ -395,6 +400,9 @@ func (s *vmSandbox) bootVsock() (<-chan acceptResult, error) {
395 400
396 const udsName = "v.sock"401 const udsName = "v.sock"
397 402
403+// maxSockPath is the longest Unix socket path Linux accepts (108 with the NUL).
404+const maxSockPath = 107
405+
398 func (s *vmSandbox) vmConfig(spec sandbox.JobSpec, netw netpolicy.Net, f vmFiles) vmConfig {406 func (s *vmSandbox) vmConfig(spec sandbox.JobSpec, netw netpolicy.Net, f vmFiles) vmConfig {
399 cfg := s.d.cfg407 cfg := s.d.cfg
400 return vmConfig{408 return vmConfig{
M internal/sandbox/vm/vm_test.go +17 -0
@@ -1029,6 +1029,12 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) {
10291029 e := newEnv(t, "ok")
10301030 e.cfg.JailerOff = false
10311031 e.cfg.Jailer = e.cfg.Firecracker // any executable: it is only resolved here
1032+ short, err := os.MkdirTemp("", "er")
1033+ if err != nil {
1034+ t.Fatal(err)
1035+ }
1036+ defer func() { _ = os.RemoveAll(short) }()
1037+ e.cfg.StateDir = filepath.Join(short, "s") // the jailed API socket path must fit in sun_path
10321038 d, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second})
10331039 if err != nil {
10341040 t.Fatal(err)
@@ -1039,3 +1045,14 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) {
10391045 t.Fatalf("jail base not created: %v", err)
10401046 }
10411047 }
1048+
1049+func TestNewRejectsAStateDirThatMakesTheAPISocketPathTooLong(t *testing.T) {
1050+ e := newEnv(t, "ok")
1051+ e.cfg.JailerOff = false
1052+ e.cfg.Jailer = e.cfg.Firecracker
1053+ e.cfg.StateDir = filepath.Join(e.cfg.StateDir, strings.Repeat("d", 60))
1054+ _, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second})
1055+ if err == nil || !strings.Contains(err.Error(), "limit of a Unix socket") {
1056+ t.Fatalf("want a clear socket path error, got %v", err)
1057+ }
1058+}
@@ -1029,6 +1029,12 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) {
1029 e := newEnv(t, "ok")1029 e := newEnv(t, "ok")
1030 e.cfg.JailerOff = false1030 e.cfg.JailerOff = false
1031 e.cfg.Jailer = e.cfg.Firecracker // any executable: it is only resolved here1031 e.cfg.Jailer = e.cfg.Firecracker // any executable: it is only resolved here
1032+ short, err := os.MkdirTemp("", "er")
1033+ if err != nil {
1034+ t.Fatal(err)
1035+ }
1036+ defer func() { _ = os.RemoveAll(short) }()
1037+ e.cfg.StateDir = filepath.Join(short, "s") // the jailed API socket path must fit in sun_path
1032 d, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second})1038 d, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second})
1033 if err != nil {1039 if err != nil {
1034 t.Fatal(err)1040 t.Fatal(err)
@@ -1039,3 +1045,14 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) {
1039 t.Fatalf("jail base not created: %v", err)1045 t.Fatalf("jail base not created: %v", err)
1040 }1046 }
1041 }1047 }
1048+
1049+func TestNewRejectsAStateDirThatMakesTheAPISocketPathTooLong(t *testing.T) {
1050+ e := newEnv(t, "ok")
1051+ e.cfg.JailerOff = false
1052+ e.cfg.Jailer = e.cfg.Firecracker
1053+ e.cfg.StateDir = filepath.Join(e.cfg.StateDir, strings.Repeat("d", 60))
1054+ _, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second})
1055+ if err == nil || !strings.Contains(err.Error(), "limit of a Unix socket") {
1056+ t.Fatalf("want a clear socket path error, got %v", err)
1057+ }
1058+}

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