1const builtin = @import("builtin");
2const native_os = builtin.os.tag;
3
4const std = @import("std.zig");
5const Io = std.Io;
6const mem = std.mem;
7const testing = std.testing;
8const elf = std.elf;
9const windows = std.os.windows;
10const posix = std.posix;
11
12/// Cross-platform dynamic library loading and symbol lookup.
13/// Platform-specific functionality is available through the `inner` field.
14pub const DynLib = struct {
15 const InnerType = switch (native_os) {
16 .linux => if (!builtin.link_libc or builtin.abi == .musl and builtin.link_mode == .static)
17 ElfDynLib
18 else
19 DlDynLib,
20 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos, .freebsd, .netbsd, .openbsd, .dragonfly, .illumos => DlDynLib,
21 else => struct {
22 const open = @compileError("unsupported platform");
23 const openZ = @compileError("unsupported platform");
24 },
25 };
26
27 inner: InnerType,
28
29 pub const Error = ElfDynLibError || DlDynLibError;
30
31 /// Trusts the file. Malicious file will be able to execute arbitrary code.
32 pub fn open(path: []const u8) Error!DynLib {
33 if (InnerType == ElfDynLib) {
34 return .{ .inner = try InnerType.open(path, null) };
35 } else {
36 return .{ .inner = try InnerType.open(path) };
37 }
38 }
39
40 /// Trusts the file. Malicious file will be able to execute arbitrary code.
41 pub fn openZ(path_c: [*:0]const u8) Error!DynLib {
42 if (InnerType == ElfDynLib) {
43 return .{ .inner = try InnerType.openZ(path_c, null) };
44 } else {
45 return .{ .inner = try InnerType.openZ(path_c) };
46 }
47 }
48
49 /// Trusts the file.
50 pub fn close(self: *DynLib) void {
51 return self.inner.close();
52 }
53
54 pub fn lookup(self: *DynLib, comptime T: type, name: [:0]const u8) ?T {
55 return self.inner.lookup(T, name);
56 }
57};
58
59// The link_map structure is not completely specified beside the fields
60// reported below, any libc is free to store additional data in the remaining
61// space.
62// An iterator is provided in order to traverse the linked list in a idiomatic
63// fashion.
64const LinkMap = extern struct {
65 addr: usize,
66 name: [*:0]const u8,
67 ld: ?*elf.Dyn,
68 next: ?*LinkMap,
69 prev: ?*LinkMap,
70
71 pub const Iterator = struct {
72 current: ?*LinkMap,
73
74 pub fn end(self: *Iterator) bool {
75 return self.current == null;
76 }
77
78 pub fn next(self: *Iterator) ?*LinkMap {
79 if (self.current) |it| {
80 self.current = it.next;
81 return it;
82 }
83 return null;
84 }
85 };
86};
87
88const RDebug = extern struct {
89 version: i32,
90 map: ?*LinkMap,
91 brk: usize,
92 ldbase: usize,
93};
94
95/// TODO fix comparisons of extern symbol pointers so we don't need this helper function.
96pub fn get_DYNAMIC() ?[*]const elf.Dyn {
97 return @extern([*]const elf.Dyn, .{
98 .name = "_DYNAMIC",
99 .linkage = .weak,
100 .visibility = .hidden,
101 });
102}
103
104pub fn linkmap_iterator() error{InvalidExe}!LinkMap.Iterator {
105 const _DYNAMIC = get_DYNAMIC() orelse {
106 // No PT.DYNAMIC means this is a statically-linked non-PIE program.
107 return .{ .current = null };
108 };
109
110 const link_map_ptr = init: {
111 var i: usize = 0;
112 while (_DYNAMIC[i].d_tag != elf.DT_NULL) : (i += 1) {
113 switch (_DYNAMIC[i].d_tag) {
114 elf.DT_DEBUG => {
115 const ptr = @as(?*RDebug, @ptrFromInt(_DYNAMIC[i].d_val));
116 if (ptr) |r_debug| {
117 if (r_debug.version != 1) return error.InvalidExe;
118 break :init r_debug.map;
119 }
120 },
121 elf.DT_PLTGOT => {
122 const ptr = @as(?[*]usize, @ptrFromInt(_DYNAMIC[i].d_val));
123 if (ptr) |got_table| {
124 // The address to the link_map structure is stored in
125 // the second slot
126 break :init @as(?*LinkMap, @ptrFromInt(got_table[1]));
127 }
128 },
129 else => {},
130 }
131 }
132 return .{ .current = null };
133 };
134
135 return .{ .current = link_map_ptr };
136}
137
138/// Separated to avoid referencing `ElfDynLib`, because its field types may not
139/// be valid on other targets.
140const ElfDynLibError = error{
141 FileTooBig,
142 NotElfFile,
143 NotDynamicLibrary,
144 MissingDynamicLinkingInformation,
145 ElfStringSectionNotFound,
146 ElfSymSectionNotFound,
147 ElfHashTableNotFound,
148 Canceled,
149 Streaming,
150} || Io.File.OpenError || posix.MMapError;
151
152pub const ElfDynLib = struct {
153 strings: [*:0]u8,
154 syms: [*]elf.Sym,
155 hash_table: HashTable,
156 versym: ?[*]elf.Versym,
157 verdef: ?*elf.Verdef,
158 memory: []align(std.heap.page_size_min) u8,
159
160 pub const Error = ElfDynLibError;
161
162 const HashTable = union(enum) {
163 dt_hash: [*]posix.Elf_Symndx,
164 dt_gnu_hash: *elf.gnu_hash.Header,
165 };
166
167 fn openPath(io: Io, path: []const u8) !Io.Dir {
168 if (path.len == 0) return error.NotDir;
169 var parts = std.mem.tokenizeScalar(u8, path, '/');
170 var parent = if (path[0] == '/') try Io.Dir.cwd().openDir(io, "/", .{}) else Io.Dir.cwd();
171 while (parts.next()) |part| {
172 const child = try parent.openDir(io, part, .{});
173 parent.close(io);
174 parent = child;
175 }
176 return parent;
177 }
178
179 fn resolveFromSearchPath(io: Io, search_path: []const u8, file_name: []const u8, delim: u8) ?Io.File {
180 var paths = std.mem.tokenizeScalar(u8, search_path, delim);
181 while (paths.next()) |p| {
182 var dir = openPath(io, p) catch continue;
183 defer dir.close(io);
184 return dir.openFile(io, file_name, .{}) catch continue;
185 }
186 return null;
187 }
188
189 fn resolveFromParent(io: Io, dir_path: []const u8, file_name: []const u8) ?Io.File {
190 var dir = Io.Dir.cwd().openDir(io, dir_path, .{}) catch return null;
191 defer dir.close(io);
192 return dir.openFile(io, file_name, .{}) catch null;
193 }
194
195 // This implements enough to be able to load system libraries in general
196 // Places where it differs from dlopen:
197 // - DT_RPATH of the calling binary is not used as a search path
198 // - DT_RUNPATH of the calling binary is not used as a search path
199 // - /etc/ld.so.cache is not read
200 fn resolveFromName(io: Io, path_or_name: []const u8, LD_LIBRARY_PATH: ?[]const u8) !Io.File {
201 if (std.mem.findScalarPos(u8, path_or_name, 0, '/')) |_| {
202 return Io.Dir.cwd().openFile(io, path_or_name, .{});
203 }
204
205 // Only read LD_LIBRARY_PATH if the binary is not setuid/setgid
206 if (std.os.linux.geteuid() == std.os.linux.getuid() and
207 std.os.linux.getegid() == std.os.linux.getgid())
208 {
209 if (LD_LIBRARY_PATH) |ld_library_path| {
210 if (resolveFromSearchPath(io, ld_library_path, path_or_name, ':')) |file| {
211 return file;
212 }
213 }
214 }
215
216 if (resolveFromParent(io, "/lib", path_or_name)) |file| return file;
217 if (resolveFromParent(io, "/lib64", path_or_name)) |file| return file;
218 if (resolveFromParent(io, "/usr/lib", path_or_name)) |file| return file;
219 if (resolveFromParent(io, "/usr/lib64", path_or_name)) |file| return file;
220 return error.FileNotFound;
221 }
222
223 /// Trusts the file. Malicious file will be able to execute arbitrary code.
224 pub fn open(path: []const u8, LD_LIBRARY_PATH: ?[]const u8) Error!ElfDynLib {
225 const io = std.Options.debug_io;
226
227 const file = try resolveFromName(io, path, LD_LIBRARY_PATH);
228 defer file.close(io);
229
230 const stat = try file.stat(io);
231 const size = std.math.cast(usize, stat.size) orelse return error.FileTooBig;
232
233 const page_size = std.heap.pageSize();
234
235 // This one is to read the ELF info. We do more mmapping later
236 // corresponding to the actual LOAD sections.
237 const file_bytes = try posix.mmap(
238 null,
239 mem.alignForward(usize, size, page_size),
240 .{ .READ = true },
241 .{ .TYPE = .PRIVATE },
242 file.handle,
243 0,
244 );
245 defer posix.munmap(file_bytes);
246
247 const eh = @as(*elf.Ehdr, @ptrCast(file_bytes.ptr));
248 if (!mem.eql(u8, eh.e_ident[0..4], elf.MAGIC)) return error.NotElfFile;
249 if (eh.e_type != elf.ET.DYN) return error.NotDynamicLibrary;
250
251 const elf_addr = @intFromPtr(file_bytes.ptr);
252
253 // Iterate over the program header entries to find out the
254 // dynamic vector as well as the total size of the virtual memory.
255 var maybe_dynv: ?[*]usize = null;
256 var virt_addr_end: usize = 0;
257 {
258 var i: usize = 0;
259 var ph_addr: usize = elf_addr + eh.e_phoff;
260 while (i < eh.e_phnum) : ({
261 i += 1;
262 ph_addr += eh.e_phentsize;
263 }) {
264 const ph = @as(*elf.ElfN.Phdr, @ptrFromInt(ph_addr));
265 switch (ph.type) {
266 .LOAD => virt_addr_end = @max(virt_addr_end, ph.vaddr + ph.memsz),
267 .DYNAMIC => maybe_dynv = @as([*]usize, @ptrFromInt(elf_addr + ph.offset)),
268 else => {},
269 }
270 }
271 }
272 const dynv = maybe_dynv orelse return error.MissingDynamicLinkingInformation;
273
274 // Reserve the entire range (with no permissions) so that we can do MAP.FIXED below.
275 const all_loaded_mem = try posix.mmap(
276 null,
277 virt_addr_end,
278 .{},
279 .{ .TYPE = .PRIVATE, .ANONYMOUS = true },
280 -1,
281 0,
282 );
283 errdefer posix.munmap(all_loaded_mem);
284
285 const base = @intFromPtr(all_loaded_mem.ptr);
286
287 // Now iterate again and actually load all the program sections.
288 {
289 var i: usize = 0;
290 var ph_addr: usize = elf_addr + eh.e_phoff;
291 while (i < eh.e_phnum) : ({
292 i += 1;
293 ph_addr += eh.e_phentsize;
294 }) {
295 const ph = @as(*elf.ElfN.Phdr, @ptrFromInt(ph_addr));
296 switch (ph.type) {
297 .LOAD => {
298 // The VirtAddr may not be page-aligned; in such case there will be
299 // extra nonsense mapped before/after the VirtAddr,MemSiz
300 const aligned_addr = (base + ph.vaddr) & ~(@as(usize, page_size) - 1);
301 const extra_bytes = (base + ph.vaddr) - aligned_addr;
302 const extended_memsz = mem.alignForward(usize, ph.memsz + extra_bytes, page_size);
303 const ptr = @as([*]align(std.heap.page_size_min) u8, @ptrFromInt(aligned_addr));
304 const prot = elfToProt(ph.flags);
305 _ = try posix.mmap(
306 ptr,
307 extended_memsz,
308 prot,
309 .{ .TYPE = .PRIVATE, .FIXED = true },
310 file.handle,
311 ph.offset - extra_bytes,
312 );
313 },
314 else => {},
315 }
316 }
317 }
318
319 var maybe_strings: ?[*:0]u8 = null;
320 var maybe_syms: ?[*]elf.Sym = null;
321 var maybe_hashtab: ?[*]posix.Elf_Symndx = null;
322 var maybe_gnu_hash: ?*elf.gnu_hash.Header = null;
323 var maybe_versym: ?[*]elf.Versym = null;
324 var maybe_verdef: ?*elf.Verdef = null;
325
326 {
327 var i: usize = 0;
328 while (dynv[i] != 0) : (i += 2) {
329 const p = base + dynv[i + 1];
330 switch (dynv[i]) {
331 elf.DT_STRTAB => maybe_strings = @ptrFromInt(p),
332 elf.DT_SYMTAB => maybe_syms = @ptrFromInt(p),
333 elf.DT_HASH => maybe_hashtab = @ptrFromInt(p),
334 elf.DT_GNU_HASH => maybe_gnu_hash = @ptrFromInt(p),
335 elf.DT_VERSYM => maybe_versym = @ptrFromInt(p),
336 elf.DT_VERDEF => maybe_verdef = @ptrFromInt(p),
337 else => {},
338 }
339 }
340 }
341
342 const hash_table: HashTable = if (maybe_gnu_hash) |gnu_hash|
343 .{ .dt_gnu_hash = gnu_hash }
344 else if (maybe_hashtab) |hashtab|
345 .{ .dt_hash = hashtab }
346 else
347 return error.ElfHashTableNotFound;
348
349 return .{
350 .memory = all_loaded_mem,
351 .strings = maybe_strings orelse return error.ElfStringSectionNotFound,
352 .syms = maybe_syms orelse return error.ElfSymSectionNotFound,
353 .hash_table = hash_table,
354 .versym = maybe_versym,
355 .verdef = maybe_verdef,
356 };
357 }
358
359 /// Trusts the file. Malicious file will be able to execute arbitrary code.
360 pub fn openZ(path_c: [*:0]const u8, LD_LIBRARY_PATH: ?[]const u8) Error!ElfDynLib {
361 return open(mem.sliceTo(path_c, 0), LD_LIBRARY_PATH);
362 }
363
364 /// Trusts the file
365 pub fn close(self: *ElfDynLib) void {
366 posix.munmap(self.memory);
367 self.* = undefined;
368 }
369
370 pub fn lookup(self: *const ElfDynLib, comptime T: type, name: [:0]const u8) ?T {
371 if (self.lookupAddress("", name)) |symbol| {
372 return @as(T, @ptrFromInt(symbol));
373 } else {
374 return null;
375 }
376 }
377
378 pub const GnuHashSection32 = struct {
379 symoffset: u32,
380 bloom_shift: u32,
381 bloom: []u32,
382 buckets: []u32,
383 chain: [*]elf.gnu_hash.ChainEntry,
384
385 pub fn fromPtr(header: *elf.gnu_hash.Header) @This() {
386 const header_offset = @intFromPtr(header);
387 const bloom_offset = header_offset + @sizeOf(elf.gnu_hash.Header);
388 const buckets_offset = bloom_offset + header.bloom_size * @sizeOf(u32);
389 const chain_offset = buckets_offset + header.nbuckets * @sizeOf(u32);
390
391 const bloom_ptr: [*]u32 = @ptrFromInt(bloom_offset);
392 const buckets_ptr: [*]u32 = @ptrFromInt(buckets_offset);
393 const chain_ptr: [*]elf.gnu_hash.ChainEntry = @ptrFromInt(chain_offset);
394
395 return .{
396 .symoffset = header.symoffset,
397 .bloom_shift = header.bloom_shift,
398 .bloom = bloom_ptr[0..header.bloom_size],
399 .buckets = buckets_ptr[0..header.nbuckets],
400 .chain = chain_ptr,
401 };
402 }
403 };
404
405 pub const GnuHashSection64 = struct {
406 symoffset: u32,
407 bloom_shift: u32,
408 bloom: []u64,
409 buckets: []u32,
410 chain: [*]elf.gnu_hash.ChainEntry,
411
412 pub fn fromPtr(header: *elf.gnu_hash.Header) @This() {
413 const header_offset = @intFromPtr(header);
414 const bloom_offset = header_offset + @sizeOf(elf.gnu_hash.Header);
415 const buckets_offset = bloom_offset + header.bloom_size * @sizeOf(u64);
416 const chain_offset = buckets_offset + header.nbuckets * @sizeOf(u32);
417
418 const bloom_ptr: [*]u64 = @ptrFromInt(bloom_offset);
419 const buckets_ptr: [*]u32 = @ptrFromInt(buckets_offset);
420 const chain_ptr: [*]elf.gnu_hash.ChainEntry = @ptrFromInt(chain_offset);
421
422 return .{
423 .symoffset = header.symoffset,
424 .bloom_shift = header.bloom_shift,
425 .bloom = bloom_ptr[0..header.bloom_size],
426 .buckets = buckets_ptr[0..header.nbuckets],
427 .chain = chain_ptr,
428 };
429 }
430 };
431
432 /// ElfDynLib specific
433 /// Returns the address of the symbol
434 pub fn lookupAddress(self: *const ElfDynLib, vername: []const u8, name: []const u8) ?usize {
435 const maybe_versym = if (self.verdef == null) null else self.versym;
436
437 const OK_TYPES = (1 << elf.STT_NOTYPE | 1 << elf.STT_OBJECT | 1 << elf.STT_FUNC | 1 << elf.STT_COMMON);
438 const OK_BINDS = (1 << elf.STB_GLOBAL | 1 << elf.STB_WEAK | 1 << elf.STB_GNU_UNIQUE);
439
440 switch (self.hash_table) {
441 .dt_hash => |hashtab| {
442 var i: usize = 0;
443 while (i < hashtab[1]) : (i += 1) {
444 if (0 == (@as(u32, 1) << @as(u5, @intCast(self.syms[i].st_info & 0xf)) & OK_TYPES)) continue;
445 if (0 == (@as(u32, 1) << @as(u5, @intCast(self.syms[i].st_info >> 4)) & OK_BINDS)) continue;
446 if (0 == self.syms[i].st_shndx) continue;
447 if (!mem.eql(u8, name, mem.sliceTo(self.strings + self.syms[i].st_name, 0))) continue;
448 if (maybe_versym) |versym| {
449 if (!checkver(self.verdef.?, versym[i], vername, self.strings))
450 continue;
451 }
452 return @intFromPtr(self.memory.ptr) + self.syms[i].st_value;
453 }
454 },
455 .dt_gnu_hash => |gnu_hash_header| {
456 const GnuHashSection = switch (@bitSizeOf(usize)) {
457 32 => GnuHashSection32,
458 64 => GnuHashSection64,
459 else => |bit_size| @compileError("Unsupported bit size " ++ bit_size),
460 };
461
462 const gnu_hash_section: GnuHashSection = .fromPtr(gnu_hash_header);
463 const hash = elf.gnu_hash.calculate(name);
464
465 const bloom_index = (hash / @bitSizeOf(usize)) % gnu_hash_header.bloom_size;
466 const bloom_val = gnu_hash_section.bloom[bloom_index];
467
468 const bit_index_0 = hash % @bitSizeOf(usize);
469 const bit_index_1 = (hash >> @intCast(gnu_hash_header.bloom_shift)) % @bitSizeOf(usize);
470
471 const one: usize = 1;
472 const bit_mask: usize = (one << @intCast(bit_index_0)) | (one << @intCast(bit_index_1));
473
474 if (bloom_val & bit_mask != bit_mask) {
475 // Symbol is not in bloom filter, so it definitely isn't here.
476 return null;
477 }
478
479 const bucket_index = hash % gnu_hash_header.nbuckets;
480 const chain_index = gnu_hash_section.buckets[bucket_index] - gnu_hash_header.symoffset;
481
482 const chains = gnu_hash_section.chain;
483 const hash_as_entry: elf.gnu_hash.ChainEntry = @bitCast(hash);
484
485 var current_index = chain_index;
486 var at_end_of_chain = false;
487 while (!at_end_of_chain) : (current_index += 1) {
488 const current_entry = chains[current_index];
489 at_end_of_chain = current_entry.end_of_chain;
490
491 if (current_entry.hash != hash_as_entry.hash) continue;
492
493 // check that symbol matches
494 const symbol_index = current_index + gnu_hash_header.symoffset;
495 const symbol = self.syms[symbol_index];
496
497 if (0 == (@as(u32, 1) << @as(u5, @intCast(symbol.st_info & 0xf)) & OK_TYPES)) continue;
498 if (0 == (@as(u32, 1) << @as(u5, @intCast(symbol.st_info >> 4)) & OK_BINDS)) continue;
499 if (0 == symbol.st_shndx) continue;
500
501 const symbol_name = mem.sliceTo(self.strings + symbol.st_name, 0);
502 if (!mem.eql(u8, name, symbol_name)) {
503 continue;
504 }
505
506 if (maybe_versym) |versym| {
507 if (!checkver(self.verdef.?, versym[symbol_index], vername, self.strings)) {
508 continue;
509 }
510 }
511
512 return @intFromPtr(self.memory.ptr) + symbol.st_value;
513 }
514 },
515 }
516
517 return null;
518 }
519
520 fn elfToProt(elf_prot: elf.PF) posix.PROT {
521 return .{
522 .READ = elf_prot.R,
523 .WRITE = elf_prot.W,
524 .EXEC = elf_prot.X,
525 };
526 }
527};
528
529fn checkver(def_arg: *elf.Verdef, vsym_arg: elf.Versym, vername: []const u8, strings: [*:0]u8) bool {
530 var def = def_arg;
531 const vsym_index = vsym_arg.VERSION;
532 while (true) {
533 if (0 == (def.flags & elf.VER_FLG_BASE) and @backingInt(def.ndx) == vsym_index) break;
534 if (def.next == 0) return false;
535 def = @ptrFromInt(@intFromPtr(def) + def.next);
536 }
537 const aux: *elf.Verdaux = @ptrFromInt(@intFromPtr(def) + def.aux);
538 return mem.eql(u8, vername, mem.sliceTo(strings + aux.name, 0));
539}
540
541test "ElfDynLib" {
542 if (native_os != .linux) return error.SkipZigTest;
543 try testing.expectError(error.FileNotFound, ElfDynLib.open("invalid_so.so", null));
544 try testing.expectError(error.FileNotFound, ElfDynLib.openZ("invalid_so.so", null));
545}
546
547/// Separated to avoid referencing `DlDynLib`, because its field types may not
548/// be valid on other targets.
549const DlDynLibError = error{ FileNotFound, NameTooLong };
550
551pub const DlDynLib = struct {
552 pub const Error = DlDynLibError;
553
554 handle: *anyopaque,
555
556 pub fn open(path: []const u8) Error!DlDynLib {
557 const path_c = try posix.toPosixPath(path);
558 return openZ(&path_c);
559 }
560
561 pub fn openZ(path_c: [*:0]const u8) Error!DlDynLib {
562 return .{
563 .handle = std.c.dlopen(path_c, .{ .LAZY = true }) orelse {
564 return error.FileNotFound;
565 },
566 };
567 }
568
569 pub fn close(self: *DlDynLib) void {
570 switch (posix.errno(std.c.dlclose(self.handle))) {
571 .SUCCESS => return,
572 else => unreachable,
573 }
574 self.* = undefined;
575 }
576
577 pub fn lookup(self: *DlDynLib, comptime T: type, name: [:0]const u8) ?T {
578 // dlsym (and other dl-functions) secretly take shadow parameter - return address on stack
579 // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=66826
580 if (@call(.never_tail, std.c.dlsym, .{ self.handle, name.ptr })) |symbol| {
581 return @as(T, @ptrCast(@alignCast(symbol)));
582 } else {
583 return null;
584 }
585 }
586
587 /// DlDynLib specific
588 /// Returns human readable string describing most recent error than occurred from `lookup`
589 /// or `null` if no error has occurred since initialization or when `getError` was last called.
590 pub fn getError() ?[:0]const u8 {
591 return mem.span(std.c.dlerror());
592 }
593};
594
595test "dynamic_library" {
596 const libname = switch (native_os) {
597 .linux, .freebsd, .openbsd, .illumos => "invalid_so.so",
598 .driverkit, .ios, .maccatalyst, .macos, .tvos, .visionos, .watchos => "invalid_dylib.dylib",
599 else => return error.SkipZigTest,
600 };
601
602 try testing.expectError(error.FileNotFound, DynLib.open(libname));
603 try testing.expectError(error.FileNotFound, DynLib.openZ(libname.ptr));
604}