CVE-2021-3487: OOB read in binutils readelf fetch_indexed_string (DWARF .debug_str_offsets)
4ba8a9f8-0ed1-4cd9-b70f-63b578d80043
CVE-2021-3487 — out-of-bounds read in GNU binutils readelf when processing DWARF debug info from a malformed .debug_str_offsets section. In binutils/dwarf.c, fetch_indexed_string() trusts the attacker-controlled length field at the head of .debug_str_offsets and fails to validate it against the real section bounds.
Two bugs combine: (1) the table-size guard at lines 773-774 uses reversed comparison logic — curr + length < (end - 8) warns only when the table is too small, so a length larger than the actual section silently passes; (2) the index check at line 788 is index_offset >= length, validated against the bogus length rather than against the section size.
Then at line 796 the code calls byte_get(curr + index_offset, offset_size) directly — byte_get does NOT do bounds checking, while the file's own SAFE_BYTE_GET macro does. The result: readelf reads offset_size bytes from a pointer that may sit far past the mapped section, an OOB read of process memory. The leaked bytes become str_offset, then str_offset -= str_section->address is unsigned subtraction that can wrap, and the wrapped value is used to index str_section->start for the printed string — potentially dereferencing arbitrary memory.
lengthread via SAFE_BYTE_GET_AND_INC at 746/751 — attacker-controlled.- Lines 773-774 guard
if ((offset_size==4 && curr+length < end-8) || ...)—<is reversed; should be>. The accompanying warn() text "index table size is too small" confirms intent vs behavior mismatch. - Line 788
if (index_offset >= length)validates against attacker-controlled length, not real section size. - Line 796:
str_offset = byte_get (curr + index_offset, offset_size);— byte_get is unchecked. SAFE_BYTE_GET (line 381) does check(PTR + amount) >= (END). Wrong macro chosen.
- Cross-checked bfd/dwarf2.c read_section (519) — does check
offset >= *section_size, so bug is specifically in binutils/dwarf.c. Tools: Bash ls/wc, Grep with output_mode=content, Read offset/limit on dwarf.c [350-410], [650-820], [6550-6940].
Fix the reversed comparison so an over-large length is caught:
if ((offset_size == 4 && curr + length > (end - 8)) || (offset_size == 8 && curr + length > (end - 16)))Strengthen the per-index check to also validate against real section bytes:
if (index_offset >= length || curr + index_offset + offset_size > end) { warn(...); return "<index offset is too big>"; }Replace the unchecked read with SAFE_BYTE_GET that already exists in the same file:
SAFE_BYTE_GET (str_offset, curr + index_offset, offset_size, end);
Exploit/verification: craft an ELF with -gdwarf-5, hex-edit the .debug_str_offsets header to length = 0xfffffff0. Run readelf --debug-dump=str-offsets poc.o under ASan — unfixed binary triggers heap-buffer-overflow read at the byte_get call site; patched binary returns "
Cross-repo pattern: parsers (objdump, readelf, libdwarf, libelf, dissectors) that derive a length from the same untrusted blob and bound subsequent reads against that length instead of the real buffer size. Reversed-direction comparisons (< where > was intended) are a recurring secondary class — check whether the warn() text agrees with the predicate.
- byte_get vs SAFE_BYTE_GET diff at 381-407 (SAFE checks PTR+amount >= END; byte_get does not).
- length attacker-controlled — read via SAFE_BYTE_GET_AND_INC at 746/751 from section bytes.
- Only intervening checks before the unchecked byte_get at 796 are the reversed guard (773) and the length-only check (788) — neither protects
curr + index_offset + offset_size <= end. - libbfd read_section in bfd/dwarf2.c does perform
offset >= *section_sizevalidation, isolating the bug to binutils/dwarf.c.