CVE-2026-23954

Incus Escape: From Templates to Host Root

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 23, 2026·6 min read·3 visits

Executive Summary (TL;DR)

Incus (an LXD fork) trusted user-supplied paths in container image templates too much. By crafting a malicious image with a specific `metadata.yaml`, an attacker can trick the root-privileged Incus daemon into reading or writing files outside the container rootfs. This is trivially exploitable for host-level Remote Code Execution (RCE).

A critical path traversal and symbolic link vulnerability in the Incus container manager allows privileged container users to escape confinement and execute arbitrary code as root on the host system.

The Hook: Trust Issues in Container Land

In the world of system containers, we often conflate "containment" with "security boundary." Incus, the community-driven fork of LXD, is a powerful tool for managing system containers that feel like full virtual machines. One of its creature comforts is image templating. When you launch a container, Incus doesn't just unpack a tarball; it looks at a metadata.yaml file to perform last-mile customization. It might update /etc/hosts, set a hostname, or inject cloud-init configurations.

Here is the catch: The Incus daemon performs these file operations. The Incus daemon runs as root. And the instructions for what to write and where to write it come from the container image itself.

This architecture creates a classic "Confused Deputy" scenario. The daemon is helpful, privileged, and obedient. If the image asks it to update a file, it does so. The security of the entire host relies on the daemon strictly checking that it isn't being tricked into writing outside the container's sandbox. In CVE-2026-23954, those checks failed spectacularly.

The Flaw: A Tale of Two Paths

The vulnerability lies in internal/server/instance/drivers/driver_lxc.go, specifically within the templateApplyNow function. This function is responsible for iterating through the templates defined in an image's metadata and applying them.

Root Cause Analysis reveals a classic Path Traversal (CWE-22) combined with Improper Link Resolution. When processing a template, the code takes a source file (from the image) and copies it to a target path (inside the container). The logic relied on standard string manipulation to determine these paths.

The fatal error was assuming that filepath.Join is sufficient security. While filepath.Join cleans up dirty paths (resolving ../), it operates purely on the lexical path string. It does not check the physical reality of the filesystem. If an attacker places a symbolic link inside the container image that points to the host's root (/), filepath.Join sees a valid path inside the container, but the operating system follows the symlink out to the host filesystem during the file write operation.

The Code: The Smoking Gun

Let's look at the vulnerable Go code vs. the secure implementation. This highlights why modern file system APIs are critical for security.

The Vulnerable Code: The original code constructed the full path by joining the container's rootfs path with the target template path. It then handed this string directly to os.Create.

// Vulnerable logic in driver_lxc.go
// d.RootfsPath() is the container's safe jail (e.g., /var/lib/incus/containers/foo/rootfs)
// tplPath comes from the attacker-controlled metadata.yaml
 
fullpath := filepath.Join(d.RootfsPath(), strings.TrimLeft(tplPath, "/"))
 
// CRITICAL FLAW: If a component of tplPath is a symlink to /, 
// os.Create follows it out of the jail.
w, err = os.Create(fullpath)

The Fix (Go 1.24 Magic): The patch leverages a new feature in Go 1.24: os.Root. This provides a capability-based file system handle. Instead of operating on global strings, we open a "root" handle to the container directory. Any operations performed relative to this handle are enforced by the OS (using mechanisms like openat2 with RESOLVE_BENEATH) to ensure they cannot escape that directory tree, regardless of symlinks or ../ trickery.

// Patched logic
rootPath, err := os.OpenRoot(d.RootfsPath())
if err != nil {
    return err
}
defer rootPath.Close()
 
// We strip the leading slash but use the root handle to create the file.
// If relPath traverses out via symlinks, this call explicitly fails.
relPath := strings.TrimLeft(tplPath, "/")
w, err = rootPath.Create(relPath)

The Exploit: Escaping the Jail

To exploit this, we don't need memory corruption or complex heap spraying. We just need to be able to import a custom image. This is a privilege usually granted to the incus group.

Step 1: The Trap (Symbolic Link) We create a malicious root filesystem containing a symlink that points to the host's root directory. Inside our image tarball:

ln -s / rootfs/realroot

Step 2: The Map (metadata.yaml) We craft the metadata.yaml to define a template action. We tell Incus: "Hey, when you start this container, please take this innocent file and write it to /realroot/proc/sys/kernel/core_pattern."

Because of the symlink we created in Step 1, /realroot/ actually resolves to the host's root directory. Incus thinks it is writing to /var/lib/incus/containers/poc/rootfs/realroot/..., but the kernel redirects the write to /proc/sys/kernel/core_pattern on the host.

Step 3: The Payload (RCE) We overwrite core_pattern to execute a shell script whenever a program crashes. This is a standard container escape technique.

templates:
  # The exploit target: Host's core_pattern
  /realroot/proc/sys/kernel/core_pattern:
    when: [start]
    template: payload.tpl

Step 4: Execution We import the image and launch it. The moment the container starts, the Incus daemon processes the template, traversing our symlink and overwriting the host's kernel configuration. We then crash a process (e.g., sleep 10 & kill -SIGSEGV %1), causing the kernel to trigger our payload as root.

The Impact: Game Over

The impact here is Critical (CVSS 8.7). While the score is slightly tempered because you need initial access to the incus group (or similar privileges to create images), the result is total system compromise.

This vulnerability allows for:

  1. Arbitrary File Write: As demonstrated, we can overwrite critical system files (/etc/shadow, /etc/ld.so.preload, /usr/bin/*).
  2. Arbitrary File Read: By reversing the logic, we can define a template source using ../ traversal (e.g., template: ../../../../../etc/shadow). Incus will read the host file and copy it into our container, allowing us to steal password hashes or SSH keys.

This effectively blurs the line between "container admin" and "host admin." If you run a multi-tenant environment where users are allowed to bring their own images, you are vulnerable.

The Fix: Remediation Strategies

The fix was released in Incus 6.21.0 (Feature release) and Incus 6.0.6 (LTS release). The primary mitigation is to upgrade immediately.

If you cannot upgrade, you must restrict access to the incus group. Treat any user with incus group membership as if they already have root access to the host—because, with this exploit, they effectively do. Additionally, avoid importing images from untrusted sources or public remotes that you do not control.

For developers, the lesson is clear: Never trust filepath strings. If you are writing file manipulation code in Go, upgrade to 1.24 and use os.OpenRoot for any directory-constrained operations. The filesystem state is volatile; checking a path string before use is a Time-of-Check Time-of-Use (TOCTOU) race condition waiting to happen. Let the OS kernel enforce the boundaries via file descriptors.

Technical Appendix

CVSS Score
8.7/ 10
CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N

Affected Systems

Incus Container Manager (Feature release <= 6.20)Incus Container Manager (LTS release <= 6.0.5)Linux Systems running vulnerable Incus versions

Affected Versions Detail

Product
Affected Versions
Fixed Version
Incus
Linux Containers
<= 6.0.56.0.6
Incus
Linux Containers
<= 6.20.06.21.0
AttributeDetail
CWE IDCWE-22 (Path Traversal)
CVSS v3.18.7 (High)
Attack VectorAdjacent Network (AV:A)
ImpactConfidentiality High, Integrity High
Exploit StatusPoC Available
PrerequisitesContainer launch permissions (incus group)
CWE-22
Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')

The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.

Vulnerability Timeline

Vulnerability Disclosed
2026-01-22
CVE-2026-23954 Assigned
2026-01-22
Patch Released in Incus 6.21.0
2026-01-22

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.