Skip to content

Commit d7fba22

Browse files
committed
Tighten QEMU launch prerequisites
1 parent 47b6e51 commit d7fba22

3 files changed

Lines changed: 60 additions & 14 deletions

File tree

lib/hypervisor/cloudhypervisor/cloudhypervisor.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,11 @@ func (c *CloudHypervisor) Capabilities() hypervisor.Capabilities {
4848
return capabilities()
4949
}
5050

51-
// capabilities resolves the effective capability set from the configured
52-
// default version (see GetDefaultVersion), matching what the capability
53-
// registry reports, rather than the compile-time vmm.DefaultVersion.
51+
// capabilities preserves the legacy client behavior for clients that do not
52+
// carry instance-version metadata. The runtime registry separately resolves
53+
// capabilities for the configured default used by new launches.
5454
func capabilities() hypervisor.Capabilities {
55-
return CapabilitiesForVersion(GetDefaultVersion())
55+
return CapabilitiesForVersion(vmm.DefaultVersion)
5656
}
5757

5858
// CapabilitiesForVersion returns capabilities for a specific CH version.

lib/hypervisor/qemu/register_linux.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ func checkLaunchPrerequisites() error {
7272
// (versionFromBinary), because every cold start persists ResolveVersion's
7373
// result and treats failure as fatal — a non-executable or broken binary
7474
// accepted by GetBinaryPath's bare os.Stat must not report available;
75-
// - the vhost-vsock host device must exist, because every created instance
76-
// receives a nonzero vsock CID and buildArgs unconditionally attaches a
77-
// vhost-vsock device for it.
75+
// - the vhost-vsock host device must be a character device that this process
76+
// can open read/write, because every created instance receives a nonzero
77+
// vsock CID and buildArgs unconditionally attaches a vhost-vsock device.
7878
//
7979
// Split from checkLaunchPrerequisites so unavailable cases are testable with
8080
// fake binaries and device paths regardless of the host's QEMU install.
@@ -85,8 +85,23 @@ func checkLaunchPrerequisitesFor(ctx context.Context, binaryPath, vsockDevicePat
8585
if _, err := versionFromBinary(ctx, binaryPath); err != nil {
8686
return fmt.Errorf("qemu binary %s is not usable: %w", binaryPath, err)
8787
}
88-
if _, err := os.Stat(vsockDevicePath); err != nil {
89-
return fmt.Errorf("vsock device %s is required for instance launches (load the vhost_vsock kernel module): %w", vsockDevicePath, err)
88+
return validateVsockDevice(vsockDevicePath, os.OpenFile)
89+
}
90+
91+
func validateVsockDevice(path string, openFile func(string, int, os.FileMode) (*os.File, error)) error {
92+
info, err := os.Stat(path)
93+
if err != nil {
94+
return fmt.Errorf("vsock device %s is required for instance launches (load the vhost_vsock kernel module): %w", path, err)
95+
}
96+
if info.Mode().Type() != os.ModeDevice|os.ModeCharDevice {
97+
return fmt.Errorf("vsock device %s must be a character device (load the vhost_vsock kernel module)", path)
98+
}
99+
device, err := openFile(path, os.O_RDWR, 0)
100+
if err != nil {
101+
return fmt.Errorf("vsock device %s is not accessible read/write for instance launches: %w", path, err)
102+
}
103+
if err := device.Close(); err != nil {
104+
return fmt.Errorf("close vsock device %s after launch check: %w", path, err)
90105
}
91106
return nil
92107
}

lib/hypervisor/qemu/register_linux_test.go

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ func fakeQEMUBinary(t *testing.T, dir string) string {
7676
return path
7777
}
7878

79-
// fakeVsockDevice creates a stand-in for /dev/vhost-vsock: the prerequisite
80-
// check only requires the device node to exist.
81-
func fakeVsockDevice(t *testing.T, dir string) string {
79+
// fakeVsockDevice uses a harmless character device as a stand-in for
80+
// /dev/vhost-vsock. The prerequisite check verifies node type and O_RDWR
81+
// access; it deliberately does not issue vhost ioctls during diagnostics.
82+
func fakeVsockDevice(t *testing.T, _ string) string {
8283
t.Helper()
83-
path := filepath.Join(dir, "vhost-vsock")
84-
require.NoError(t, os.WriteFile(path, nil, 0o600))
84+
const path = "/dev/null"
85+
info, err := os.Stat(path)
86+
require.NoError(t, err)
87+
require.Equal(t, os.ModeDevice|os.ModeCharDevice, info.Mode().Type())
8588
return path
8689
}
8790

@@ -149,6 +152,34 @@ func TestCheckLaunchPrerequisitesFor(t *testing.T) {
149152
require.ErrorContains(t, err, "vhost_vsock")
150153
})
151154

155+
t.Run("regular file is not a vsock device", func(t *testing.T) {
156+
dir := t.TempDir()
157+
binary := fakeQEMUBinary(t, dir)
158+
path := filepath.Join(dir, "vhost-vsock")
159+
require.NoError(t, os.WriteFile(path, nil, 0o600))
160+
err := retryingETXTBSY(t, func() error {
161+
return checkLaunchPrerequisitesFor(ctx, binary, path)
162+
})
163+
require.ErrorContains(t, err, "must be a character device")
164+
})
165+
166+
t.Run("directory is not a vsock device", func(t *testing.T) {
167+
dir := t.TempDir()
168+
binary := fakeQEMUBinary(t, dir)
169+
err := retryingETXTBSY(t, func() error {
170+
return checkLaunchPrerequisitesFor(ctx, binary, dir)
171+
})
172+
require.ErrorContains(t, err, "must be a character device")
173+
})
174+
175+
t.Run("inaccessible character device fails", func(t *testing.T) {
176+
err := validateVsockDevice("/dev/null", func(string, int, os.FileMode) (*os.File, error) {
177+
return nil, os.ErrPermission
178+
})
179+
require.ErrorContains(t, err, "not accessible read/write")
180+
require.ErrorIs(t, err, os.ErrPermission)
181+
})
182+
152183
t.Run("hung binary fails at the context deadline", func(t *testing.T) {
153184
// A wedged QEMU binary must fail the prerequisite check when its
154185
// bounded context expires instead of blocking the capability request

0 commit comments

Comments
 (0)