Skip to content

fix: use-after-free on bbolt mmap-backed cache slice#696

Merged
brianmcgee merged 1 commit into
numtide:mainfrom
Enzime:push-qxrxymwxpkym
May 11, 2026
Merged

fix: use-after-free on bbolt mmap-backed cache slice#696
brianmcgee merged 1 commit into
numtide:mainfrom
Enzime:push-qxrxymwxpkym

Conversation

@Enzime
Copy link
Copy Markdown
Contributor

@Enzime Enzime commented May 10, 2026

I ran into the following crash:

unexpected fault address 0x12a659d7c
fatal error: fault
[signal SIGSEGV: segmentation violation code=0x2 addr=0x12a659d7c pc=0x100d51e0c]

goroutine 1 gp=0x3b09f7ca01e0 m=5 mp=0x3b09f7d80008 [running]:
runtime.throw({0x1010cbdef?, 0x30373431203332?})
        runtime/panic.go:1229 +0x38 fp=0x3b09f80ef590 sp=0x3b09f80ef560 pc=0x100dce258
runtime.sigpanic()
        runtime/signal_unix.go:945 +0x21c fp=0x3b09f80ef5f0 sp=0x3b09f80ef590 pc=0x100dcff7c
runtime.memequal()
        internal/bytealg/equal_arm64.s:64 +0x7c fp=0x3b09f80ef600 sp=0x3b09f80ef600 pc=0x100d51e0c
bytes.Equal(...)
        bytes/bytes.go:22
github.com/numtide/treefmt/v2/format.(*scheduler).submit(0x3b09f7e9bb80, {0x101504f58, 0x3b09f7dea6e0}, 0x3b09f83628c0, {0x3b09f8c14000, 0x4, 0x4})
        github.com/numtide/treefmt/v2/format/scheduler.go:114 +0x20c fp=0x3b09f80ef6d0 sp=0x3b09f80ef600 pc=0x101089c1c
github.com/numtide/treefmt/v2/format.(*CompositeFormatter).Apply(0x3b09f7e9bbc0, {0x101504f58, 0x3b09f7dea6e0}, {0x3b09f7ec0008, 0x400, 0x3b09f7da8a40?})
        github.com/numtide/treefmt/v2/format/composite.go:94 +0x278 fp=0x3b09f80ef800 sp=0x3b09f80ef6d0 pc=0x101087d18
github.com/numtide/treefmt/v2/cmd/format.Run(0x0?, 0x3b09f7db4940, 0x3b09f7e3fb50?, {0x3b09f7da7e90, 0x1, 0x3})
        github.com/numtide/treefmt/v2/cmd/format/format.go:152 +0x7a4 fp=0x3b09f80efac0 sp=0x3b09f80ef800 pc=0x10108b854
github.com/numtide/treefmt/v2/cmd.runE(0x3b09f7dd6780, 0x3b09f7db4940, 0x3b09f7e2c008, {0x3b09f7da7e90, 0x1, 0x3})
        github.com/numtide/treefmt/v2/cmd/root.go:183 +0x714 fp=0x3b09f80efc10 sp=0x3b09f80efac0 pc=0x10108cd54
github.com/numtide/treefmt/v2/cmd.NewRoot.func1(0x3b09f7e22100?, {0x3b09f7da7e90?, 0x7?, 0x1010cb677?})
        github.com/numtide/treefmt/v2/cmd/root.go:37 +0x3c fp=0x3b09f80efc50 sp=0x3b09f80efc10 pc=0x10108c60c
github.com/spf13/cobra.(*Command).execute(0x3b09f7e2c008, {0x3b09f7d92050, 0x3, 0x3})
        github.com/spf13/cobra@v1.10.2/command.go:1015 +0x814 fp=0x3b09f80efe00 sp=0x3b09f80efc50 pc=0x100f0eb04
github.com/spf13/cobra.(*Command).ExecuteC(0x3b09f7e2c008)
        github.com/spf13/cobra@v1.10.2/command.go:1148 +0x350 fp=0x3b09f80efef0 sp=0x3b09f80efe00 pc=0x100f0f260
github.com/spf13/cobra.(*Command).Execute(0x101521268?)
        github.com/spf13/cobra@v1.10.2/command.go:1071 +0x1c fp=0x3b09f80eff10 sp=0x3b09f80efef0 pc=0x100f0ee6c
main.main()
        github.com/numtide/treefmt/v2/main.go:12 +0x20 fp=0x3b09f80eff30 sp=0x3b09f80eff10 pc=0x10108cdc0
