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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) | |||
| 126 | 126 | if err := os.MkdirAll(jailBase(cfg.StateDir), 0o700); err != nil { | |
| 127 | 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 | 134 | for _, p := range []string{cfg.KernelPath(), cfg.BaseImagePath()} { | |
| 130 | 135 | if err := checkWorldReadable(p); err != nil { | |
| 131 | 136 | return fail(err) | |
| @@ -395,6 +400,9 @@ func (s *vmSandbox) bootVsock() (<-chan acceptResult, error) { | |||
| 395 | 400 | ||
| 396 | 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 | 406 | func (s *vmSandbox) vmConfig(spec sandbox.JobSpec, netw netpolicy.Net, f vmFiles) vmConfig { | |
| 399 | 407 | cfg := s.d.cfg | |
| 400 | 408 | 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.cfg | 407 | 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) { | |||
| 1029 | 1029 | e := newEnv(t, "ok") | |
| 1030 | 1030 | e.cfg.JailerOff = false | |
| 1031 | 1031 | 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 | 1038 | d, err := New(context.Background(), e.cfg, Options{Run: e.run, Net: e.plumb, BootTimeout: 5 * time.Second}) | |
| 1033 | 1039 | if err != nil { | |
| 1034 | 1040 | t.Fatal(err) | |
| @@ -1039,3 +1045,14 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) { | |||
| 1039 | 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 | + | } | |
| @@ -1029,6 +1029,12 @@ func TestNewCreatesTheJailerChrootBase(t *testing.T) { | |||||
| 1029 | e := newEnv(t, "ok") | 1029 | e := newEnv(t, "ok") | ||
| 1030 | e.cfg.JailerOff = false | 1030 | e.cfg.JailerOff = false | ||
| 1031 | e.cfg.Jailer = e.cfg.Firecracker // any executable: it is only resolved here | 1031 | 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 | + | } | |||