diff --git a/lxd/instance/drivers/driver_qemu.go b/lxd/instance/drivers/driver_qemu.go index 61ed6449d1f3..5e38fe5cae01 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,62 @@ 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) + } + return vsockID, nil + } - info := DriverStatuses()[instancetype.VM].Info - return info.Features["vhost_vsock"].(int) + 1 + d.id + 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]) + + // Try to find a new Context ID. + // Since the first guess might already be a collission (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; i < 1000; i++ { + candidateVsockID := firstBytes + uint32(i) + + // 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 { + syscallError, ok := opError.Unwrap().(*os.SyscallError) + if ok { + syscallErrno, ok := syscallError.Unwrap().(syscall.Errno) + if ok { + // 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 { + c.Close() + } + + // Continue if the error is not ENODEV or the address is already in use. + continue + } + + return 0, fmt.Errorf("Failed to acquire the vsock Context ID") } // InitPID returns the instance's current process ID.