diff --git a/ddprof-lib/src/main/cpp/sframe.cpp b/ddprof-lib/src/main/cpp/sframe.cpp index e607bf2a4..a2f1da1fe 100644 --- a/ddprof-lib/src/main/cpp/sframe.cpp +++ b/ddprof-lib/src/main/cpp/sframe.cpp @@ -4,6 +4,8 @@ */ #include "sframe.h" +#include "log.h" +#include #include #include @@ -16,7 +18,8 @@ SFrameParser::SFrameParser(const char* name, const char* section_base, _capacity(128), _count(0), _table(static_cast(malloc(128 * sizeof(FrameDesc)))), - _linked_frame_size(-1) {} + _linked_frame_size(-1), + _oom(false) {} SFrameParser::~SFrameParser() { free(_table); // safe: free(nullptr) is a no-op; table() nulls _table on success @@ -37,10 +40,15 @@ const FrameDesc& SFrameParser::detectedDefaultFrame() const { } FrameDesc* SFrameParser::addRecord(u32 loc, u32 cfa, int fp_off, int pc_off) { + if (!_table) return nullptr; // constructor malloc failed if (_count >= _capacity) { + if (_capacity > (INT_MAX / 2)) return nullptr; // overflow guard FrameDesc* resized = static_cast( realloc(_table, _capacity * 2 * sizeof(FrameDesc))); - if (!resized) return nullptr; + if (!resized) { + _oom = true; + return nullptr; + } _capacity *= 2; _table = resized; } @@ -52,54 +60,242 @@ FrameDesc* SFrameParser::addRecord(u32 loc, u32 cfa, int fp_off, int pc_off) { return fd; } -bool SFrameParser::parseFDE(const SFrameHeader* /*hdr*/, const SFrameFDE* /*fde*/, - const char* /*fre_section*/, const char* /*fre_end*/) { - return true; // stub — implemented in Task 4 +bool SFrameParser::parseFDE(const SFrameHeader* hdr, const SFrameFDE* fde, + const char* fre_section, const char* fre_end) { + // Determine FRE start address size + int addr_size; + switch (SFRAME_FUNC_FRE_TYPE(fde->info)) { + case SFRAME_FRE_TYPE_ADDR1: addr_size = 1; break; + case SFRAME_FRE_TYPE_ADDR2: addr_size = 2; break; + case SFRAME_FRE_TYPE_ADDR4: addr_size = 4; break; + default: return false; + } + + // Bounds-check fre_off before pointer arithmetic + size_t fre_section_len = static_cast(fre_end - fre_section); + if (fde->fre_off >= fre_section_len) return false; + + const char* fre_ptr = fre_section + fde->fre_off; + + for (uint32_t j = 0; j < fde->fre_num; j++) { + // (a) Entry-level bounds check + if (fre_ptr >= fre_end) return false; + + // (b) Read FRE start address offset (unsigned) + if (fre_ptr + addr_size > fre_end) return false; + uint32_t fre_start = 0; + if (addr_size == 1) { + fre_start = *reinterpret_cast(fre_ptr); + } else if (addr_size == 2) { + uint16_t v; memcpy(&v, fre_ptr, 2); fre_start = v; + } else { + memcpy(&fre_start, fre_ptr, 4); + } + fre_ptr += addr_size; + + // (c) Read FRE info byte + if (fre_ptr + 1 > fre_end) return false; + uint8_t fre_info = *reinterpret_cast(fre_ptr); + fre_ptr += 1; + + // (d) Determine offset encoding size + int off_size; + switch (SFRAME_FRE_OFFSET_SIZE(fre_info)) { + case SFRAME_FRE_OFFSET_1B: off_size = 1; break; + case SFRAME_FRE_OFFSET_2B: off_size = 2; break; + case SFRAME_FRE_OFFSET_4B: off_size = 4; break; + default: return false; + } + + // Decide what to read from the stream (governed by FRE info byte alone) + bool fp_tracked = SFRAME_FRE_FP_TRACKED(fre_info); + bool ra_in_fre = SFRAME_FRE_RA_TRACKED(fre_info); + + // (e) Bounds check all remaining reads for this FRE + int n_offsets = 1 + (fp_tracked ? 1 : 0) + (ra_in_fre ? 1 : 0); + if (fre_ptr + n_offsets * off_size > fre_end) return false; + + // (f) Read CFA offset (signed) + int32_t cfa_offset = 0; + if (off_size == 1) { + cfa_offset = *reinterpret_cast(fre_ptr); + } else if (off_size == 2) { + int16_t v; memcpy(&v, fre_ptr, 2); cfa_offset = v; + } else { + memcpy(&cfa_offset, fre_ptr, 4); + } + fre_ptr += off_size; + + // Guard: CFA offset must fit in the 24-bit field packed into FrameDesc::cfa + if (cfa_offset < -8388608 || cfa_offset > 8388607) return false; + + // (g) Read RA offset if tracked + int32_t ra_offset = 0; + if (ra_in_fre) { + if (off_size == 1) { + ra_offset = *reinterpret_cast(fre_ptr); + } else if (off_size == 2) { + int16_t v; memcpy(&v, fre_ptr, 2); ra_offset = v; + } else { + memcpy(&ra_offset, fre_ptr, 4); + } + fre_ptr += off_size; + } + + // (h) Read FP offset if tracked + int32_t fp_offset = 0; + if (fp_tracked) { + if (off_size == 1) { + fp_offset = *reinterpret_cast(fre_ptr); + } else if (off_size == 2) { + int16_t v; memcpy(&v, fre_ptr, 2); fp_offset = v; + } else { + memcpy(&fp_offset, fre_ptr, 4); + } + fre_ptr += off_size; + } + + // (i) Translate to FrameDesc fields + // Use unsigned arithmetic to avoid implementation-defined signed cast for large offsets + u32 loc = _section_offset + static_cast(fde->start_addr) + fre_start; + + u32 cfa_reg = SFRAME_FRE_BASE_REG(fre_info) ? static_cast(DW_REG_FP) + : static_cast(DW_REG_SP); + u32 cfa = (static_cast(cfa_offset) << 8) | cfa_reg; + + // aarch64 GCC vs Clang detection: first FP-based entry with cfa_offset > 0 + if (_linked_frame_size < 0 && cfa_reg == static_cast(DW_REG_FP) && cfa_offset > 0) { + _linked_frame_size = cfa_offset; + } + + // Determine fp_off: per-FRE value takes priority; fall back to header fixed offset + int fp_off; + if (fp_tracked) { + fp_off = static_cast(fp_offset); + } else if (hdr->cfa_fixed_fp_offset != 0) { + fp_off = static_cast(hdr->cfa_fixed_fp_offset); + } else { + fp_off = DW_SAME_FP; + } + + // Header fixed RA offset takes priority over per-FRE value + int pc_off; + if (hdr->cfa_fixed_ra_offset != 0) { + pc_off = static_cast(hdr->cfa_fixed_ra_offset); + } else if (ra_in_fre) { + pc_off = static_cast(ra_offset); + } else { + pc_off = DW_LINK_REGISTER; + } + + // (j) Append record + if (!addRecord(loc, cfa, fp_off, pc_off)) return false; + } + + return true; } bool SFrameParser::parse() { // 1. Size check - if (_section_size < sizeof(SFrameHeader)) return false; + if (_section_size < sizeof(SFrameHeader)) { + Log::warn("SFrame section too small in %s", _name); + return false; + } const SFrameHeader* hdr = reinterpret_cast(_section_base); // 2-4. Header field validation - if (hdr->magic != SFRAME_MAGIC) return false; - if (hdr->version != SFRAME_VERSION_2) return false; + if (hdr->magic != SFRAME_MAGIC) { + Log::warn("SFrame bad magic in %s", _name); + return false; + } + if (hdr->version != SFRAME_VERSION_2) { + Log::warn("SFrame unsupported version %d in %s", (int)hdr->version, _name); + return false; + } #if defined(__x86_64__) - if (hdr->abi_arch != SFRAME_ABI_AMD64_ENDIAN_LITTLE) return false; + if (hdr->abi_arch != SFRAME_ABI_AMD64_ENDIAN_LITTLE) { + Log::warn("SFrame wrong ABI 0x%x in %s", (int)hdr->abi_arch, _name); + return false; + } #elif defined(__aarch64__) - if (hdr->abi_arch != SFRAME_ABI_AARCH64_ENDIAN_LITTLE) return false; + if (hdr->abi_arch != SFRAME_ABI_AARCH64_ENDIAN_LITTLE) { + Log::warn("SFrame wrong ABI 0x%x in %s", (int)hdr->abi_arch, _name); + return false; + } #else return false; #endif // 5. Bounds check auxhdr_len before computing data_start - if (sizeof(SFrameHeader) + hdr->auxhdr_len > _section_size) return false; + if (sizeof(SFrameHeader) + hdr->auxhdr_len > _section_size) { + Log::warn("SFrame auxhdr_len overflows section in %s", _name); + return false; + } const char* data_start = _section_base + sizeof(SFrameHeader) + hdr->auxhdr_len; const char* section_end = _section_base + _section_size; + // Bounds-check fdeoff, freoff, and fre_len before pointer arithmetic + size_t data_len = static_cast(section_end - data_start); + if (hdr->fdeoff > data_len) { + Log::warn("SFrame fdeoff out of bounds in %s", _name); + return false; + } + if (hdr->freoff > data_len) { + Log::warn("SFrame freoff out of bounds in %s", _name); + return false; + } + if (hdr->fre_len > data_len - hdr->freoff) { + Log::warn("SFrame fre_len overflows section in %s", _name); + return false; + } + const SFrameFDE* fde_array = reinterpret_cast(data_start + hdr->fdeoff); const char* fre_section = data_start + hdr->freoff; const char* fre_end = fre_section + hdr->fre_len; // 6-7. Bounds checks for FDE array and FRE section if (reinterpret_cast(fde_array) + - (size_t)hdr->num_fdes * sizeof(SFrameFDE) > section_end) return false; - if (fre_end > section_end) return false; + (size_t)hdr->num_fdes * sizeof(SFrameFDE) > section_end) { + Log::warn("SFrame FDE array overflows section in %s", _name); + return false; + } + if (fre_end > section_end) { + Log::warn("SFrame FRE section overflows in %s", _name); + return false; + } + + // 8. FDE array / FRE section overlap check + if (hdr->num_fdes > 0 && hdr->fre_len > 0) { + const char* fde_start_ptr = reinterpret_cast(fde_array); + const char* fde_end_ptr = fde_start_ptr + (size_t)hdr->num_fdes * sizeof(SFrameFDE); + if (fde_end_ptr > fre_section && fde_start_ptr < fre_end) { + Log::warn("SFrame FDE array overlaps FRE section in %s", _name); + return false; + } + } - // 8. Iterate FDEs + // 9. Iterate FDEs for (uint32_t i = 0; i < hdr->num_fdes; i++) { const SFrameFDE* fde = &fde_array[i]; if (SFRAME_FUNC_FDE_TYPE(fde->info) != 0) continue; // skip PCMASK if (fde->fre_num == 0) continue; // empty FDE - parseFDE(hdr, fde, fre_section, fre_end); // ignore return; skip corrupt FDE + int saved_count = _count; + int saved_linked_frame_size = _linked_frame_size; + if (!parseFDE(hdr, fde, fre_section, fre_end)) { + if (_oom) return false; // OOM: partial table is not safe to use + // Corrupt FDE: roll back any partially-added records. + _count = saved_count; + _linked_frame_size = saved_linked_frame_size; + } } - // 9. Sort - qsort(_table, _count, sizeof(FrameDesc), FrameDesc::comparator); + // 10. Sort (skip if the section header guarantees sorted order) + if (_count > 0 && !(hdr->flags & SFRAME_F_FDE_SORTED)) { + qsort(_table, _count, sizeof(FrameDesc), FrameDesc::comparator); + } return _count > 0; } diff --git a/ddprof-lib/src/main/cpp/sframe.h b/ddprof-lib/src/main/cpp/sframe.h index 0407d59e0..345f6ad73 100644 --- a/ddprof-lib/src/main/cpp/sframe.h +++ b/ddprof-lib/src/main/cpp/sframe.h @@ -42,7 +42,7 @@ const int SFRAME_FRE_TYPE_ADDR1 = 0; const int SFRAME_FRE_TYPE_ADDR2 = 1; const int SFRAME_FRE_TYPE_ADDR4 = 2; -struct __attribute__((packed)) SFrameHeader { // 28 bytes +struct __attribute__((packed, may_alias)) SFrameHeader { // 28 bytes uint16_t magic; uint8_t version; uint8_t flags; @@ -57,7 +57,7 @@ struct __attribute__((packed)) SFrameHeader { // 28 bytes uint32_t freoff; }; -struct __attribute__((packed)) SFrameFDE { // 20 bytes +struct __attribute__((packed, may_alias)) SFrameFDE { // 20 bytes int32_t start_addr; // signed, relative to .sframe section start (V2) uint32_t func_size; uint32_t fre_off; // byte offset into FRE sub-section @@ -78,6 +78,7 @@ class SFrameParser { int _count; FrameDesc* _table; int _linked_frame_size; // for aarch64 GCC vs Clang detection; -1 = undetected + bool _oom; // set by addRecord on realloc failure bool parseFDE(const SFrameHeader* hdr, const SFrameFDE* fde, const char* fre_section, const char* fre_end); diff --git a/ddprof-lib/src/main/cpp/symbols_linux.cpp b/ddprof-lib/src/main/cpp/symbols_linux.cpp index d6dd12ee2..5d4a544c5 100644 --- a/ddprof-lib/src/main/cpp/symbols_linux.cpp +++ b/ddprof-lib/src/main/cpp/symbols_linux.cpp @@ -25,6 +25,7 @@ #include "common.h" #include "symbols.h" #include "dwarf.h" +#include "sframe.h" #include "fdtransferClient.h" #include "log.h" #include "os.h" @@ -702,6 +703,27 @@ void ElfParser::parseDynamicSection() { void ElfParser::parseDwarfInfo() { if (!DWARF_SUPPORTED) return; + // Try SFrame first (simpler format, faster parsing, no opcode interpretation). + ElfProgramHeader* sframe_phdr = findProgramHeader(PT_GNU_SFRAME); + if (sframe_phdr != NULL && sframe_phdr->p_vaddr != 0) { + const char* section_base = at(sframe_phdr); + uintptr_t section_offset_full = static_cast(section_base - _base); + if (section_offset_full > static_cast(UINT32_MAX)) { + Log::warn("SFrame section offset too large for u32 in %s; falling back to DWARF", _cc->name()); + goto dwarf_fallback; + } + u32 section_offset = static_cast(section_offset_full); + SFrameParser sframe(_cc->name(), section_base, + static_cast(sframe_phdr->p_filesz), section_offset); + if (sframe.parse()) { + _cc->setDwarfTable(sframe.table(), sframe.count(), + sframe.detectedDefaultFrame()); + return; + } + // SFrame parse failed; fall through to DWARF + } + + dwarf_fallback: ElfProgramHeader* eh_frame_hdr = findProgramHeader(PT_GNU_EH_FRAME); if (eh_frame_hdr != NULL) { if (eh_frame_hdr->p_vaddr != 0) { diff --git a/ddprof-lib/src/test/cpp/sframe_ut.cpp b/ddprof-lib/src/test/cpp/sframe_ut.cpp index c12615fd5..31f9a157b 100644 --- a/ddprof-lib/src/test/cpp/sframe_ut.cpp +++ b/ddprof-lib/src/test/cpp/sframe_ut.cpp @@ -93,46 +93,49 @@ static void buildFDE(std::vector& buf, // Appends an FRE with 1-byte start address (ADDR1) and 1-byte signed offsets (OFFSET_1B). // fre_info bits: bit0=CFA_base(0=SP,1=FP), bits1-2=0(1B), bit3=RA_tracked, bit4=FP_tracked. -// fp_off and ra_off are written only when the corresponding bit is set in fre_info. +// SFrame V2 wire order after CFA: RA offset (if tracked), then FP offset (if tracked). +// ra_off and fp_off are written only when the corresponding bit is set in fre_info. static void buildFRE_1B(std::vector& buf, uint8_t start_offset, uint8_t fre_info, int8_t cfa_off, - int8_t fp_off = 0, - int8_t ra_off = 0) { + int8_t ra_off = 0, + int8_t fp_off = 0) { put8(buf, start_offset); put8(buf, fre_info); put8(buf, static_cast(cfa_off)); - if (SFRAME_FRE_FP_TRACKED(fre_info)) put8(buf, static_cast(fp_off)); if (SFRAME_FRE_RA_TRACKED(fre_info)) put8(buf, static_cast(ra_off)); + if (SFRAME_FRE_FP_TRACKED(fre_info)) put8(buf, static_cast(fp_off)); } // Appends an FRE with 2-byte start address (ADDR2) and 2-byte signed offsets (OFFSET_2B). +// SFrame V2 wire order after CFA: RA offset (if tracked), then FP offset (if tracked). static void buildFRE_2B(std::vector& buf, uint16_t start_offset, uint8_t fre_info, int16_t cfa_off, - int16_t fp_off = 0, - int16_t ra_off = 0) { + int16_t ra_off = 0, + int16_t fp_off = 0) { put16(buf, start_offset); put8(buf, fre_info); put16(buf, static_cast(cfa_off)); - if (SFRAME_FRE_FP_TRACKED(fre_info)) put16(buf, static_cast(fp_off)); if (SFRAME_FRE_RA_TRACKED(fre_info)) put16(buf, static_cast(ra_off)); + if (SFRAME_FRE_FP_TRACKED(fre_info)) put16(buf, static_cast(fp_off)); } // Appends an FRE with 4-byte start address (ADDR4) and 4-byte signed offsets (OFFSET_4B). +// SFrame V2 wire order after CFA: RA offset (if tracked), then FP offset (if tracked). static void buildFRE_4B(std::vector& buf, uint32_t start_offset, uint8_t fre_info, int32_t cfa_off, - int32_t fp_off = 0, - int32_t ra_off = 0) { + int32_t ra_off = 0, + int32_t fp_off = 0) { put32(buf, start_offset); put8(buf, fre_info); put32(buf, static_cast(cfa_off)); - if (SFRAME_FRE_FP_TRACKED(fre_info)) put32(buf, static_cast(fp_off)); if (SFRAME_FRE_RA_TRACKED(fre_info)) put32(buf, static_cast(ra_off)); + if (SFRAME_FRE_FP_TRACKED(fre_info)) put32(buf, static_cast(fp_off)); } // ============================================================ @@ -154,8 +157,18 @@ TEST(SFrameParser, UnsupportedVersion) { } TEST(SFrameParser, WrongArch) { + // Build a structurally valid section with WRONG_ABI to confirm arch check fires. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); // start=0, SP-based, cfa_off=8 + std::vector buf; - buildHeader(buf, WRONG_ABI, -8, 0, 0, 0, 0, 0); + buildHeader(buf, WRONG_ABI, -8, 1, 1, + static_cast(fre_buf.size()), // fre_len + 0, // fdeoff (FDE array right after header) + 20); // freoff (FRE section after FDE) + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); EXPECT_FALSE(parser.parse()); } @@ -177,4 +190,699 @@ TEST(SFrameParser, AuxhdrLenBoundsCheck) { EXPECT_FALSE(parser.parse()); } +TEST(SFrameParser, EmptyFDEArray) { + // num_fdes=0 → no records → parse() returns false + std::vector buf; + buildHeader(buf, HOST_ABI, -8, /*num_fdes=*/0, 0, 0, 0, 0); + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, PCMASK_Skipped) { + // A single FDE with PCMASK type (bit 0 set in info) is skipped; no records → false + // + // Layout: header(28) | FDE(20) | FRE(3) + // fdeoff=0 (FDE array starts at data_start), freoff=20 (FRE section after FDE) + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); // valid FRE, but FDE is PCMASK so it's skipped + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/20); + buildFDE(buf, 0x100, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1, /*pcmask=*/true); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, EmptyFDE_Skipped) { + // A single FDE with fre_num=0 is skipped; no records → false + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 0, 0, /*fdeoff=*/0, /*freoff=*/20); + buildFDE(buf, 0x100, 64, 0, /*fre_num=*/0, SFRAME_FRE_TYPE_ADDR1); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, SingleFDE_SingleFRE_SPBased) { + // SP-based CFA with fixed RA offset -8 (x86_64 style). + // fre_info = 0x00: SP base (bit0=0), 1B offsets (bits1-2=0), no RA (bit3=0), no FP (bit4=0) + // CFA offset = 8 → cfa = (8 << 8) | DW_REG_SP + // fp_off = DW_SAME_FP (FP not tracked) + // pc_off = -8 (from cfa_fixed_ra_offset) + // + // Layout: header(28) | FDE(20) | FRE(3 bytes: 1 addr + 1 info + 1 cfa_off) + std::vector fre_buf; + buildFRE_1B(fre_buf, /*start_offset=*/0, /*fre_info=*/0x00, /*cfa_off=*/8); + + std::vector buf; + buildHeader(buf, HOST_ABI, /*cfa_fixed_ra_offset=*/-8, 1, 1, + (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/20); + buildFDE(buf, /*start_addr=*/0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 1); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // loc = section_offset(0) + start_addr(0x1000) + fre_start(0) = 0x1000 + EXPECT_EQ(table[0].loc, static_cast(0x1000)); + // cfa = (8 << 8) | DW_REG_SP + EXPECT_EQ(table[0].cfa, static_cast((8u << 8) | (unsigned)DW_REG_SP)); + EXPECT_EQ(table[0].fp_off, DW_SAME_FP); + EXPECT_EQ(table[0].pc_off, -8); + free(table); +} + +TEST(SFrameParser, SingleFDE_SingleFRE_FPBased) { + // FP-based CFA with FP tracked, fixed RA offset -8. + // fre_info = 0x11: FP base (bit0=1), 1B offsets (bits1-2=0), no RA (bit3=0), FP tracked (bit4=1) + // = 0b00010001 = 0x11 + // CFA offset = 16 → cfa = (16 << 8) | DW_REG_FP + // fp_off = -16 + // pc_off = -8 + std::vector fre_buf; + buildFRE_1B(fre_buf, 4, /*fre_info=*/0x11, /*cfa_off=*/16, /*ra_off=*/0, /*fp_off=*/-16); + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x2000, 128, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 1); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // loc = 0 + 0x2000 + 4 = 0x2004 + EXPECT_EQ(table[0].loc, static_cast(0x2004)); + EXPECT_EQ(table[0].cfa, static_cast((16u << 8) | (unsigned)DW_REG_FP)); + EXPECT_EQ(table[0].fp_off, -16); + EXPECT_EQ(table[0].pc_off, -8); + free(table); +} + +TEST(SFrameParser, FixedRAOffset) { + // Verify cfa_fixed_ra_offset drives pc_off even when FRE RA bit is set. + // fre_info = 0x08: SP base (bit0=0), 1B offsets (bits1-2=0), RA tracked (bit3=1), no FP (bit4=0) + // The FRE encodes ra_off=-16, but the header's cfa_fixed_ra_offset=-8 must win. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, /*fre_info=*/0x08, /*cfa_off=*/8, /*ra_off=*/-16); + + std::vector buf; + buildHeader(buf, HOST_ABI, /*cfa_fixed_ra_offset=*/-8, 1, 1, + (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x500, 32, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // pc_off must come from the header (-8), not from the FRE RA offset (-16) + EXPECT_EQ(table[0].pc_off, -8); + free(table); +} + +TEST(SFrameParser, PerFRE_RA) { + // aarch64 style: cfa_fixed_ra_offset=0, RA tracked per-FRE. + // fre_info = 0x08: SP base (bit0=0), 1B offsets (bits1-2=0), RA tracked (bit3=1), no FP (bit4=0) + // = 0b00001000 = 0x08 + // RA offset = -8 + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, /*fre_info=*/0x08, /*cfa_off=*/16, /*ra_off=*/-8); + + std::vector buf; + buildHeader(buf, HOST_ABI, /*cfa_fixed_ra_offset=*/0, 1, 1, + (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x3000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].pc_off, -8); + free(table); +} + +TEST(SFrameParser, PerFRE_RA_Untracked) { + // aarch64 leaf function: cfa_fixed_ra_offset=0, RA not tracked → DW_LINK_REGISTER. + // fre_info = 0x00: SP base, 1B offsets, no RA, no FP + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + + std::vector buf; + buildHeader(buf, HOST_ABI, /*cfa_fixed_ra_offset=*/0, 1, 1, + (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x4000, 32, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].pc_off, DW_LINK_REGISTER); + free(table); +} + +TEST(SFrameParser, OffsetSize_2B) { + // FRE with 2-byte start address (ADDR2) and 2-byte offset encoding (OFFSET_2B). + // fre_info = 0x02: SP base (bit0=0), 2B offsets (bits1-2=01), no RA, no FP + // = 0b00000010 = 0x02 + // CFA offset = 512 (requires 2 bytes) + std::vector fre_buf; + buildFRE_2B(fre_buf, /*start_offset=*/8, /*fre_info=*/0x02, /*cfa_off=*/512); + + // fre_buf size: 2 (addr) + 1 (info) + 2 (cfa) = 5 bytes + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x5000, 256, 0, 1, SFRAME_FRE_TYPE_ADDR2); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // loc = 0 + 0x5000 + 8 = 0x5008 + EXPECT_EQ(table[0].loc, static_cast(0x5008)); + // cfa = (512 << 8) | DW_REG_SP + EXPECT_EQ(table[0].cfa, static_cast((512u << 8) | (unsigned)DW_REG_SP)); + free(table); +} + +TEST(SFrameParser, OffsetSize_4B) { + // FRE with 4-byte start address (ADDR4) and 4-byte offset encoding (OFFSET_4B). + // fre_info = 0x04: SP base (bit0=0), 4B offsets (bits1-2=10), no RA, no FP + // = 0b00000100 = 0x04 + // CFA offset = 65536 (requires 4 bytes) + std::vector fre_buf; + buildFRE_4B(fre_buf, /*start_offset=*/0, /*fre_info=*/0x04, /*cfa_off=*/65536); + + // fre_buf size: 4 (addr) + 1 (info) + 4 (cfa) = 9 bytes + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x6000, 512, 0, 1, SFRAME_FRE_TYPE_ADDR4); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].loc, static_cast(0x6000)); + EXPECT_EQ(table[0].cfa, static_cast((65536u << 8) | (unsigned)DW_REG_SP)); + free(table); +} + +TEST(SFrameParser, MultipleFDEs) { + // 3 FDEs with 2 FREs each → 6 total records, verify sorted by loc. + // + // FRE layout (2 FREs per FDE × 3 FDEs, each FRE = 3 bytes: 1addr+1info+1cfa): + // FDE0 FREs at fre_section+0: offset 0 cfa=8, offset 8 cfa=16 + // FDE1 FREs at fre_section+6: offset 0 cfa=8, offset 4 cfa=16 + // FDE2 FREs at fre_section+12: offset 0 cfa=8, offset 2 cfa=16 + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + buildFRE_1B(fre_buf, 8, 0x00, 16); + buildFRE_1B(fre_buf, 0, 0x00, 8); + buildFRE_1B(fre_buf, 4, 0x00, 16); + buildFRE_1B(fre_buf, 0, 0x00, 8); + buildFRE_1B(fre_buf, 2, 0x00, 16); + // fre_buf.size() = 18 + + std::vector buf; + // freoff = 3 * sizeof(SFrameFDE) = 60 + buildHeader(buf, HOST_ABI, -8, 3, 6, (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/60); + buildFDE(buf, 0xA000, 32, 0, 2, SFRAME_FRE_TYPE_ADDR1); // FDE0: FREs at offset 0 + buildFDE(buf, 0xB000, 32, 6, 2, SFRAME_FRE_TYPE_ADDR1); // FDE1: FREs at offset 6 + buildFDE(buf, 0xC000, 32, 12, 2, SFRAME_FRE_TYPE_ADDR1); // FDE2: FREs at offset 12 + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 6); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // Verify sorted ascending + for (int i = 0; i + 1 < 6; i++) { + EXPECT_LT(table[i].loc, table[i + 1].loc) + << "table not sorted at index " << i; + } + // Spot-check locs: FDE0@0xA000, FDE0@0xA008, FDE1@0xB000, ... + EXPECT_EQ(table[0].loc, static_cast(0xA000)); + EXPECT_EQ(table[1].loc, static_cast(0xA008)); + EXPECT_EQ(table[2].loc, static_cast(0xB000)); + free(table); +} + +TEST(SFrameParser, MultipleFDEs_ReverseOrder) { + // 3 FDEs in descending address order: 0xC000, 0xB000, 0xA000. + // The SFRAME_F_FDE_SORTED flag is NOT set so qsort must fire. + // Output must be sorted ascending. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); // FDE0 (start=0xC000) + buildFRE_1B(fre_buf, 4, 0x00, 16); + buildFRE_1B(fre_buf, 0, 0x00, 8); // FDE1 (start=0xB000) + buildFRE_1B(fre_buf, 4, 0x00, 16); + buildFRE_1B(fre_buf, 0, 0x00, 8); // FDE2 (start=0xA000) + buildFRE_1B(fre_buf, 4, 0x00, 16); + // fre_buf.size() = 18 + + std::vector buf; + // freoff = 3 * 20 = 60; flags = 0 (no SORTED flag) + put16(buf, SFRAME_MAGIC); + put8(buf, SFRAME_VERSION_2); + put8(buf, 0); // flags: NOT SFRAME_F_FDE_SORTED + put8(buf, HOST_ABI); + put8(buf, 0); // cfa_fixed_fp_offset + put8(buf, static_cast(-8)); // cfa_fixed_ra_offset + put8(buf, 0); // auxhdr_len + put32(buf, 3); // num_fdes + put32(buf, 6); // num_fres + put32(buf, (uint32_t)fre_buf.size()); // fre_len + put32(buf, 0); // fdeoff + put32(buf, 60); // freoff + + buildFDE(buf, 0xC000, 32, 0, 2, SFRAME_FRE_TYPE_ADDR1); + buildFDE(buf, 0xB000, 32, 6, 2, SFRAME_FRE_TYPE_ADDR1); + buildFDE(buf, 0xA000, 32, 12, 2, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 6); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + // Must be sorted ascending after qsort + for (int i = 0; i + 1 < 6; i++) { + EXPECT_LT(table[i].loc, table[i + 1].loc) + << "table not sorted at index " << i; + } + EXPECT_EQ(table[0].loc, static_cast(0xA000)); + EXPECT_EQ(table[5].loc, static_cast(0xC004)); + free(table); +} + +TEST(SFrameParser, AddressTranslation) { + // section_offset=0x1000, FDE start_addr=0x200, FRE start=0x10 + // Expected loc = 0x1000 + 0x200 + 0x10 = 0x1210 + std::vector fre_buf; + buildFRE_1B(fre_buf, /*start_offset=*/0x10, 0x00, 8); + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, /*start_addr=*/0x200, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), + /*section_offset=*/0x1000); + ASSERT_TRUE(parser.parse()); + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].loc, static_cast(0x1210)); + free(table); +} + +TEST(SFrameParser, BoundsCheck_FREOverrun) { + // FDE0's fre_off points at fre_end → parseFDE entry-level bounds check fires + // immediately (no records added). FDE1 has a valid FRE → 1 record. + // + // Layout: header(28) | FDE0(20) | FDE1(20) | FRE(3 bytes) + // fre_section = data_start + 40, fre_end = fre_section + 3 + // FDE0: fre_off=3, fre_num=1 → fre_ptr = fre_section+3 = fre_end → fail + // FDE1: fre_off=0, fre_num=1 → fre_ptr = fre_section+0 → reads 3 bytes → OK + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); // FDE1's FRE (3 bytes at offset 0) + // fre_buf.size() = 3; FDE0's fre_off=3 points exactly at fre_end + + std::vector buf; + // freoff = 2 * sizeof(SFrameFDE) = 40 + buildHeader(buf, HOST_ABI, -8, 2, 1, (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/40); + buildFDE(buf, 0xD000, 64, /*fre_off=*/3, /*fre_num=*/1, SFRAME_FRE_TYPE_ADDR1); // overruns + buildFDE(buf, 0xE000, 64, /*fre_off=*/0, /*fre_num=*/1, SFRAME_FRE_TYPE_ADDR1); // OK + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + // parse() returns true because FDE1 produced a record + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 1); // only FDE1's FRE made it through + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].loc, static_cast(0xE000)); + free(table); +} + +TEST(SFrameParser, BoundsCheck_AddrSizeTruncated) { + // ADDR2 FRE: buffer ends after only 1 of the 2 start-address bytes. + // fre_ptr + addr_size(2) > fre_end triggers guard (b). + std::vector fre_buf; + // Write only 1 byte of the 2-byte start address (truncated) + put8(fre_buf, 0x00); // first byte of start address — second byte missing + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR2); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + // FDE is skipped due to bounds failure; no records → false + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, BoundsCheck_InfoByteTruncated) { + // ADDR1 FRE: buffer ends exactly after the 1-byte start address, no room for info byte. + // fre_ptr + 1 > fre_end triggers guard (c). + std::vector fre_buf; + put8(fre_buf, 0x00); // start address byte only; info byte missing + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, BoundsCheck_FDEOffOutOfBounds) { + // fdeoff set beyond data_len; parse() must return false. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + + std::vector buf; + // data_len = buf.size() - 28 (header). Set fdeoff = data_len + 100 (way past end). + uint32_t fake_fdeoff = 9999; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), + /*fdeoff=*/fake_fdeoff, /*freoff=*/20); + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, BoundsCheck_FRELenOverflow) { + // fre_len set so that freoff + fre_len > data_len; parse() must return false. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + + std::vector buf; + // freoff=20 (valid), fre_len=9999 (way past end) + buildHeader(buf, HOST_ABI, -8, 1, 1, /*fre_len=*/9999, /*fdeoff=*/0, /*freoff=*/20); + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, BoundsCheck_FREOffOutOfBounds) { + // fde->fre_off == fre_section_len: points exactly past the FRE sub-section. + // parseFDE must reject before doing pointer arithmetic. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); // 3 bytes; fre_section_len = 3 + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/20); + // fre_off == fre_len (3): exactly at end, out-of-bounds + buildFDE(buf, 0x1000, 64, /*fre_off=*/3, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, ReservedFREAddrSize) { + // FDE info with addr-size bits = 0b11 (value 3) → default:return false in parseFDE. + // FDE is skipped; no records → parse() returns false. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + // Encode addr-size = 3 (0b11) in bits 1-2 of FDE info byte: info = 0b00000110 = 0x06 + uint8_t fde_info = 0x06; // not pcmask (bit0=0), addr-size=3 (bits1-2=11) + put32(buf, static_cast(0x1000)); // start_addr + put32(buf, 64u); // func_size + put32(buf, 0u); // fre_off + put32(buf, 1u); // fre_num + put8(buf, fde_info); + put8(buf, 0); // rep_size + put16(buf, 0); // padding + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, ReservedFREOffsetSize) { + // FRE info byte with offset-size bits = 0b11 (value 3) → default:return false. + // Build a minimal ADDR1 FRE but with reserved offset-size in the info byte. + // fre_info = 0b00000110 = 0x06: SP base, offset-size=3 (reserved), no RA, no FP + std::vector fre_buf; + put8(fre_buf, 0x00); // start address = 0 + put8(fre_buf, 0x06); // fre_info: offset-size bits = 0b11 → reserved + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); +} + +TEST(SFrameParser, AuxhdrLen_NonZero) { + // auxhdr_len=4: 4 extra bytes between header and data_start. + // fdeoff and freoff are relative to data_start (after auxhdr), so no adjustment needed. + std::vector fre_buf; + buildFRE_1B(fre_buf, 0, 0x00, 8); + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + // Overwrite auxhdr_len byte (offset 7 in header) with 4 + buf[7] = 4; + // Append 4 auxhdr bytes + put32(buf, 0xDEADBEEF); // dummy auxhdr content + // Now append FDE and FRE (positions shift by 4, but offsets are relative to data_start) + buildFDE(buf, 0x1000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 1); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].loc, static_cast(0x1000)); + free(table); +} + +TEST(SFrameParser, ParseFailure_FreesTable) { + // parse() fails (wrong magic) → destructor must free _table without crashing. + // With ASAN this also detects leaks. + std::vector buf(28, 0); // all zeros → wrong magic + { + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + EXPECT_FALSE(parser.parse()); + // destructor runs here; _table was malloc'd in constructor and must be freed + } + // Reaching here without crash/ASAN report is the pass condition +} + +TEST(SFrameParser, DefaultFrameDetection) { + // Build a section with one FP-based FRE so _linked_frame_size gets set. + // fre_info = 0x11: FP base (bit0=1), 1B offsets (bits1-2=0), no RA, FP tracked (bit4=1) + // = 0b00010001 = 0x11 + // cfa_offset = LINKED_FRAME_CLANG_SIZE (16 on both x86_64 and aarch64 clang) + std::vector fre_buf; + buildFRE_1B(fre_buf, 4, 0x11, (int8_t)LINKED_FRAME_CLANG_SIZE, /*ra_off=*/0, (int8_t)-LINKED_FRAME_CLANG_SIZE); + + std::vector buf; + buildHeader(buf, HOST_ABI, -8, 1, 1, (uint32_t)fre_buf.size(), 0, 20); + buildFDE(buf, 0xF000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + + const FrameDesc& def = parser.detectedDefaultFrame(); + +#if defined(__aarch64__) + // On aarch64: LINKED_FRAME_CLANG_SIZE(16) != LINKED_FRAME_SIZE(0) → clang frame + EXPECT_EQ(&def, &FrameDesc::default_clang_frame); +#elif defined(__x86_64__) + // On x86_64: LINKED_FRAME_CLANG_SIZE == LINKED_FRAME_SIZE → default_frame + EXPECT_EQ(&def, &FrameDesc::default_frame); +#endif + + free(parser.table()); +} + +// ============================================================ +// T1 — Both RA and FP tracked: correct offset assignment +// ============================================================ + +TEST(SFrameParser, BothRA_and_FP_Tracked_CorrectOrder) { + // fre_info = 0x18: SP base (bit0=0), 1B offsets (bits1-2=0), + // RA tracked (bit3=1), FP tracked (bit4=1) → 0b00011000 + // SFrame V2 wire order: CFA, then RA slot, then FP slot. + // RA slot = -8, FP slot = -16. + // cfa_fixed_ra_offset = 0 so per-FRE RA value drives pc_off. + // + // Expected: pc_off == -8 (from RA slot), fp_off == -16 (from FP slot). + // If the read order is swapped: pc_off == -16, fp_off == -8. + static const uint8_t fre_info = 0x18; + std::vector fre_buf; + buildFRE_1B(fre_buf, /*start_offset=*/0, fre_info, /*cfa_off=*/16, + /*ra_off=*/-8, /*fp_off=*/-16); + + std::vector buf; + buildHeader(buf, HOST_ABI, /*cfa_fixed_ra_offset=*/0, 1, 1, + (uint32_t)fre_buf.size(), /*fdeoff=*/0, /*freoff=*/20); + buildFDE(buf, 0x7000, 64, 0, 1, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + EXPECT_EQ(parser.count(), 1); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].pc_off, -8) << "RA slot must map to pc_off (read order: RA before FP)"; + EXPECT_EQ(table[0].fp_off, -16) << "FP slot must map to fp_off (read order: RA before FP)"; + free(table); +} + +// ============================================================ +// T2 — Partial FDE rollback: corrupt second FDE leaves no trace +// ============================================================ + +TEST(SFrameParser, PartialFDE_Rollback_CountRestored) { + // FDE0 (valid): one FRE at 0xA000 → adds 1 record. + // FDE1 (corrupt): fre_num=2 but the buffer for the second FRE is missing. + // parseFDE adds the first FRE of FDE1 then returns false on the second. + // With rollback, that partial record must not survive. + // + // Each valid FRE: 1 (start_offset) + 1 (fre_info) + 1 (cfa_off) = 3 bytes. + std::vector fre_buf; + // FDE0's FRE (offset 0 in fre section) + buildFRE_1B(fre_buf, 0, 0x00, 8); + // FDE1's first FRE (offset 3) — valid bytes + buildFRE_1B(fre_buf, 0, 0x00, 8); + // FDE1's second FRE is intentionally absent (truncated buffer). + + // fre_section layout: [FDE0-fre0(3)] [FDE1-fre0(3)] total = 6 bytes + // FDE0: fre_off=0, fre_num=1 + // FDE1: fre_off=3, fre_num=2 → second FRE missing → parseFDE returns false + std::vector buf; + // freoff = 2 * sizeof(SFrameFDE) = 40 + buildHeader(buf, HOST_ABI, -8, 2, /*num_fres=*/3, (uint32_t)fre_buf.size(), + /*fdeoff=*/0, /*freoff=*/40); + buildFDE(buf, 0xA000, 32, /*fre_off=*/0, /*fre_num=*/1, SFRAME_FRE_TYPE_ADDR1); + buildFDE(buf, 0xB000, 32, /*fre_off=*/3, /*fre_num=*/2, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + + // Only FDE0's record must survive; FDE1's partial record must be rolled back. + EXPECT_EQ(parser.count(), 1); + + FrameDesc* table = parser.table(); + ASSERT_NE(table, nullptr); + EXPECT_EQ(table[0].loc, static_cast(0xA000)); + free(table); +} + +// ============================================================ +// T3 — Partial FDE rollback: _linked_frame_size not poisoned +// ============================================================ + +TEST(SFrameParser, PartialFDE_Rollback_LinkedFrameSizeRestored) { + // FDE array order matters: FDE1 (valid, SP-based) is processed FIRST so it + // produces one committed record. FDE0 (FP-based, fre_num=2) is processed + // SECOND: its FRE0 succeeds and sets _linked_frame_size, then FRE1 runs off + // the end of the fre section → parseFDE returns false → rollback restores + // _count to 1 and _linked_frame_size to -1. + // + // fre_info for FP-based FRE with FP tracked: + // 0x11 = 0b00010001: FP base (bit0=1), 1B offsets, no RA (bit3=0), FP tracked (bit4=1) + // Each such FRE: 1(start) + 1(info) + 1(cfa) + 1(fp) = 4 bytes + // fre_info for SP-based FRE, no tracked offsets: + // 0x00: 1(start) + 1(info) + 1(cfa) = 3 bytes + static const uint8_t fp_fre_info = 0x11; + + std::vector fre_buf; + + // Offset 0: FDE1's single FRE (SP-based, 3 bytes). + buildFRE_1B(fre_buf, 0, 0x00, 8); + + // Offset 3: FDE0's FRE0 (FP-based, 4 bytes). Sets _linked_frame_size. + // After this FRE fre_ptr == fre_end, so FRE1 triggers the fre_ptr>=fre_end + // early-exit in parseFDE, returning false. + buildFRE_1B(fre_buf, 0, fp_fre_info, (int8_t)LINKED_FRAME_CLANG_SIZE, + /*ra_off=*/0, (int8_t)-LINKED_FRAME_CLANG_SIZE); + // fre_buf.size() == 7; fre_end == fre_section + 7. + // FDE0's second FRE iteration: fre_ptr (= fre_section + 7) >= fre_end → false. + + std::vector buf; + // freoff = 2 * sizeof(SFrameFDE) = 40 + buildHeader(buf, HOST_ABI, -8, 2, /*num_fres=*/3, (uint32_t)fre_buf.size(), + /*fdeoff=*/0, /*freoff=*/40); + // FDE1 first in array: valid, fre_off=0, fre_num=1. + buildFDE(buf, 0xC000, 32, /*fre_off=*/0, /*fre_num=*/1, SFRAME_FRE_TYPE_ADDR1); + // FDE0 second: FP-based, fre_off=3, fre_num=2 (second FRE runs off fre section). + buildFDE(buf, 0xA000, 64, /*fre_off=*/3, /*fre_num=*/2, SFRAME_FRE_TYPE_ADDR1); + buf.insert(buf.end(), fre_buf.begin(), fre_buf.end()); + + SFrameParser parser("test", reinterpret_cast(buf.data()), buf.size(), 0); + ASSERT_TRUE(parser.parse()); + + // FDE1's record must survive; FDE0's partial record must be rolled back. + EXPECT_EQ(parser.count(), 1); + + // _linked_frame_size was mutated by FDE0's FRE0, then restored by rollback. + // On aarch64 (where LINKED_FRAME_CLANG_SIZE != LINKED_FRAME_SIZE) this asserts + // the clang-frame variant is NOT selected; on x86_64 both sizes are equal so + // detectedDefaultFrame() always returns default_frame regardless — the assertion + // is structurally correct but only diagnostic on aarch64. + const FrameDesc& def = parser.detectedDefaultFrame(); + EXPECT_EQ(&def, &FrameDesc::default_frame) + << "_linked_frame_size must be rolled back when parseFDE returns false"; + + free(parser.table()); +} + +// ============================================================ +// T4 — section_offset_full overflow guard +// ============================================================ + +// The guard `section_offset_full > UINT32_MAX` lives in ElfParser::parseDwarfInfo() +// (symbols_linux.cpp). That function requires a live ELF image and cannot be +// exercised in the SFrameParser unit-test harness without pulling in ElfParser. +// +// Validation approach: the guard is a single arithmetic comparison inserted before +// the SFrame path is entered. When section_base - _base > 0xFFFFFFFF the function +// falls through to the DWARF path rather than constructing an SFrameParser with a +// truncated u32 offset. The correctness of this guard is verified by code review +// of the production change in symbols_linux.cpp:710-711. +// +// The placeholder test below documents the requirement so it appears in the test +// suite and can be extended if an ElfParser integration harness is added later. +TEST(SFrameParser, SectionOffsetOverflow_GuardDocumented) { + // This test serves as a permanent marker that the UINT32_MAX guard in + // parseDwarfInfo() is a required correctness fix (spec requirement #3). + // No SFrameParser API is needed to verify the guard; the production code + // path that is guarded does not reach SFrameParser::parse() when the offset + // exceeds UINT32_MAX. + SUCCEED(); +} + #endif // __linux__