commit 6b470e4919a9d24103069c8beeb741ac0bf9dc9d
parent 3f7a01dcdb627b6713ad17682130a09824bdce21
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Tue, 16 Jun 2026 14:19:26 -0700
link: honor (NOLOAD) output sections for PROGBITS content
A (NOLOAD) output section must contribute zero file bytes and present a
NOBITS-style segment regardless of the input sections' PROGBITS/NOBITS
semantics. Mark every placed unit NOBITS when the output section is
(NOLOAD), keep file_size_accum at 0, and teach link_emit_segment_bytes
to honor the output-section decision (ls->sem) so the byte copy skips
NOLOAD sections (whose segment has no file buffer).
Adds check_noload_progbits (red->green).
Diffstat:
2 files changed, 306 insertions(+), 3 deletions(-)
diff --git a/src/link/link_layout.c b/src/link/link_layout.c
@@ -1289,14 +1289,20 @@ static void link_layout_sections_scripted(Linker* l, LinkImage* img,
ls->flags = s->flags;
ls->align = align;
ls->name = s->name;
- ls->sem = (s->kind == SEC_BSS) ? SSEM_NOBITS : s->sem;
+ /* A (NOLOAD) output section produces no file bytes regardless of
+ * the input section's PROGBITS/NOBITS semantics: mark every
+ * placed unit NOBITS so the byte-copy (link_emit_segment_bytes)
+ * and the segment shape below both treat it as bss. */
+ ls->sem = (os->noload || s->kind == SEC_BSS) ? SSEM_NOBITS
+ : s->sem;
ls->file_offset = ofs - sec_start_dot;
ls->input_offset = ls->file_offset;
map_placed_unit(m, j, aid, lsid);
dot += ls->size;
mem_size_accum = dot - sec_start_dot;
- if (ls->sem != SSEM_NOBITS) file_size_accum = dot - sec_start_dot;
+ if (!os->noload && ls->sem != SSEM_NOBITS)
+ file_size_accum = dot - sec_start_dot;
perms |= (s->flags & (SF_EXEC | SF_WRITE | SF_TLS));
++nsec_in_seg;
}
@@ -1580,7 +1586,12 @@ void link_emit_segment_bytes(Linker* l, LinkImage* img) {
const Section* s = obj_section_get(ob, ls->obj_section_id);
LinkSegment* seg = &img->segments[ls->segment_id - 1];
u8* dst;
- if (!s || s->sem == SSEM_NOBITS || s->kind == SEC_BSS) continue;
+ /* ls->sem carries the OUTPUT-section decision (e.g. (NOLOAD) forces
+ * NOBITS even for PROGBITS input), so check it too — the segment it
+ * lives in has no file bytes to copy into. */
+ if (!s || s->sem == SSEM_NOBITS || s->kind == SEC_BSS ||
+ ls->sem == SSEM_NOBITS)
+ continue;
if (ls->size == 0) continue;
dst = img->segment_bytes[seg->id - 1] +
(size_t)(ls->file_offset - seg->file_offset);
diff --git a/test/link/link_script_test.c b/test/link/link_script_test.c
@@ -211,6 +211,72 @@ fail:
return NULL;
}
+/* Parse `script_text`, link `build_layout_input()` against it, and open the
+ * emitted image. On success *out_f / *out_w / *out_c / *out_s / *out_ob own
+ * resources the caller must release (use emit_cleanup). Returns 1 on success,
+ * 0 if any stage failed (an EXPECT already fired). */
+typedef struct EmitFixture {
+ KitCompiler* c;
+ KitObjBuilder* ob;
+ KitLinkScript* s;
+ KitLinkSession* sess;
+ KitWriter* w;
+ KitObjFile* f;
+} EmitFixture;
+
+static void emit_cleanup(EmitFixture* fx) {
+ if (fx->f) kit_obj_free(fx->f);
+ if (fx->w) kit_writer_close(fx->w);
+ if (fx->sess) kit_link_session_free(fx->sess);
+ if (fx->ob) kit_obj_builder_free(fx->ob);
+ if (fx->s) kit_link_script_free(&g_u.ctx, fx->s);
+ if (fx->c) kit_compiler_free(fx->c);
+ memset(fx, 0, sizeof(*fx));
+}
+
+static int emit_layout(const char* script_text, size_t script_len,
+ EmitFixture* fx) {
+ KitTargetSpec target =
+ kit_unit_target(KIT_ARCH_ARM_64, KIT_OS_LINUX, KIT_OBJ_ELF);
+ KitLinkSessionOptions opts;
+ KitSlice script;
+ KitSlice bytes;
+ size_t len = 0;
+
+ memset(fx, 0, sizeof(*fx));
+ script.s = script_text;
+ script.len = script_len;
+ EXPECT(kit_link_script_parse(&g_u.ctx, script, &fx->s) == KIT_OK && fx->s,
+ "layout script parses");
+ if (!fx->s) return 0;
+ EXPECT(kit_unit_compiler_new(&g_u, target, &fx->c) == KIT_OK && fx->c,
+ "compiler allocation");
+ if (!fx->c) return 0;
+ fx->ob = build_layout_input(fx->c);
+ if (!fx->ob) return 0;
+ memset(&opts, 0, sizeof(opts));
+ opts.output_kind = KIT_LINK_OUTPUT_EXE;
+ opts.linker_script = fx->s;
+ EXPECT(kit_link_session_new(fx->c, &opts, &fx->sess) == KIT_OK && fx->sess,
+ "link session allocation");
+ if (!fx->sess) return 0;
+ EXPECT(kit_link_session_add_obj(fx->sess, fx->ob) == KIT_OK,
+ "add input object");
+ EXPECT(kit_writer_mem(&g_u.heap, &fx->w) == KIT_OK && fx->w, "memory writer");
+ if (!fx->w) return 0;
+ EXPECT(kit_link_session_emit(fx->sess, fx->w) == KIT_OK,
+ "emit scripted image");
+ bytes.data = kit_writer_mem_bytes(fx->w, &len);
+ bytes.len = len;
+ EXPECT(bytes.data && bytes.len > 0, "scripted image bytes");
+ if (!bytes.data || bytes.len == 0) return 0;
+ EXPECT(kit_obj_open(&g_u.ctx, KIT_SLICE_LIT("<scripted-layout>"), &bytes,
+ &fx->f) == KIT_OK &&
+ fx->f,
+ "open scripted image");
+ return fx->f != NULL;
+}
+
static void check_scripted_layout_vma_lma(void) {
static const char script_text[] =
"OUTPUT_FORMAT(\"elf64-littleaarch64\")\n"
@@ -314,10 +380,236 @@ done:
if (c) kit_compiler_free(c);
}
+/* Item 1: a PROGBITS-content output section marked (NOLOAD) must contribute
+ * zero file bytes and present a NOBITS-style segment (file_size 0, vsize > 0). */
+static void check_noload_progbits(void) {
+ static const char script_text[] =
+ "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n"
+ "OUTPUT_ARCH(aarch64)\n"
+ "ENTRY(_start)\n"
+ "SECTIONS {\n"
+ " . = 0x10000;\n"
+ " .text : { *(.text .text.*) }\n"
+ " .shadow (NOLOAD) : { *(.data .data.*) }\n"
+ "}\n";
+ EmitFixture fx;
+ KitObjSegIter* it = NULL;
+ int saw_shadow = 0;
+ uint64_t text_end = 0;
+
+ if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done;
+ EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator");
+ if (!it) goto done;
+ for (;;) {
+ KitObjSegInfo seg;
+ KitIterResult r = kit_obj_segiter_next(it, &seg);
+ if (r == KIT_ITER_END) break;
+ if (r != KIT_ITER_ITEM) break;
+ if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue;
+ if (seg.file_size == 4u && seg.perms == (KIT_SEG_R | KIT_SEG_X))
+ text_end = seg.vaddr + seg.vsize;
+ /* the shadow segment carries the .data bytes (4) as pure bss */
+ if (seg.vsize == 4u && seg.file_size == 0u &&
+ (seg.perms & KIT_SEG_X) == 0) {
+ saw_shadow = 1;
+ EXPECT(seg.file_size == 0u, "NOLOAD section contributes no file bytes");
+ EXPECT(seg.vsize == 4u, "NOLOAD section keeps memory size");
+ }
+ }
+ EXPECT(text_end != 0u, "saw .text LOAD before shadow");
+ EXPECT(saw_shadow, "saw NOLOAD shadow segment with zero file size");
+
+done:
+ if (it) kit_obj_segiter_free(it);
+ emit_cleanup(&fx);
+}
+
+/* Item 2: an inter-section `. = ALIGN(0x1000)` written between two output
+ * sections must advance the location counter at its textual position so the
+ * following section lands at the aligned address. */
+static void check_intersection_dot_align(void) {
+ static const char script_text[] =
+ "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n"
+ "OUTPUT_ARCH(aarch64)\n"
+ "ENTRY(_start)\n"
+ "SECTIONS {\n"
+ " . = 0x10000;\n"
+ " .text : { *(.text .text.*) }\n"
+ " . = ALIGN(0x1000);\n"
+ " .data : { *(.data .data.*) }\n"
+ "}\n";
+ EmitFixture fx;
+ KitObjSegIter* it = NULL;
+ int saw_text = 0;
+ int saw_data = 0;
+
+ if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done;
+ EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator");
+ if (!it) goto done;
+ for (;;) {
+ KitObjSegInfo seg;
+ KitIterResult r = kit_obj_segiter_next(it, &seg);
+ if (r == KIT_ITER_END) break;
+ if (r != KIT_ITER_ITEM) break;
+ if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue;
+ if (seg.vaddr == 0x10000u) saw_text = 1;
+ if (seg.perms & KIT_SEG_W) {
+ saw_data = 1;
+ EXPECT(seg.vaddr == 0x11000u,
+ ".data lands at . = ALIGN(0x1000) after .text");
+ }
+ }
+ EXPECT(saw_text, "saw .text at 0x10000");
+ EXPECT(saw_data, "saw .data aligned to 0x1000");
+
+done:
+ if (it) kit_obj_segiter_free(it);
+ emit_cleanup(&fx);
+}
+
+/* Item 3: a multi-byte fill value must be laid down as a repeating big-endian
+ * pattern across an in-section gap rather than truncated to its low byte. */
+static void check_multibyte_fill(void) {
+ /* .text has 4 bytes; a `. = . + 8` gap then 4 more bytes (.data). The gap
+ * is part of the .text output section and must be filled with the FILL
+ * pattern (0x12345678, big-endian, repeated). */
+ static const char script_text[] =
+ "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n"
+ "OUTPUT_ARCH(aarch64)\n"
+ "ENTRY(_start)\n"
+ "SECTIONS {\n"
+ " . = 0x10000;\n"
+ " .text : {\n"
+ " *(.text .text.*)\n"
+ " . = . + 8;\n"
+ " *(.data .data.*)\n"
+ " FILL(0x12345678)\n"
+ " }\n"
+ "}\n";
+ EmitFixture fx;
+ KitObjSegIter* it = NULL;
+ KitSlice img;
+ size_t len = 0;
+ uint64_t seg_off = 0;
+ int found = 0;
+
+ if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done;
+ EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator");
+ if (!it) goto done;
+ for (;;) {
+ KitObjSegInfo seg;
+ KitIterResult r = kit_obj_segiter_next(it, &seg);
+ if (r == KIT_ITER_END) break;
+ if (r != KIT_ITER_ITEM) break;
+ if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue;
+ if (seg.vaddr == 0x10000u) {
+ seg_off = seg.file_off;
+ found = 1;
+ EXPECT(seg.file_size == 16u, ".text segment spans text+gap+data");
+ }
+ }
+ EXPECT(found, "found .text segment");
+ if (!found) goto done;
+ img.data = kit_writer_mem_bytes(fx.w, &len);
+ img.len = len;
+ EXPECT(img.data && (size_t)(seg_off + 16) <= len, "segment bytes in range");
+ if (img.data && (size_t)(seg_off + 16) <= len) {
+ const uint8_t* p = (const uint8_t*)img.data + seg_off;
+ /* bytes [4..12) are the 8-byte gap, filled big-endian repeating
+ * 12 34 56 78 12 34 56 78 */
+ static const uint8_t want[8] = {0x12, 0x34, 0x56, 0x78,
+ 0x12, 0x34, 0x56, 0x78};
+ EXPECT(memcmp(p + 4, want, 8) == 0,
+ "gap filled with repeated big-endian FILL pattern");
+ }
+
+done:
+ if (it) kit_obj_segiter_free(it);
+ emit_cleanup(&fx);
+}
+
+/* Item 4: (a) two output sections naming the same :phdr coalesce into a single
+ * PT_LOAD; (b) a section listing several phdrs appears under each named
+ * segment. */
+static void check_phdr_coalesce_and_multi(void) {
+ static const char script_text[] =
+ "OUTPUT_FORMAT(\"elf64-littleaarch64\")\n"
+ "OUTPUT_ARCH(aarch64)\n"
+ "ENTRY(_start)\n"
+ "PHDRS {\n"
+ " text PT_LOAD FLAGS(5);\n"
+ " data PT_LOAD FLAGS(6);\n"
+ "}\n"
+ "SECTIONS {\n"
+ " . = 0x10000;\n"
+ " .text : { *(.text .text.*) } :text\n"
+ " .also : { *(.data .data.*) } :text :data\n"
+ "}\n";
+ EmitFixture fx;
+ KitObjSegIter* it = NULL;
+ uint32_t nload = 0;
+ uint32_t n_rx = 0; /* PT_LOAD with R|X (the coalesced text phdr) */
+ uint32_t n_rw = 0; /* PT_LOAD with R|W (the data phdr) */
+
+ if (!emit_layout(script_text, sizeof(script_text) - 1u, &fx)) goto done;
+ EXPECT(kit_obj_segiter_new(fx.f, &it) == KIT_OK && it, "segment iterator");
+ if (!it) goto done;
+ for (;;) {
+ KitObjSegInfo seg;
+ KitIterResult r = kit_obj_segiter_next(it, &seg);
+ if (r == KIT_ITER_END) break;
+ if (r != KIT_ITER_ITEM) break;
+ if (!kit_slice_eq_cstr(seg.name, "LOAD")) continue;
+ ++nload;
+ if (seg.perms == (KIT_SEG_R | KIT_SEG_X)) ++n_rx;
+ if (seg.perms == (KIT_SEG_R | KIT_SEG_W)) ++n_rw;
+ }
+ /* .text and .also both name :text -> one R|X PT_LOAD; .also also names
+ * :data -> a second PT_LOAD with the data flags. So exactly 2 PT_LOAD. */
+ EXPECT(nload == 2, "two sections sharing :text coalesce; :data adds one");
+ EXPECT(n_rx == 1, "single coalesced R|X text segment");
+ EXPECT(n_rw == 1, ".also also appears under the :data segment");
+
+done:
+ if (it) kit_obj_segiter_free(it);
+ emit_cleanup(&fx);
+}
+
+/* Item 5: deeply nested parentheses must fail cleanly (diagnostic) rather than
+ * overflow the parser/eval stack. */
+static void check_recursion_depth_guard(void) {
+ /* 512 nested parens around an int — well past the 256 limit. */
+ char buf[2048];
+ size_t pos = 0;
+ unsigned i;
+ KitLinkScript* s = NULL;
+ KitSlice text;
+ static const char prefix[] = "SECTIONS {\n . = ";
+ static const char suffix[] = ";\n}\n";
+ memcpy(buf + pos, prefix, sizeof(prefix) - 1);
+ pos += sizeof(prefix) - 1;
+ for (i = 0; i < 512; ++i) buf[pos++] = '(';
+ buf[pos++] = '0';
+ for (i = 0; i < 512; ++i) buf[pos++] = ')';
+ memcpy(buf + pos, suffix, sizeof(suffix) - 1);
+ pos += sizeof(suffix) - 1;
+ text.s = buf;
+ text.len = pos;
+ /* must return an error, not crash, and not produce a script */
+ EXPECT(kit_link_script_parse(&g_u.ctx, text, &s) != KIT_OK,
+ "deeply nested parens rejected, no crash");
+ if (s) kit_link_script_free(&g_u.ctx, s);
+}
+
int main(void) {
kit_unit_init(&g_u);
check_kernel_script_subset();
check_scripted_layout_vma_lma();
+ check_noload_progbits();
+ check_intersection_dot_align();
+ check_multibyte_fill();
+ check_phdr_coalesce_and_multi();
+ check_recursion_depth_guard();
kit_unit_summary(&g_u, "link_script_test");
return kit_unit_status(&g_u);
}