From 1ba9452c409efb473282317eb8f4b4aa1ed7ad1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20Peliz=C3=A4us?= Date: Tue, 27 Jun 2023 12:25:03 +0200 Subject: [PATCH] lxd/instance/drivers/qemu: Pick a random vsock Context ID MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When acquiring a new Context ID for the communication via vsock, pick the first four bytes of the instances UUID (try to get as much randomness as possible out of the 16 bytes) and convert it into an uint32. If there is a collision, try again after adding +1 to the ID. The syscall to the vsock returns ENODEV in case the Context ID is not yet assigned. Fixes https://github.com/lxc/lxd/issues/11508 Signed-off-by: Julian Pelizäus --- lxd/instance/drivers/driver_qemu.go | 106 +++++++++++++++++++++++----- 1 file changed, 88 insertions(+), 18 deletions(-) diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index 61ed6449d1f3..d0593986a24d 100644 --- a/lxd/instance/drivers/driver_qemu.go +++ b/lxd/instance/drivers/driver_qemu.go @@ -10,6 +10,7 @@ import ( "crypto/x509" "database/sql" "encoding/base64" + "encoding/binary" "encoding/json" "errors" "fmt" @@ -24,6 +25,7 @@ import ( "sort" "strconv" "strings" + "syscall" "time" "github.com/flosch/pongo2" @@ -37,7 +39,7 @@ import ( "google.golang.org/protobuf/proto" "gopkg.in/yaml.v2" - "github.com/lxc/lxd/client" + lxd "github.com/lxc/lxd/client" agentAPI "github.com/lxc/lxd/lxd-agent/api" "github.com/lxc/lxd/lxd/apparmor" "github.com/lxc/lxd/lxd/cgroup" @@ -375,7 +377,11 @@ func (d *qemu) getAgentClient() (*http.Client, error) { return nil, err } - vsockID := d.vsockID() // Default to using the vsock ID that will be used on next start. + // Default to using the vsock ID that will be used on next start. + vsockID, err := d.vsockID() + if err != nil { + return nil, err + } // But if vsock ID from last VM start is present in volatile, then use that. // This allows a running VM to be recovered after DB record deletion and that agent connection still work @@ -1151,9 +1157,14 @@ func (d *qemu) start(stateful bool, op *operationlock.InstanceOperation) error { volatileSet := make(map[string]string) + vsockID, err := d.vsockID() + if err != nil { + return err + } + // Update vsock ID in volatile if needed for recovery (do this before UpdateBackupFile() call). oldVsockID := d.localConfig["volatile.vsock_id"] - newVsockID := strconv.Itoa(d.vsockID()) + newVsockID := strconv.Itoa(vsockID) if oldVsockID != newVsockID { volatileSet["volatile.vsock_id"] = newVsockID } @@ -2943,6 +2954,11 @@ func (d *qemu) generateQemuConfigFile(cpuInfo *cpuTopology, mountInfo *storagePo cfg = append(cfg, qemuTablet(&tabletOpts)...) + vsockID, err := d.vsockID() + if err != nil { + return "", nil, err + } + devBus, devAddr, multi = bus.allocate(busFunctionGroupGeneric) vsockOpts := qemuVsockOpts{ dev: qemuDevOpts{ @@ -2951,7 +2967,7 @@ func (d *qemu) generateQemuConfigFile(cpuInfo *cpuTopology, mountInfo *storagePo devAddr: devAddr, multifunction: multi, }, - vsockID: d.vsockID(), + vsockID: vsockID, } cfg = append(cfg, qemuVsock(&vsockOpts)...) @@ -7427,21 +7443,75 @@ func (d *qemu) DeviceEventHandler(runConf *deviceConfig.RunConfig) error { } // vsockID returns the vsock Context ID for the VM. -func (d *qemu) vsockID() int { - // We use the system's own VsockID as the base. - // - // This is either "2" for a physical system or the VM's own id if - // running inside of a VM. - // - // To this we add 1 for backward compatibility with prior logic - // which would start at id 3 rather than id 2. Removing that offset - // would cause conflicts between existing VMs until they're all rebooted. - // - // We then add the VM's own instance id (1 or higher) to give us a - // unique, non-clashing context ID for our guest. +func (d *qemu) vsockID() (int, error) { + // Return an already existing Context ID. + existingVsockID, ok := d.localConfig["volatile.vsock_id"] + if ok { + vsockID, err := strconv.Atoi(existingVsockID) + if err != nil { + return 0, fmt.Errorf("Failed to convert Context ID: %w", err) + } - info := DriverStatuses()[instancetype.VM].Info - return info.Features["vhost_vsock"].(int) + 1 + d.id + return vsockID, nil + } + + instanceUuid := uuid.Parse(d.localConfig["volatile.uuid"]) + if instanceUuid == nil { + return 0, fmt.Errorf("Instance does not have a UUID") + } + + // Pick the first 4 bytes of the random UUID (v4) to create an uint32 representation. + // Since the instances UUID already exist, we don't need to create another random value. + firstBytes := binary.LittleEndian.Uint32(instanceUuid[:4]) + + var i uint32 + var overflow bool + + // Try to find a new Context ID. + // Since the first guess might already be a collision (only 4 bytes out of 16), try again. + // If the candidate vsock ID overflows, it just starts all over but skipping 0-2. + for i = 0; (firstBytes+i >= firstBytes && !overflow) || firstBytes+i < firstBytes; i++ { + candidateVsockID := firstBytes + i + + if candidateVsockID == 0 { + overflow = true + } + + // Don't try to acquire the reserved Context IDs 0-2 + if candidateVsockID <= 2 { + continue + } + + c, err := lxdvsock.Dial(candidateVsockID, shared.HTTPSDefaultPort) + if err != nil { + opError, ok := err.(*net.OpError) + if !ok { + continue + } + + syscallError, ok := opError.Unwrap().(*os.SyscallError) + if !ok { + continue + } + + syscallErrno, ok := syscallError.Unwrap().(syscall.Errno) + if !ok { + continue + } + + // The syscall to the vsock device returned "no such device". + // This means the address (Context ID) is free. + if syscallErrno == syscall.ENODEV { + return int(candidateVsockID), nil + } + } else { + // Continue if the address is already in use. + c.Close() + continue + } + } + + return 0, fmt.Errorf("Failed to acquire the vsock Context ID") } // InitPID returns the instance's current process ID.