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:
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);