Skip to content

Commit

Permalink
lxd/instance/drivers/qemu: Pick a random vsock Context ID
Browse files Browse the repository at this point in the history
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 <[email protected]>
  • Loading branch information
roosterfish committed Jun 27, 2023
1 parent 4db7b8c commit 43dfdab
Showing 1 changed file with 88 additions and 18 deletions.
106 changes: 88 additions & 18 deletions lxd/instance/drivers/driver_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/x509"
"database/sql"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
Expand All @@ -24,6 +25,7 @@ import (
"sort"
"strconv"
"strings"
"syscall"
"time"

"github.com/flosch/pongo2"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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{
Expand All @@ -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)...)
Expand Down Expand Up @@ -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
}

if syscallErrno == syscall.ENODEV {
// The syscall to the vsock device returned "no such device".
// This means the address (Context ID) is free.
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.
Expand Down

0 comments on commit 43dfdab

Please sign in to comment.