kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

commit 3f5930bf4415e49b3a734938b04b96c8b9d0be84
parent 15f53482a61de6b6c43f85d1845e53b580e9080f
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Thu, 16 Jul 2026 11:29:55 -0700

coff: preserve runnable PE images when stripping

Diffstat:
Msrc/obj/coff/rewrite.c | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mtest/api/object_rewrite_test.c | 28+++++++++++++++++++++-------
2 files changed, 137 insertions(+), 41 deletions(-)

diff --git a/src/obj/coff/rewrite.c b/src/obj/coff/rewrite.c @@ -16,6 +16,10 @@ typedef struct CoffRewriteView { u16 optional_size; u32 directory_count; u32 size_of_headers; + u32 symbol_table_off; + u32 symbol_count; + u8* string_table; + u32 string_table_size; } CoffRewriteView; static KitStatus coff_bad(CoffRewriteView* v, const char* detail) { @@ -67,23 +71,67 @@ static int coff_rva_file_range(CoffRewriteView* v, u32 rva, u32 size, return 0; } -static int coff_fixed_prefix(const u8* name, const char* prefix) { +static int coff_name_prefix(const u8* name, size_t name_len, + const char* prefix) { size_t n = strlen(prefix); - return n <= 8u && memcmp(name, prefix, n) == 0; + return n <= name_len && memcmp(name, prefix, n) == 0; } -static int coff_debug_section(const u8* sec) { - const u8* name = sec; - return coff_fixed_prefix(name, ".debug") || - coff_fixed_prefix(name, ".zdebug") || - coff_fixed_prefix(name, ".stab"); +static KitStatus coff_section_name(CoffRewriteView* v, const u8* sec, + const u8** name_out, + size_t* name_len_out) { + const u8* raw = sec; + size_t len = 0; + u32 off = 0; + u32 i; + if (raw[0] != '/') { + while (len < 8u && raw[len]) ++len; + *name_out = raw; + *name_len_out = len; + return KIT_OK; + } + + /* Linked PE images produced by LLVM use COFF's /<decimal> string-table + * convention for DWARF section names even though the PE specification only + * blesses it for relocatable objects. Resolve it here so a real external + * producer's .debug_* sections are not mistaken for ordinary load data. */ + if (raw[1] < '0' || raw[1] > '9') return coff_bad(v, "section name"); + for (i = 1; i < 8u && raw[i]; ++i) { + u32 digit; + if (raw[i] < '0' || raw[i] > '9') return coff_bad(v, "section name"); + digit = (u32)(raw[i] - '0'); + if (off > (UINT32_MAX - digit) / 10u) + return coff_bad(v, "section name"); + off = off * 10u + digit; + } + if (!v->string_table || off < 4u || off >= v->string_table_size) + return coff_bad(v, "section name"); + while (off + len < v->string_table_size && v->string_table[off + len]) + ++len; + if (off + len == v->string_table_size) + return coff_bad(v, "section name"); + *name_out = v->string_table + off; + *name_len_out = len; + return KIT_OK; +} + +static KitStatus coff_debug_section(CoffRewriteView* v, const u8* sec, + int* debug_out) { + const u8* name; + size_t name_len; + KitStatus st = coff_section_name(v, sec, &name, &name_len); + if (st != KIT_OK) return st; + *debug_out = coff_name_prefix(name, name_len, ".debug") || + coff_name_prefix(name, name_len, ".zdebug") || + coff_name_prefix(name, name_len, ".stab"); + return KIT_OK; } static KitStatus coff_validate(CoffRewriteView* v, const KitContext* ctx, KitSlice name, const KitSlice* input) { KitStatus st; - u32 peoff, i; - u64 sections_size; + u32 peoff, i, strings_size; + u64 sections_size, symbols_size, strings_off; memset(v, 0, sizeof *v); st = obj_rewrite_init(&v->rw, ctx, name, input); if (st != KIT_OK) return st; @@ -129,6 +177,23 @@ static KitStatus coff_validate(CoffRewriteView* v, const KitContext* ctx, if (raw_size && !obj_rw_range(v->rw.len, raw_off, raw_size)) return coff_bad(v, "section contents"); } + v->symbol_table_off = obj_rw_u32(v->file_header + 8, 0); + v->symbol_count = obj_rw_u32(v->file_header + 12, 0); + if (!v->symbol_table_off && !v->symbol_count) return KIT_OK; + if (!v->symbol_table_off || + v->symbol_count > UINT32_MAX / COFF_SYMBOL_SIZE) + return coff_bad(v, "COFF symbol table"); + symbols_size = (u64)v->symbol_count * COFF_SYMBOL_SIZE; + strings_off = (u64)v->symbol_table_off + symbols_size; + if (!obj_rw_range(v->rw.len, v->symbol_table_off, symbols_size) || + !obj_rw_range(v->rw.len, strings_off, 4u)) + return coff_bad(v, "COFF symbol table"); + strings_size = obj_rw_u32(v->rw.bytes + (size_t)strings_off, 0); + if (strings_size < 4u || + !obj_rw_range(v->rw.len, strings_off, strings_size)) + return coff_bad(v, "COFF string table"); + v->string_table = v->rw.bytes + (size_t)strings_off; + v->string_table_size = strings_size; return KIT_OK; } @@ -189,42 +254,59 @@ static KitStatus coff_strip_debug_sections(CoffRewriteView* v) { for (i = 0; i < v->section_count; ++i) { u8* sec = coff_section(v, i); u32 raw_size, raw_off; - if (!coff_debug_section(sec)) continue; + int debug; + KitStatus st = coff_debug_section(v, sec, &debug); + if (st != KIT_OK) return st; + if (!debug) continue; raw_size = obj_rw_u32(sec + 16, 0); raw_off = obj_rw_u32(sec + 20, 0); if (raw_size) memset(v->rw.bytes + raw_off, 0, raw_size); - /* Leave the ordinal in place for any retained COFF symbol records, but - * make the section non-mapped and contentless. RVA-based loader metadata - * and every non-debug section retain their original layout. */ - memset(sec, 0, 8u); - obj_rw_put_u32(sec + 8, 0, 0); - obj_rw_put_u32(sec + 16, 0, 0); - obj_rw_put_u32(sec + 20, 0, 0); - obj_rw_put_u32(sec + 36, IMAGE_SCN_MEM_DISCARDABLE, 0); + /* PE loaders validate the complete mapped section table. Removing a + * header in place (or leaving a zero-sized hole before later sections) + * makes otherwise-valid images unloadable. Keep the ordinal, RVA, raw + * span, and permissions intact, but zero the payload and replace any + * short or string-table-backed debug name with an inline neutral name. */ + memcpy(sec, ".discard", 8u); ++v->rw.report.removed_debug_sections; } return KIT_OK; } static KitStatus coff_strip_symbol_table(CoffRewriteView* v) { - u32 off = obj_rw_u32(v->file_header + 8, 0); - u32 count = obj_rw_u32(v->file_header + 12, 0); - u64 symbols_size, end, total; - u32 strings_size; + u32 off = v->symbol_table_off; + u32 count = v->symbol_count; + u64 symbols_size, total; + u32 i; + int needs_strings = 0; if (!off && !count) return KIT_OK; - if (!off || count > UINT32_MAX / COFF_SYMBOL_SIZE) - return coff_bad(v, "COFF symbol table"); symbols_size = (u64)count * COFF_SYMBOL_SIZE; - end = (u64)off + symbols_size; - if (!obj_rw_range(v->rw.len, off, symbols_size) || - !obj_rw_range(v->rw.len, end, 4u)) - return coff_bad(v, "COFF symbol table"); - strings_size = obj_rw_u32(v->rw.bytes + (size_t)end, 0); - if (strings_size < 4u || !obj_rw_range(v->rw.len, end, strings_size)) - return coff_bad(v, "COFF string table"); - total = symbols_size + strings_size; - memset(v->rw.bytes + off, 0, (size_t)total); - obj_rw_put_u32(v->file_header + 8, 0, 0); + total = symbols_size + v->string_table_size; + for (i = 0; i < v->section_count; ++i) { + const u8* sec = coff_section(v, i); + if (sec[0] == '/') { + const u8* resolved; + size_t resolved_len; + KitStatus st = coff_section_name(v, sec, &resolved, &resolved_len); + if (st != KIT_OK) return st; + (void)resolved; + (void)resolved_len; + needs_strings = 1; + } + } + if (needs_strings) { + /* NumberOfSymbols controls where readers locate the string table. Move it + * over the removed symbol records so /offset section names keep resolving + * with a zero symbol count. The fixed file extent remains transactional. */ + memmove(v->rw.bytes + off, v->string_table, v->string_table_size); + memset(v->rw.bytes + off + v->string_table_size, 0, + (size_t)total - v->string_table_size); + v->string_table = v->rw.bytes + off; + } else { + memset(v->rw.bytes + off, 0, (size_t)total); + obj_rw_put_u32(v->file_header + 8, 0, 0); + v->string_table = NULL; + v->string_table_size = 0; + } obj_rw_put_u32(v->file_header + 12, 0, 0); v->rw.report.removed_symbols += count; return KIT_OK; diff --git a/test/api/object_rewrite_test.c b/test/api/object_rewrite_test.c @@ -62,6 +62,7 @@ static void put_section(uint8_t* pe, uint32_t index, const char* name, static void build_signed_debug_pe(uint8_t pe[PE_SIZE]) { uint8_t* debug_entry; + uint8_t* string_table; memset(pe, 0, PE_SIZE); put16(pe, 0x5a4du); put32(pe + 0x3c, PE_OFF); @@ -83,9 +84,9 @@ static void build_signed_debug_pe(uint8_t pe[PE_SIZE]) { put_dir(pe, 9u, 0x55550000u, 0x66u); /* TLS metadata sentinel */ put_section(pe, 0u, ".text", 0x80u, 0x1000u, 0x200u, 0x200u, 0x60000020u); - put_section(pe, 1u, ".debug", 0x80u, 0x2000u, 0x200u, DEBUG_RAW, + put_section(pe, 1u, "/4", 0x80u, 0x2000u, 0x200u, DEBUG_RAW, 0x42000040u); - put_section(pe, 2u, ".rdata", 0x80u, 0x3000u, 0x200u, RDATA_RAW, + put_section(pe, 2u, "/16", 0x80u, 0x3000u, 0x200u, RDATA_RAW, 0x40000040u); memset(pe + 0x200u, 0xa5, 0x200u); memset(pe + DEBUG_RAW, 0xdb, 0x200u); @@ -96,7 +97,10 @@ static void build_signed_debug_pe(uint8_t pe[PE_SIZE]) { put32(debug_entry + 24, 0x420u); memset(pe + CERT_RAW, 0xce, 0x80u); memset(pe + SYMTAB_RAW, 0x5a, 18u); - put32(pe + SYMTAB_RAW + 18u, 4u); + string_table = pe + SYMTAB_RAW + 18u; + put32(string_table, 31u); + memcpy(string_table + 4u, ".debug_info", 12u); + memcpy(string_table + 16u, ".readonly_data", 15u); } static int all_byte(const uint8_t* p, size_t n, uint8_t value) { @@ -163,6 +167,8 @@ static void check_strip_debug(KitUnit* u, const uint8_t* pe) { "mapped text bytes preserved"); CU_EXPECT(u, all_byte(bytes + DEBUG_RAW, 0x200u, 0), "debug section bytes removed"); + CU_EXPECT(u, memcmp(bytes + SEC_HDR + 40u, ".discard", 8u) == 0, + "long-name debug section header neutralized"); CU_EXPECT(u, all_byte(bytes + CERT_RAW, 0x80u, 0), "certificate bytes removed"); CU_EXPECT(u, get32(bytes + OPT_HDR + 112u + 4u * 8u) == 0u && @@ -199,11 +205,19 @@ static void check_strip_all(KitUnit* u, const uint8_t* pe) { "strip-all rewrite succeeds"); bytes = kit_writer_mem_bytes(out, &len); CU_EXPECT(u, len == PE_SIZE, "strip-all preserves PE extent"); - CU_EXPECT(u, get32(bytes + FILE_HDR + 8) == 0u && + CU_EXPECT(u, get32(bytes + FILE_HDR + 8) == SYMTAB_RAW && get32(bytes + FILE_HDR + 12) == 0u, - "strip-all clears COFF symbol-table header"); - CU_EXPECT(u, all_byte(bytes + SYMTAB_RAW, 22u, 0), - "strip-all clears COFF symbol and string records"); + "strip-all retains locator for section-name string table"); + CU_EXPECT(u, all_byte(bytes + DEBUG_RAW, 0x200u, 0), + "strip-all removes long-name debug section"); + CU_EXPECT(u, memcmp(bytes + SEC_HDR + 40u, ".discard", 8u) == 0, + "strip-all neutralizes long-name debug section header"); + CU_EXPECT(u, get32(bytes + SYMTAB_RAW) == 31u && + memcmp(bytes + SYMTAB_RAW + 16u, ".readonly_data", 15u) == + 0, + "strip-all preserves long non-debug section name"); + CU_EXPECT(u, all_byte(bytes + SYMTAB_RAW + 31u, 18u, 0), + "strip-all clears removed COFF symbol records"); CU_EXPECT(u, report.removed_symbols == 1u, "strip-all reports removed symbols"); kit_writer_close(out);