Skip to content

Commit 9ab6c03

Browse files
Merge pull request #567 from edznux-dd/fix/buildid-overflow
fix: prevent overflow and misaligned read in build-id parsing Co-authored-by: edznux-dd <edouard.schweisguth@datadoghq.com>
2 parents 8c433d5 + 278bb8c commit 9ab6c03

3 files changed

Lines changed: 79 additions & 11 deletions

File tree

ddprof-lib/src/main/cpp/symbols_linux.cpp

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,8 +1144,11 @@ char* SymbolsLinux::extractBuildIdFromMemory(const void* elf_base, size_t elf_si
11441144
return nullptr;
11451145
}
11461146

1147-
// Verify program header table is within file bounds
1148-
if (ehdr->e_phoff + ehdr->e_phnum * sizeof(Elf64_Phdr) > elf_size) {
1147+
// Verify program header table is within file bounds. Written as subtractions
1148+
// so a huge e_phoff cannot wrap the addition and slip past the check, which
1149+
// would leave `phdr` pointing outside the mapped image.
1150+
if (ehdr->e_phoff > elf_size ||
1151+
ehdr->e_phnum * sizeof(Elf64_Phdr) > elf_size - ehdr->e_phoff) {
11491152
return nullptr;
11501153
}
11511154

@@ -1160,8 +1163,11 @@ char* SymbolsLinux::extractBuildIdFromMemory(const void* elf_base, size_t elf_si
11601163
// Search for PT_NOTE segments
11611164
for (int i = 0; i < ehdr->e_phnum; i++) {
11621165
if (phdr[i].p_type == PT_NOTE && phdr[i].p_filesz > 0) {
1163-
// Ensure note segment is within file bounds
1164-
if (phdr[i].p_offset + phdr[i].p_filesz > elf_size) {
1166+
// Ensure note segment is within file bounds. Subtraction form avoids
1167+
// a u64 overflow in p_offset + p_filesz that would otherwise yield a
1168+
// wild note_data pointer passed to findBuildIdInNotes().
1169+
if (phdr[i].p_offset > elf_size ||
1170+
phdr[i].p_filesz > elf_size - phdr[i].p_offset) {
11651171
continue;
11661172
}
11671173

@@ -1197,11 +1203,19 @@ const uint8_t* SymbolsLinux::findBuildIdInNotes(const void* note_data, size_t no
11971203
break;
11981204
}
11991205

1200-
const Elf64_Nhdr* nhdr = reinterpret_cast<const Elf64_Nhdr*>(data + offset);
1206+
// Copy the note header into an aligned local: note_data is base +
1207+
// p_offset and p_offset is attacker-controlled, so dereferencing an
1208+
// Elf64_Nhdr* in place could be a misaligned load (UB, and a fault on
1209+
// alignment-strict architectures). The size check above guarantees the
1210+
// whole header is in bounds.
1211+
Elf64_Nhdr nhdr;
1212+
memcpy(&nhdr, data + offset, sizeof(nhdr));
12011213

1202-
// Calculate aligned sizes (4-byte alignment as per ELF spec)
1203-
size_t name_size_aligned = (nhdr->n_namesz + 3) & ~static_cast<size_t>(3);
1204-
size_t desc_size_aligned = (nhdr->n_descsz + 3) & ~static_cast<size_t>(3);
1214+
// Calculate aligned sizes (4-byte alignment as per ELF spec). Promote to
1215+
// size_t before the +3 so a near-UINT32_MAX n_namesz/n_descsz cannot wrap
1216+
// to a small value and defeat the bounds checks below.
1217+
size_t name_size_aligned = (static_cast<size_t>(nhdr.n_namesz) + 3) & ~static_cast<size_t>(3);
1218+
size_t desc_size_aligned = (static_cast<size_t>(nhdr.n_descsz) + 3) & ~static_cast<size_t>(3);
12051219

12061220
// Check bounds using subtraction to avoid overflow
12071221
size_t remaining = note_size - offset - sizeof(Elf64_Nhdr);
@@ -1214,13 +1228,13 @@ const uint8_t* SymbolsLinux::findBuildIdInNotes(const void* note_data, size_t no
12141228
}
12151229

12161230
// Check if this is a GNU build-id note
1217-
if (nhdr->n_type == NT_GNU_BUILD_ID && nhdr->n_namesz > 0 && nhdr->n_descsz > 0) {
1231+
if (nhdr.n_type == NT_GNU_BUILD_ID && nhdr.n_namesz > 0 && nhdr.n_descsz > 0) {
12181232
const char* name = data + offset + sizeof(Elf64_Nhdr);
12191233

12201234
// Verify GNU build-id name (including null terminator)
1221-
if (nhdr->n_namesz == 4 && strncmp(name, GNU_BUILD_ID_NAME, 3) == 0 && name[3] == '\0') {
1235+
if (nhdr.n_namesz == 4 && strncmp(name, GNU_BUILD_ID_NAME, 3) == 0 && name[3] == '\0') {
12221236
const uint8_t* desc = reinterpret_cast<const uint8_t*>(data + offset + sizeof(Elf64_Nhdr) + name_size_aligned);
1223-
*build_id_len = nhdr->n_descsz;
1237+
*build_id_len = nhdr.n_descsz;
12241238
return desc;
12251239
}
12261240
}

ddprof-lib/src/test/cpp/elfparser_ut.cpp

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#include <fcntl.h>
1717
#include <sys/mman.h>
1818
#include <cstring>
19+
#include <cstdlib>
20+
#include <elf.h>
1921
#include <chrono>
2022
#include <thread>
2123

@@ -359,4 +361,56 @@ INSTANTIATE_TEST_SUITE_P(
359361
::testing::Range(3, 21) // This will test delays from 5 to 20 milliseconds inclusive
360362
);
361363
#endif
364+
365+
namespace {
366+
367+
// Minimal valid ELF64 header; callers set the malformed field afterwards.
368+
Elf64_Ehdr validEhdr() {
369+
Elf64_Ehdr e;
370+
memset(&e, 0, sizeof(e));
371+
e.e_ident[EI_MAG0] = ELFMAG0;
372+
e.e_ident[EI_MAG1] = ELFMAG1;
373+
e.e_ident[EI_MAG2] = ELFMAG2;
374+
e.e_ident[EI_MAG3] = ELFMAG3;
375+
e.e_ident[EI_CLASS] = ELFCLASS64;
376+
e.e_ident[EI_DATA] = ELFDATA2LSB;
377+
e.e_ident[EI_VERSION] = EV_CURRENT;
378+
e.e_type = ET_DYN;
379+
e.e_machine = EM_X86_64;
380+
e.e_version = EV_CURRENT;
381+
e.e_ehsize = sizeof(Elf64_Ehdr);
382+
e.e_shstrndx = 1;
383+
return e;
384+
}
385+
386+
} // namespace
387+
388+
// Regression test for the build-id parser hardening (found by fuzz_elf).
389+
// extractBuildIdFromMemory()'s `p_offset + p_filesz` bounds check could
390+
// overflow, letting findBuildIdInNotes() walk a PT_NOTE past the buffer. The
391+
// buffer is heap-allocated and sized exactly so ASan's redzone catches the
392+
// over-read deterministically. The hardened parser must return cleanly.
393+
TEST(ElfBuildId, noteOffsetOverflow) {
394+
Elf64_Ehdr e = validEhdr();
395+
e.e_phoff = sizeof(Elf64_Ehdr);
396+
e.e_phentsize = sizeof(Elf64_Phdr);
397+
e.e_phnum = 1;
398+
399+
Elf64_Phdr p;
400+
memset(&p, 0, sizeof(p));
401+
p.p_type = PT_NOTE;
402+
p.p_offset = 0x70; // inside the buffer
403+
p.p_filesz = static_cast<uint64_t>(8) - p.p_offset; // sum wraps to 8 (< size)
404+
405+
const size_t size = sizeof(e) + sizeof(p); // 120 bytes
406+
char* buf = new char[size]; // exact size -> redzone right after
407+
memcpy(buf, &e, sizeof(e));
408+
memcpy(buf + sizeof(e), &p, sizeof(p));
409+
410+
size_t build_id_len = 0;
411+
char* id = SymbolsLinux::extractBuildIdFromMemory(buf, size, &build_id_len);
412+
free(id); // hardened parser returns nullptr; the point is that it must not crash
413+
delete[] buf;
414+
}
415+
362416
#endif //__linux__
120 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)