runtime.main()
        runtime/proc.go:290 +0x2b4 fp=0x3b09f80effd0 sp=0x3b09f80eff30 pc=0x100d9add4
runtime.goexit({})
        runtime/asm_arm64.s:1447 +0x4 fp=0x3b09f80effd0 sp=0x3b09f80effd0 pc=0x100dd5724

Full crash log

Here is a minimal reproducible example:

MRE
#!/usr/bin/env nix-shell
#!nix-shell -i bash --extra-experimental-features "nix-command flakes"
#!nix-shell -p "(builtins.getFlake \"github:numtide/treefmt\").packages.\${builtins.currentSystem}.default"

# Reproduces the bbolt mmap-backed cache-slice use-after-free in treefmt.
#
# By default the nix-shell shebang fetches treefmt from `github:numtide/treefmt`
# (latest main, which has the bug). To test against a different build, edit
# the shebang or bypass it with an outer wrapper, e.g.:
#
#   nix shell <flake-ref> --command bash /tmp/uaf-repro.sh
#
# Tunables (env):
#   NUM_FILES=20000          # number of dummy files to populate the cache with
#   MAX_ITERATIONS=5         # iterations of touch-all + treefmt before giving up
#
# Exit status:
#   0  reproduced the crash (non-zero exit from treefmt; signal name printed)
#   1  survived MAX_ITERATIONS without crashing — try larger NUM_FILES
#   2  setup failure (initial run failed for non-bug reasons, or no treefmt)

set -uo pipefail

NUM_FILES="${NUM_FILES:-20000}"
MAX_ITERATIONS="${MAX_ITERATIONS:-5}"

WORKDIR=$(mktemp -d -t treefmt-uaf-XXXXXX)
trap 'rm -rf "$WORKDIR"' EXIT
# Redirect XDG cache too so we don't pollute the real one.
export XDG_CACHE_HOME="$WORKDIR/cache"

cd "$WORKDIR"

echo "Generating $NUM_FILES files in $WORKDIR..." >&2
for i in $(seq 1 "$NUM_FILES"); do printf 'x\n' > "f_$i.txt"; done

cat > treefmt.toml <<'EOF'
[formatter.noop]
command = "true"
includes = ["*.txt"]
EOF

echo "Cold-cache populate run..." >&2
if ! treefmt >/dev/null 2>&1; then
    echo "Initial run failed unexpectedly" >&2
    exit 2
fi

echo "Looping (touch-all + treefmt) up to $MAX_ITERATIONS times..." >&2
for i in $(seq 1 "$MAX_ITERATIONS"); do
    printf '\rIteration %d/%d  ' "$i" "$MAX_ITERATIONS" >&2
    # Bump every mtime so cached signatures stop matching: every file goes back
    # through the scheduler and the cache writer goroutine flushes new entries
    # while bytes.Equal is still walking previously-loaded mmap-backed slices.
    find . -maxdepth 1 -name '*.txt' -exec touch {} +

    output=$(treefmt 2>&1)
    rc=$?
    if [ $rc -ne 0 ]; then
        echo >&2
        echo "treefmt exited rc=$rc on iteration $i" >&2
        # Go's runtime catches SIGSEGV, prints a panic, and exits with rc=2,
        # so the kernel-signal exit (rc>=128) path never fires. Detect it from
        # the output instead.
        if printf '%s\n' "$output" | grep -q "signal SIGSEGV"; then
            echo "Reproduced the use-after-free (SIGSEGV in Go panic)." >&2
        elif [ $rc -ge 128 ]; then
            echo "Killed by signal $((rc - 128)) (unexpected)" >&2
        fi
        echo "--- treefmt output (head -40) ---" >&2
        printf '%s\n' "$output" | head -40 >&2
        exit 0
    fi
done

echo >&2
echo "Survived $MAX_ITERATIONS iterations without crashing." >&2
exit 1

Possibly the cause of #548

CachedFormatSignature held a bucket.Get slice past its tx; a concurrent
db.Update grow unmaps and remaps the file so a later bytes.Equal in
scheduler.submit faulted.

Possibly the cause of numtide#548
Copy link
Copy Markdown
Collaborator

@jfly jfly left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice find and writeup!

@brianmcgee brianmcgee merged commit afce30d into numtide:main May 11, 2026
5 checks passed
@Enzime Enzime deleted the push-qxrxymwxpkym branch May 11, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants