authorgravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-21 11:01:50+01:00
committergravatar for mlugg@mlugg.co.ukMatthew Lugg <mlugg@mlugg.co.uk> 2026-05-26 06:48:53+01:00
log15314965c2fc45bf52f703046bff138e414e789c
treec338c87939a72a1e1ae83035d552765cfedfc6dd
parent3c6bcad995f26e33d11e7f5c4b28d66ff872b0d1
signaturelock-open Commit is signed but in an unrecognized format.

Elf2: scan input DSO symbol tables


1 files changed, 209 insertions(+), 102 deletions(-)

src/link/Elf2.zig+209-102
......@@ -54,6 +54,14 @@ globals: struct {
5454/// We use a separate hash map for this data rather than storing it in `navs` etc to save memory,
5555/// because the vast majority of nodes which can export global symbols actually will not.
5656node_global_symbols: std.array_hash_map.Auto(MappedFile.Node.Index, String(.strtab)),
57/// Contains all globals symbols defined in any needed DSO. This map serves two purposes:
58///
59/// * If we discover an undefined reference to one of these symbols, we will know the associated
60/// symbol type, which is important because it may cause us to create a PLT entry.
61///
62/// * When emitting a dynamic executable, we can detect which undefined references are resolved by a
63/// linked DSO, so can emit "undefined global symbol" errors for any other undefined references.
64dso_globals: std.array_hash_map.Auto(String(.strtab), std.elf.STT),
5765shstrtab: StringTable,
5866strtab: StringTable,
5967dynstr: StringTable,
......@@ -431,55 +439,60 @@ fn ensureUnusedSymbolCapacity(elf: *Elf, len: u32, kind: enum { all_local, maybe
431439 try elf.shndx.dynsym.get(elf).ni.resize(&elf.mf, gpa, new_size);
432440 }
433441
434 try elf.got.plt.ensureUnusedCapacity(gpa, len);
435 const need_plt_capacity = elf.got.plt.count() + len;
442 try elf.ensureUnusedPltCapacity(len);
443 }
444 },
445 }
446}
447fn ensureUnusedPltCapacity(elf: *Elf, len: u32) !void {
448 const gpa = elf.base.comp.gpa;
436449
437 switch (elf.ehdrField(.machine)) {
438 else => |machine| @panic(@tagName(machine)),
439 .X86_64 => {
440 // Ensure the `.plt` section's node is big enough
441 const plt_need_size: usize = 16 * (1 + need_plt_capacity);
442 _, const plt_cur_size = elf.shndx.plt.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
443 if (plt_cur_size < plt_need_size) {
444 const new_size = plt_need_size +| plt_need_size / MappedFile.growth_factor;
445 try elf.shndx.plt.get(elf).ni.resize(&elf.mf, gpa, new_size);
446 }
450 try elf.got.plt.ensureUnusedCapacity(gpa, len);
451 const need_plt_capacity = elf.got.plt.count() + len;
447452
448 // Ensure the `.got.plt` section's node is big enough
449 const got_plt_need_size: usize = switch (elf.identClass()) {
450 .NONE, _ => unreachable,
451 inline else => |class| @sizeOf(class.ElfN().Addr) * (3 + need_plt_capacity),
452 };
453 _, const got_plt_cur_size = elf.shndx.got_plt.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
454 if (got_plt_cur_size < got_plt_need_size) {
455 const new_size = got_plt_need_size +| got_plt_need_size / MappedFile.growth_factor;
456 try elf.shndx.got_plt.get(elf).ni.resize(&elf.mf, gpa, new_size);
457 }
453 switch (elf.ehdrField(.machine)) {
454 else => |machine| @panic(@tagName(machine)),
455 .X86_64 => {
456 // Ensure the `.plt` section's node is big enough
457 const plt_need_size: usize = 16 * (1 + need_plt_capacity);
458 _, const plt_cur_size = elf.shndx.plt.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
459 if (plt_cur_size < plt_need_size) {
460 const new_size = plt_need_size +| plt_need_size / MappedFile.growth_factor;
461 try elf.shndx.plt.get(elf).ni.resize(&elf.mf, gpa, new_size);
462 }
458463
459 // Ensure the `.plt.sec` section's node is big enough
460 const plt_sec_need_size: usize = 16 * need_plt_capacity;
461 _, const plt_sec_cur_size = elf.shndx.plt_sec.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
462 if (plt_sec_cur_size < plt_sec_need_size) {
463 const new_size = plt_sec_need_size +| plt_sec_need_size / MappedFile.growth_factor;
464 try elf.shndx.plt_sec.get(elf).ni.resize(&elf.mf, gpa, new_size);
465 }
464 // Ensure the `.got.plt` section's node is big enough
465 const got_plt_need_size: usize = switch (elf.identClass()) {
466 .NONE, _ => unreachable,
467 inline else => |class| @sizeOf(class.ElfN().Addr) * (3 + need_plt_capacity),
468 };
469 _, const got_plt_cur_size = elf.shndx.got_plt.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
470 if (got_plt_cur_size < got_plt_need_size) {
471 const new_size = got_plt_need_size +| got_plt_need_size / MappedFile.growth_factor;
472 try elf.shndx.got_plt.get(elf).ni.resize(&elf.mf, gpa, new_size);
473 }
466474
467 // Ensure the `.rela.plt` section's node is big enough
468 const rela_plt_shndx = elf.shndx.got_plt.get(elf).rela_shndx;
469 const rela_plt_need_size: usize = switch (elf.shdrPtr(rela_plt_shndx)) {
470 inline else => |shdr| @intCast(elf.targetLoad(&shdr.entsize) * need_plt_capacity),
471 };
472 _, const rela_plt_cur_size = rela_plt_shndx.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
473 if (rela_plt_cur_size < rela_plt_need_size) {
474 const new_size = rela_plt_need_size +| rela_plt_need_size / MappedFile.growth_factor;
475 try rela_plt_shndx.get(elf).ni.resize(&elf.mf, gpa, new_size);
476 } else {
477 // Still mark `.rela.plt` as resized so that the DT_PLTRELSZ entry can
478 // be updated if we do indeed add a PLT entry.
479 try rela_plt_shndx.get(elf).ni.resized(gpa, &elf.mf);
480 }
481 },
482 }
475 // Ensure the `.plt.sec` section's node is big enough
476 const plt_sec_need_size: usize = 16 * need_plt_capacity;
477 _, const plt_sec_cur_size = elf.shndx.plt_sec.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
478 if (plt_sec_cur_size < plt_sec_need_size) {
479 const new_size = plt_sec_need_size +| plt_sec_need_size / MappedFile.growth_factor;
480 try elf.shndx.plt_sec.get(elf).ni.resize(&elf.mf, gpa, new_size);
481 }
482
483 // Ensure the `.rela.plt` section's node is big enough
484 const rela_plt_shndx = elf.shndx.got_plt.get(elf).rela_shndx;
485 const rela_plt_need_size: usize = switch (elf.shdrPtr(rela_plt_shndx)) {
486 inline else => |shdr| @intCast(elf.targetLoad(&shdr.entsize) * need_plt_capacity),
487 };
488 _, const rela_plt_cur_size = rela_plt_shndx.get(elf).ni.location(&elf.mf).resolve(&elf.mf);
489 if (rela_plt_cur_size < rela_plt_need_size) {
490 const new_size = rela_plt_need_size +| rela_plt_need_size / MappedFile.growth_factor;
491 try rela_plt_shndx.get(elf).ni.resize(&elf.mf, gpa, new_size);
492 } else {
493 // Still mark `.rela.plt` as resized so that the DT_PLTRELSZ entry can
494 // be updated if we do indeed add a PLT entry.
495 try rela_plt_shndx.get(elf).ni.resized(gpa, &elf.mf);
483496 }
484497 },
485498 }
......@@ -700,6 +713,11 @@ fn addGlobalSymbolAssumeCapacity(elf: *Elf, opts: AddGlobalSymbolOptions) error{
700713 .weak => .WEAK,
701714 };
702715
716 const @"type": std.elf.STT = switch (opts.type) {
717 .NOTYPE => elf.dso_globals.get(opts.name.strtab) orelse .NOTYPE,
718 else => |t| t,
719 };
720
703721 const sym_index: Symbol.Index = @enumFromInt(elf.symtab.items.len);
704722 elf.symtab.appendAssumeCapacity(.{
705723 .node = opts.node,
......@@ -718,7 +736,7 @@ fn addGlobalSymbolAssumeCapacity(elf: *Elf, opts: AddGlobalSymbolOptions) error{
718736 .name = @intFromEnum(opts.name.strtab),
719737 .value = @intCast(opts.value),
720738 .size = @intCast(opts.size),
721 .info = .{ .type = opts.type, .bind = bind },
739 .info = .{ .type = @"type", .bind = bind },
722740 .other = .{ .visibility = opts.visibility },
723741 .shndx = opts.shndx.toSection().?,
724742 };
......@@ -754,7 +772,7 @@ fn addGlobalSymbolAssumeCapacity(elf: *Elf, opts: AddGlobalSymbolOptions) error{
754772 .name = @intFromEnum(opts.name.dynstr),
755773 .value = @intCast(opts.value),
756774 .size = @intCast(opts.size),
757 .info = .{ .type = opts.type, .bind = bind },
775 .info = .{ .type = @"type", .bind = bind },
758776 .other = .{ .visibility = opts.visibility },
759777 .shndx = opts.shndx.toSection().?,
760778 };
......@@ -783,7 +801,7 @@ fn addGlobalSymbolAssumeCapacity(elf: *Elf, opts: AddGlobalSymbolOptions) error{
783801 if (new_global_ptr.dynsym_index != 0 and
784802 opts.visibility == .DEFAULT and
785803 opts.shndx == .UNDEF and
786 opts.type == .FUNC)
804 @"type" == .FUNC)
787805 {
788806 // We're adding an undefined global STT_FUNC symbol which could be resolved by another DSO.
789807 // We therefore might need a PLT entry, so let's add one now. TODO: it'd be good to remove
......@@ -1935,6 +1953,7 @@ fn create(
19351953 .weak_undef = .empty,
19361954 },
19371955 .node_global_symbols = .empty,
1956 .dso_globals = .empty,
19381957 .shstrtab = .{ .map = .empty },
19391958 .strtab = .{ .map = .empty },
19401959 .dynstr = .{ .map = .empty },
......@@ -1981,6 +2000,7 @@ pub fn deinit(elf: *Elf) void {
19812000 elf.globals.strong_undef.deinit(gpa);
19822001 elf.globals.weak_undef.deinit(gpa);
19832002 elf.node_global_symbols.deinit(gpa);
2003 elf.dso_globals.deinit(gpa);
19842004 elf.shstrtab.map.deinit(gpa);
19852005 elf.strtab.map.deinit(gpa);
19862006 elf.dynstr.map.deinit(gpa);
......@@ -3183,7 +3203,14 @@ pub fn loadInput(elf: *Elf, input: link.Input) (Io.File.Reader.SizeError ||
31833203 else => |e| return e,
31843204 };
31853205 },
3186 .dso_exact => |dso_exact| try elf.loadDsoExact(dso_exact.name),
3206 .dso_exact => |dso_exact| {
3207 log.debug("load dso_exact '{f}'", .{std.zig.fmtString(dso_exact.name)});
3208 if (elf.shndx.dynamic != .UNDEF) {
3209 try elf.needed.put(elf.base.comp.gpa, try elf.string(.dynstr, dso_exact.name), {});
3210 }
3211 // TODO: we need to get a resolved file path from the frontend, because we need to read
3212 // the shared object to discover symbol types.
3213 },
31873214 }
31883215}
31893216fn loadArchive(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) !void {
......@@ -3643,6 +3670,7 @@ fn loadObject(
36433670}
36443671fn loadDso(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) !void {
36453672 const comp = elf.base.comp;
3673 const gpa = comp.gpa;
36463674 const diags = &comp.link_diags;
36473675 const r = &fr.interface;
36483676
......@@ -3657,68 +3685,147 @@ fn loadDso(elf: *Elf, path: std.Build.Cache.Path, fr: *Io.File.Reader) !void {
36573685 if (ehdr.type != .DYN) return diags.failParse(path, "unsupported dso type", .{});
36583686 if (ehdr.machine != elf.ehdrField(.machine))
36593687 return diags.failParse(path, "bad machine", .{});
3660 if (ehdr.phoff == 0 or ehdr.phnum <= 1)
3661 return diags.failParse(path, "no program headers", .{});
3662 try fr.seekTo(ehdr.phoff);
3663 const dynamic_ph = for (0..ehdr.phnum) |_| {
3664 const ph = try r.peekStruct(ElfN.Phdr, target_endian);
3665 try r.discardAll(ehdr.phentsize);
3666 switch (ph.type) {
3667 else => {},
3668 .DYNAMIC => break ph,
3688 if (ehdr.shnum > 0) try fr.seekTo(ehdr.shoff);
3689 const dynamic_sh: ElfN.Shdr, const dynsym_sh: ElfN.Shdr = sh: {
3690 var dynamic_sh: ?ElfN.Shdr = null;
3691 var dynsym_sh: ?ElfN.Shdr = null;
3692 for (0..ehdr.shnum) |_| {
3693 const sh = try r.peekStruct(ElfN.Shdr, target_endian);
3694 try r.discardAll(ehdr.shentsize);
3695 switch (sh.type) {
3696 else => {},
3697 .DYNAMIC => dynamic_sh = sh,
3698 .DYNSYM => dynsym_sh = sh,
3699 }
36693700 }
3670 } else return diags.failParse(path, "no dynamic segment", .{});
3701 break :sh .{
3702 dynamic_sh orelse return diags.failParse(path, "missing SHT_DYNAMIC section", .{}),
3703 dynsym_sh orelse return diags.failParse(path, "missing SHT_DYNSYM section", .{}),
3704 };
3705 };
3706 const dynstr_sh: ElfN.Shdr = sh: {
3707 if (dynsym_sh.link >= ehdr.shnum) {
3708 return diags.failParse(path, "bad dynamic string table section index", .{});
3709 }
3710 try fr.seekTo(ehdr.shoff + dynsym_sh.link * ehdr.shentsize);
3711 break :sh try r.peekStruct(ElfN.Shdr, target_endian);
3712 };
3713
3714 if (dynamic_sh.entsize != @sizeOf(ElfN.Addr) * 2) {
3715 return diags.failParse(path, "bad dynamic section entsize", .{});
3716 }
36713717 const dynnum = std.math.divExact(
36723718 u32,
3673 @intCast(dynamic_ph.filesz),
3719 @intCast(dynamic_sh.size),
36743720 @sizeOf(ElfN.Addr) * 2,
36753721 ) catch return diags.failParse(
36763722 path,
3677 "dynamic segment filesz (0x{x}) is not a multiple of entsize (0x{x})",
3678 .{ dynamic_ph.filesz, @sizeOf(ElfN.Addr) * 2 },
3723 "dynamic section size (0x{x}) is not a multiple of entsize (0x{x})",
3724 .{ dynamic_sh.size, @sizeOf(ElfN.Addr) * 2 },
3725 );
3726
3727 if (dynsym_sh.entsize < @sizeOf(ElfN.Sym)) {
3728 return diags.failParse(path, "bad dynsym entsize", .{});
3729 }
3730 const symnum = std.math.divExact(
3731 u32,
3732 @intCast(dynsym_sh.size),
3733 @intCast(dynsym_sh.entsize),
3734 ) catch return diags.failParse(
3735 path,
3736 "dynsym size (0x{x}) is not a multiple of entsize (0x{x})",
3737 .{ dynsym_sh.size, dynsym_sh.entsize },
36793738 );
3680 var strtab: ?ElfN.Addr = null;
3681 var strsz: ?ElfN.Addr = null;
3682 var soname: ?ElfN.Addr = null;
3683 try fr.seekTo(dynamic_ph.offset);
3684 for (0..dynnum) |_| {
3739
3740 const dynstr = try gpa.alloc(u8, @intCast(dynstr_sh.size));
3741 defer gpa.free(dynstr);
3742 try fr.seekTo(dynstr_sh.offset);
3743 try r.readSliceAll(dynstr);
3744
3745 // Find the DT_SONAME dynamic entry so that it can become our DT_NEEDED entry.
3746 try fr.seekTo(dynamic_sh.offset);
3747 const soname: []const u8 = for (0..dynnum) |_| {
36853748 const tag = try r.takeInt(ElfN.Addr, target_endian);
36863749 const val = try r.takeInt(ElfN.Addr, target_endian);
3687 switch (tag) {
3688 else => {},
3689 std.elf.DT_STRTAB => strtab = val,
3690 std.elf.DT_STRSZ => strsz = val,
3691 std.elf.DT_SONAME => soname = val,
3750 if (tag == std.elf.DT_SONAME) {
3751 // val is a dynstr index
3752 if (val >= dynstr.len) {
3753 return diags.failParse(path, "bad soname string", .{});
3754 }
3755 break std.mem.sliceTo(dynstr[@intCast(val)..], 0);
36923756 }
3693 }
3694 if (strtab == null or soname == null)
3695 return elf.loadDsoExact(std.fs.path.basename(path.sub_path));
3696 if (strsz) |size| if (soname.? >= size)
3697 return diags.failParse(path, "bad soname string", .{});
3698 try fr.seekTo(ehdr.phoff);
3699 const ph = for (0..ehdr.phnum) |_| {
3700 const ph = try r.peekStruct(ElfN.Phdr, target_endian);
3701 try r.discardAll(ehdr.phentsize);
3702 switch (ph.type) {
3703 else => {},
3704 .LOAD => if (strtab.? >= ph.vaddr and
3705 strtab.? + (strsz orelse 0) <= ph.vaddr + ph.filesz) break ph,
3757 } else std.fs.path.basename(path.sub_path);
3758 try elf.needed.put(gpa, try elf.string(.dynstr, soname), {});
3759
3760 // Scan the symbol table and populate `elf.dso_globals`.
3761 const first_global = @min(dynsym_sh.info, symnum);
3762 try elf.dso_globals.ensureUnusedCapacity(gpa, symnum - first_global);
3763 try elf.ensureUnusedPltCapacity(symnum - first_global);
3764 try fr.seekTo(dynsym_sh.offset + first_global * dynsym_sh.entsize);
3765 for (first_global..symnum) |_| {
3766 const sym = try r.peekStruct(ElfN.Sym, target_endian);
3767 try r.discardAll(@intCast(dynsym_sh.entsize));
3768
3769 switch (sym.info.bind) {
3770 else => continue,
3771 .GLOBAL, .WEAK, .GNU_UNIQUE => {},
37063772 }
3707 } else return diags.failParse(path, "strtab not part of a loaded segment", .{});
3708 try fr.seekTo(strtab.? + soname.? - ph.vaddr + ph.offset);
3709 return elf.loadDsoExact(r.peekSentinel(0) catch |err| switch (err) {
3710 error.StreamTooLong => return diags.failParse(path, "soname too lang", .{}),
3711 else => |e| return e,
3712 });
3773 // STV_HIDDEN/STV_INTERNAL symbols should be marked as STB_LOCAL and hence skipped
3774 // above, but we might as well double-check.
3775 switch (sym.other.visibility) {
3776 .HIDDEN, .INTERNAL => continue,
3777 .DEFAULT, .PROTECTED => {},
3778 }
3779
3780 if (sym.name >= dynstr.len) {
3781 return diags.failParse(path, "bad symbol name string", .{});
3782 }
3783
3784 const name = try elf.string(.strtab, std.mem.sliceTo(dynstr[sym.name..], 0));
3785 const gop = elf.dso_globals.getOrPutAssumeCapacity(name);
3786 if (!gop.found_existing or gop.value_ptr.* == .NOTYPE) {
3787 gop.value_ptr.* = sym.info.type;
3788 }
3789
3790 // If there's already an undefined symbol by this name of type STT_NOTYPE, populate
3791 // its type now.
3792 update_sym_type: {
3793 const global_ptr = elf.globals.strong_undef.getPtr(name) orelse
3794 elf.globals.weak_undef.getPtr(name) orelse
3795 break :update_sym_type;
3796
3797 if (global_ptr.dynsym_index == 0) break :update_sym_type;
3798
3799 const sym_ptr = @field(elf.symPtr(global_ptr.symtab_index), @tagName(class));
3800 switch (elf.targetLoad(&sym_ptr.other).visibility) {
3801 .HIDDEN, .INTERNAL, .PROTECTED => break :update_sym_type,
3802 .DEFAULT => {},
3803 }
3804
3805 const cur_info = elf.targetLoad(&sym_ptr.info);
3806 if (cur_info.type == .NOTYPE) {
3807 elf.targetStore(&sym_ptr.info, .{
3808 .bind = cur_info.bind,
3809 .type = sym.info.type,
3810 });
3811
3812 const dynsym_ptr = @field(elf.dynsymPtr(global_ptr.dynsym_index), @tagName(class));
3813 elf.targetStore(&dynsym_ptr.info, .{
3814 .bind = elf.targetLoad(&dynsym_ptr.info).bind,
3815 .type = sym.info.type,
3816 });
3817
3818 if (sym.info.type == .FUNC) {
3819 // We've just determined that this symbol actually needs a PLT entry.
3820 elf.addPltEntry(name, global_ptr.dynsym_index);
3821 // TODO: we therefore need to re-apply PLT32 relocs for that symbol!
3822 }
3823 }
3824 }
3825 }
37133826 },
37143827 }
37153828}
3716fn loadDsoExact(elf: *Elf, name: []const u8) !void {
3717 log.debug("loadDsoExact({f})", .{std.zig.fmtString(name)});
3718 if (elf.shndx.dynamic != .UNDEF) {
3719 try elf.needed.put(elf.base.comp.gpa, try elf.string(.dynstr, name), {});
3720 }
3721}
37223829
37233830/// Validates that the `std.elf.Ident` present at the start of `r` is a compatible link input.
37243831///
......@@ -4434,14 +4541,14 @@ pub fn flush(
44344541 _ = arena;
44354542 _ = prog_node;
44364543
4437 if (elf.ehdrField(.type) != .REL and
4438 elf.shndx.dynamic == .UNDEF and
4439 elf.globals.strong_undef.count() > 0)
4440 {
4544 if (comp.config.output_mode == .Exe) {
4545 var any_undef = false;
44414546 for (elf.globals.strong_undef.keys()) |name| {
4547 if (elf.dso_globals.contains(name)) continue;
4548 any_undef = true;
44424549 comp.link_diags.addError("undefined global symbol '{s}'", .{name.slice(elf)});
44434550 }
4444 return error.LinkFailure;
4551 if (any_undef) return error.LinkFailure;
44454552 }
44464553
44474554 while (try elf.idle(tid)) {}