Skip to content

fix(dracut-install): bounds-check RUNPATH walk and ELF string derefs - #2622

Open
prabhakarpujeri wants to merge 2 commits into
dracut-ng:mainfrom
prabhakarpujeri:fix/elf-runpath-bounds
Open

fix(dracut-install): bounds-check RUNPATH walk and ELF string derefs#2622
prabhakarpujeri wants to merge 2 commits into
dracut-ng:mainfrom
prabhakarpujeri:fix/elf-runpath-bounds

Conversation

@prabhakarpujeri

Copy link
Copy Markdown

Stacked on #2615 (depends on its PARSE_ELF_START extent checks and elf_sect_name helper). Will be rebased after #2615 merges.

Problem

FIND_LIBRARY_RUNPATH_FOR_BITS (in src/install/dracut-install.c) walks the ELF .dynamic section's Elf_Dyn array and pulls out the DT_RUNPATH / DT_RPATH strings to search for libraries. Unlike the sibling macros RESOLVE_DEPS_NEEDED_FOR_BITS and RESOLVE_DEPS_DLOPEN_FOR_BITS, this walk validates only e_shoff/e_shstrndx — the per-iteration pointers into the map are never bounds-checked. Additionally, in the interpreter/DT_SONAME/DT_NEEDED code paths the strings extracted with (char *)map + shdr[i].sh_offset + d->d_un.d_val are handed to strlen/strdup/hashmap_get/expand_runpath/regexec without verifying they are NUL-terminated within the mapped file (a malformed ELF reading past the end of the map can hang on a never-terminated strcmp/strdup).

Also found one off-by-boundary check in the interpreter path: interpreter > (char *)map + src_len accepts the pointer at exactly one-past-end (should be >=).

Fix

  • New shared helper elf_map_string(map, src_len, offset): returns NULL if the offset is outside the map or the string there isn't NUL-terminated within the map. Used for the runpath string, interpreter string, DT_SONAME and DT_NEEDED strings.
  • The RUNPATH walk now mirrors its siblings' per-iteration bounds checks on dyn and d.

Verification

Crafted 3 malformed ELF64 inputs exercising each new check; ran the real dracut-install -l binary built before/after, and valgrind on the difference-maker:

input before after
H7A2: DT_RPATH whose strtab pointer lands 4 bytes before map end, runpath not NUL-terminated valgrind Invalid read of size 1 → SIGSEGV clean exit 0 (section rejected)
H7B2: PT_INTERP at exactly src_len one-past-end no crash (never dereferenced) clean exit 0 (no change needed⁠—it's never actually reached the new helper with that offset as a valid string; check now correctly excludes)
H7C2: PT_INTERP in-map but non-terminated clean clean exit 0

(The last two are safe in both versions only because no code path dereferences them — they're defensive/paranoia checks. The RUNPATH one is the reachable crash.)

Regression control: -l dependency resolution of /bin/bash, /bin/ls, and /usr/bin/ssh — byte-identical 33-entry file list before vs after. H6 test set re-verified — the 6 malformed ELFs from #2615's verification table still all exit cleanly.

The PARSE_ELF_START macro checked only that e_shoff and e_shstrndx are
individually < src_len, then indexed shdr[e_shstrndx].sh_offset into the
mmap'ed file. Two classes of OOB reads remain on truncated or malformed
ELFs:

1. The section header table extent is not validated: e_shnum section
   headers starting at shoff can extend past the end of the file, so
   iterating them reads past the map.
2. The section header string table (shstrtab) content extent isn't
   validated: sh_offset/sh_size come from the file, and the
   strcmp(&shstrtab[shdr[i].sh_name], ...) reads at attacker-controlled
   offsets, potentially past the mapping.

Add the missing bounds checks:

- PARSE_ELF_START now verifies the whole section table fits in the
  mapped file, then treats it as untrusted: a helper reads the shstrtab
  section's offset and size, validates both against src_len, and the
  section-name lookup checks the name offset and NUL termination inside
  the table before any strcmp.
- DRUNPATH/DT_SONAME/DT_NEEDED walks now validate the shadow-section
  link index (sh_link < e_shnum) before dereferencing shdr[sh_link].

Verified by crafting five malformed ELF64 inputs to the new
boundschecks and feeding them to the dependency resolution path:
- Off-map shstrtab.sh_offset: segfault without patch, clean skip with
- sh_name past table end:                    segfault without patch
- No NUL within name's table window:             segfault without patch
- Section table extent truncated (F1) / sh_link past end (F6b): clean
  rejection with patch
Regression control: -l dependency resolution of /bin/bash and /bin/ls
yields byte-identical results before/after. Also verified syntax with
and without HAVE_SYSTEMD (covers RESOLVE_DEPS_DLOPEN_FOR_BITS).
@prabhakarpujeri
prabhakarpujeri requested a review from a team as a code owner August 20, 2026 06:11
@github-actions github-actions Bot added c dracut-install Issues related to dracut install labels Aug 20, 2026
The FIND_LIBRARY_RUNPATH_FOR_BITS macro lacked the bounds checks its
sibling macros (RESOLVE_DEPS_NEEDED_FOR_BITS, RESOLVE_DEPS_DLOPEN_FOR_BITS)
have:

- the .dynamic section's shdr[]-indexed pointer into the map
- the per-iteration Elf_Dyn pointer in the DT_NULL-terminated walk
- the interpreter string (PT_INTERP) and DT_SONAME/DT_NEEDED string,
  passed to strdup/strlen with no NUL-termination verification

A malformed ELF could put a non-NUL-terminated runpath right at the end
of the map (reads past it in strlen), or an interpreter offset exactly
== src_len (off by > vs >=) that passed the old bounds check.

Add a shared helper, elf_map_string(), that validates the offset is
inside the map and the string is NUL-terminated there, and use it at
every place the code dereferences a dynamic-entry/segment-list string.
Mirror the per-iteration map-range checks in the RUNPATH walk that the
sibling macros already carry.

Depends on 0549d1c ("fix(dracut-install): validate ELF section-table
bounds"), which introduced elf_sect_name() and the PARSE_ELF_START
extent checks.

Verified with valgrind on crafted malformed ELF64s (pre-patch SIGSEGV
reading past the mmap; post-patch clean) and byte-for-byte identical
dependency sets for real binaries (/bin/bash, /bin/ls, /usr/bin/ssh).
@devkontrol

Copy link
Copy Markdown
Collaborator

CC @chewi

continue; \
\
Elf##B##_Dyn *dyn = (Elf##B##_Dyn *)((char *)map + ELF_BYTESWAP(B, shdr[i].sh_offset)); \
if ((char *)dyn < (char *)map || (char *)dyn > (char *)map + src_len) \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think void * rather than char * here and below would be equivalent but less confusing?

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

Labels

c dracut-install Issues related to dracut install

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants