CVE-2026-48750: Arbitrary File Write on Host via exec-output Symlink in Canonical LXD and Incus
Vulnerability ID: GHSA-9J25-MM2H-2F76
CVSS Score: 9.9
Published: 2026-11-06
Canonical LXD and LXC Incus contain an arbitrary file write vulnerability (CWE-73 / CWE-59) in the /1.0/instances/<name>/exec REST API endpoint. When an instance is unpacked from an image archive, top-level symbolic links are extracted directly onto the host filesystem within the instance storage directory. If a crafted image includes a symbolic link named exec-output pointing to an arbitrary host path, invoking the execution endpoint with record-output: true causes the privileged host daemon to follow the symlink and write command output files directly to the target host directory. An authenticated attacker with permissions to import images and execute commands can exploit this flaw to write controlled content to sensitive host directories such as /etc/cron.d, resulting in arbitrary code execution on the host as root.
TL;DR
An unsanitized symlink named exec-output inside a crafted container image allows authenticated LXD and Incus users to write arbitrary command output to any directory on the host filesystem when invoking the /exec endpoint with record-output: true, leading to host-level privilege escalation and arbitrary code execution as root.
⚠️ Exploit Status: POC
Technical Details
- CVE Identifier: CVE-2026-48750 (GHSA-9j25-mm2h-2f76)
- Weakness Class: CWE-73: External Control of File Name or Path / CWE-59: Symlink Following
- CVSS v3.1 Score: 9.9 Critical (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H)
- EPSS Score: 0.78% (56.22nd Percentile)
- Attack Vector: Authenticated REST API (/1.0/instances//exec with record-output: true)
- Security Impact: Arbitrary file creation on host leading to root code execution (Container Escape)
- Exploit Maturity: Public Proof-of-Concept (PoC) Available
- CISA KEV Status: Not Listed
Affected Systems
- Canonical LXD
- LXC Incus
- LXD (4.0 LTS): >= 4.0.0, < 4.0.11 (Fixed in:
4.0.11) - LXD (5.0 LTS): >= 5.0.0, < 5.0.7 (Fixed in:
5.0.7) - LXD (5.x / 5.21 LTS): >= 5.1.0, < 5.21.5 (Fixed in:
5.21.5) - LXD (6.x): >= 6.0.0, < 6.9.0 (Fixed in:
6.9.0) - Incus: < 7.2.0 (Fixed in:
7.2.0)
Code Analysis
Commit: 124f901
lxd/instance_exec: Confine opening of exec outputs
--- a/lxd/instance_exec.go +++ b/lxd/instance_exec.go @@ -718,24 +716,24 @@ func instanceExecPost(d *Daemon, r *http.Request) response.Response { var stdout, stderr *os.File if post.RecordOutput { - // Ensure exec-output directory exists - execOutputDir := inst.ExecOutputPath() - err = os.Mkdir(execOutputDir, 0600) - if err != nil && !errors.Is(err, fs.ErrExist) { + execOutputRoot, err := inst.OpenExecOutput() + if err != nil { return err } + defer func() { _ = execOutputRoot.Close() }() + // Prepare stdout and stderr recording. stdoutFile := "exec_" + opID + ".stdout" - stdout, err = os.OpenFile(filepath.Join(execOutputDir, stdoutFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + stdout, err = execOutputRoot.OpenFile(stdoutFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { return err } defer func() { _ = stdout.Close() }() stderrFile := "exec_" + opID + ".stderr" - stderr, err = os.OpenFile(filepath.Join(execOutputDir, stderrFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) + stderr, err = execOutputRoot.OpenFile(stderrFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666) if err != nil { return err } Commit: 254b85c
lxd/instance/drivers/common: Helper to ensure opening of instance paths is confined
--- a/lxd/instance/drivers/driver_common.go +++ b/lxd/instance/drivers/driver_common.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "io/fs" "net/http" "os" "path/filepath" @@ -396,6 +397,64 @@ func (d *common) Path() string { return storagePools.InstancePath(d.dbType, d.project.Name, d.name, d.isSnapshot) } +// openSubPath opens name as a confined [os.Root] beneath [common.Path], rejecting any attempt to +// escape the instance path. +func (d *common) openSubPath(name string) (*os.Root, error) { + parent, err := os.OpenRoot(d.Path()) + if err != nil { + return nil, err + } + + defer func() { _ = parent.Close() }() + + root, err := parent.OpenRoot(name) + if err != nil { + return nil, fmt.Errorf("Failed opening %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err) + } + + return root, nil +} + +// openOrCreateSubPath is like openSubPath but also creates the name if it does not exist. +func (d *common) openOrCreateSubPath(name string, mode os.FileMode) (*os.Root, error) { + parent, err := os.OpenRoot(d.Path()) + if err != nil { + return nil, err + } + + defer func() { _ = parent.Close() }() + + err = parent.Mkdir(name, mode) + if err != nil && !errors.Is(err, fs.ErrExist) { + return nil, fmt.Errorf("Failed creating %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err) + } + + root, err := parent.OpenRoot(name) + if err != nil { + return nil, fmt.Errorf("Failed opening %q for instance %q (path %s): %w", name, d.Name(), d.Path(), err) + } + + return root, nil +} + +// OpenRootfs opens the instance's rootfs directory as a confined *os.Root. +// Caller must close the returned Root. +func (d *common) OpenRootfs() (*os.Root, error) { + return d.openSubPath("rootfs") +} + +// OpenExecOutput opens the instance's exec-output directory as a confined *os.Root, creating it if absent. +// Caller must close the returned Root. +func (d *common) OpenExecOutput() (*os.Root, error) { + return d.openOrCreateSubPath("exec-output", 0700) +} + +// OpenTemplates opens the instance's templates directory as a confined *os.Root, creating it if absent. +// Caller must close the returned Root. +func (d *common) OpenTemplates() (*os.Root, error) { + return d.openOrCreateSubPath("templates", 0700) +} + // ExecOutputPath returns the instance's exec output path. func (d *common) ExecOutputPath() string { return filepath.Join(d.Path(), "exec-output") Commit: 4cd6334
lxd/instance/drivers/common: Helper to ensure opening of instance paths is confined (cherry-pick)
Commit: 937b761
lxd/instance_exec: Confine opening of exec outputs (cherry-pick)
Commit: bb0f252
lxd/instance_exec: Confine opening of exec outputs (cherry-pick)
Exploit Details
- GitHub Security Advisory (Canonical LXD): Official security advisory containing a shell script PoC demonstrating image repacking with an exec-output symlink and triggering file creation via the REST API.
- GitHub Security Advisory (LXC Incus): Upstream Incus security advisory with full reproduction steps and PoC script.
Mitigation Strategies
- Upgrade Canonical LXD or LXC Incus to the patched release corresponding to your deployment branch.
- Restrict access to the LXD/Incus UNIX control socket and TLS REST API endpoints to trusted host administrators.
- Audit instance directories across all storage pools for symbolic links named exec-output.
- Monitor host configuration directories (such as /etc/cron.d and /etc/profile.d) for files matching the pattern exec_.stdout or exec_.stderr.
Remediation Steps:
- Identify the currently running LXD or Incus version using
lxd --versionorincus --version. - For Snap-based LXD deployments, run
sudo snap refresh lxdto update to version 5.21.5, 5.0.7, 4.0.11, or 6.9.0+. - For Incus deployments, upgrade the
incuspackage via your distribution package manager to version 7.2.0 or higher. - Verify that no existing container directories contain symbolic links for
exec-outputusingfind /var/lib/lxd/containers/ -maxdepth 2 -name "exec-output" -type l -ls.
References
- GitHub Security Advisory GHSA-9j25-mm2h-2f76 (Canonical LXD)
- Incus Security Advisory GHSA-73hr-m85f-64v9
- Incus Reporter Advisory GHSA-48q5-w887-33wv
- National Vulnerability Database (NVD) Entry for CVE-2026-48750
- CVE.org Record for CVE-2026-48750
- Canonical LXD Release Tag lxd-5.21.5
- Upstream Canonical LXD Repository
- Patch Commit: lxd/instance_exec: Confine opening of exec outputs
- Patch Commit: lxd/instance/drivers/common: Helper to ensure opening of instance paths is confined
- Incus 7.2 Release Announcement
- Debian Cron(8) Manual Page
Read the full report for GHSA-9J25-MM2H-2F76 on our website for more details including interactive diagrams and full exploit analysis.
