Incus Escape: From Templates to Host Root
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/realrootStep 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.tplStep 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:
- Arbitrary File Write: As demonstrated, we can overwrite critical system files (
/etc/shadow,/etc/ld.so.preload,/usr/bin/*). - 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.
Official Patches
Technical Appendix
CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
Incus Linux Containers | <= 6.0.5 | 6.0.6 |
Incus Linux Containers | <= 6.20.0 | 6.21.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-22 (Path Traversal) |
| CVSS v3.1 | 8.7 (High) |
| Attack Vector | Adjacent Network (AV:A) |
| Impact | Confidentiality High, Integrity High |
| Exploit Status | PoC Available |
| Prerequisites | Container launch permissions (incus group) |
MITRE ATT&CK Mapping
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.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.