From fa940bafa2720f49ee249eda1ee4cf26a247172a Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Sep 2022 18:02:38 -0700 Subject: [PATCH 1/7] std.zig.system.NativeTargetInfo: improve glibc version detection Previously, this code would fail to detect glibc version because it relied on libc.so.6 being a symlink which revealed the answer. On modern distros, this is no longer the case. This new strategy finds the path to libc.so.6 from /usr/bin/env, then inspects the .dynstr section of libc.so.6, looking for symbols that start with "GLIBC_2.". It then parses those as semantic versions and takes the maximum value as the system-native glibc version. closes #6469 see #11137 closes #12567 --- lib/std/zig/system/NativeTargetInfo.zig | 228 ++++++++++++++++++++---- 1 file changed, 192 insertions(+), 36 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 3aeab0fdba257b6338658f04dacb8c52c1c42e34..67092e17a9f609ecc8a144c6d236ee6e83f19be5 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -28,6 +28,7 @@ pub const DetectError = error{ SystemFdQuotaExceeded, DeviceBusy, OSVersionDetectionFail, + Unexpected, }; /// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected @@ -332,9 +333,7 @@ fn detectAbiAndDynamicLinker( { for (lib_paths) |lib_path| { if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) { - os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName => continue, - error.InvalidGnuLibCVersion => continue, + os_adjusted.version_range.linux.glibc = glibcVerFromSo(lib_path) catch |err| switch (err) { error.GnuLibCVersionUnavailable => continue, else => |e| return e, }; @@ -369,7 +368,7 @@ fn detectAbiAndDynamicLinker( // #! (2) + 255 (max length of shebang line since Linux 5.1) + \n (1) var buffer: [258]u8 = undefined; while (true) { - const file = std.fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) { + const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) { error.NoSpaceLeft => unreachable, error.NameTooLong => unreachable, error.PathAlreadyExists => unreachable, @@ -396,6 +395,7 @@ fn detectAbiAndDynamicLinker( else => |e| return e, }; + errdefer file.close(); const line = file.reader().readUntilDelimiter(&buffer, '\n') catch |err| switch (err) { error.IsDir => unreachable, // Handled before @@ -413,15 +413,12 @@ fn detectAbiAndDynamicLinker( error.NotOpenForReading, => break :blk file, - else => |e| { - file.close(); - return e; - }, + else => |e| return e, }; if (!mem.startsWith(u8, line, "#!")) break :blk file; var it = std.mem.tokenize(u8, line[2..], " "); - file.close(); file_name = it.next() orelse return defaultAbiAndDynamicLinker(cpu, os, cross_target); + file.close(); } }; defer elf_file.close(); @@ -455,23 +452,158 @@ fn detectAbiAndDynamicLinker( const glibc_so_basename = "libc.so.6"; -fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version { - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) { - error.AccessDenied => return error.GnuLibCVersionUnavailable, - error.FileSystem => return error.FileSystem, - error.SymLinkLoop => return error.SymLinkLoop, +fn glibcVerFromSo(so_path: [:0]const u8) !std.builtin.Version { + const file = fs.openFileAbsolute(so_path, .{}) catch |err| switch (err) { + // Contextually impossible errors. + error.NoSpaceLeft => unreachable, error.NameTooLong => unreachable, - error.NotLink => return error.GnuLibCVersionUnavailable, + error.PathAlreadyExists => unreachable, + error.SharingViolation => unreachable, + error.InvalidUtf8 => unreachable, + error.BadPathName => unreachable, + error.PipeBusy => unreachable, + error.FileLocksNotSupported => unreachable, + error.WouldBlock => unreachable, + error.FileBusy => unreachable, // opened without write permissions + error.NoDevice => unreachable, // not accessing special device + error.InvalidHandle => unreachable, // should not be in the error set + error.DeviceBusy => unreachable, // read-only + + // Errors that indicate a false negative may occur if we treat this as + // not a libc shared object. + error.ProcessFdQuotaExceeded => return error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded => return error.SystemFdQuotaExceeded, + error.SystemResources => return error.SystemResources, + error.Unexpected => return error.Unexpected, + + // Errors that indicate this file is not a libc shared object. + error.SymLinkLoop => return error.GnuLibCVersionUnavailable, + error.IsDir => return error.GnuLibCVersionUnavailable, + error.AccessDenied => return error.GnuLibCVersionUnavailable, error.FileNotFound => return error.GnuLibCVersionUnavailable, - error.SystemResources => return error.SystemResources, + error.FileTooBig => return error.GnuLibCVersionUnavailable, error.NotDir => return error.GnuLibCVersionUnavailable, - error.Unexpected => return error.GnuLibCVersionUnavailable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only }; - return glibcVerFromLinkName(link_name, "libc-"); + defer file.close(); + + return glibcVerFromSoFile(file) catch |err| switch (err) { + error.InvalidElfMagic => return error.GnuLibCVersionUnavailable, + error.InvalidElfEndian => return error.GnuLibCVersionUnavailable, + error.InvalidElfClass => return error.GnuLibCVersionUnavailable, + error.InvalidElfFile => return error.GnuLibCVersionUnavailable, + error.InvalidElfVersion => return error.GnuLibCVersionUnavailable, + error.InvalidGnuLibCVersion => return error.GnuLibCVersionUnavailable, + error.UnexpectedEndOfFile => return error.GnuLibCVersionUnavailable, + error.UnableToReadElfFile => return error.GnuLibCVersionUnavailable, + + error.SystemResources => return error.SystemResources, + error.FileSystem => return error.FileSystem, + error.Unexpected => return error.Unexpected, + }; +} + +fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { + var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; + _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len); + const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf); + const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf); + if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic; + const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) { + elf.ELFDATA2LSB => .Little, + elf.ELFDATA2MSB => .Big, + else => return error.InvalidElfEndian, + }; + const need_bswap = elf_endian != native_endian; + if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion; + + const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) { + elf.ELFCLASS32 => false, + elf.ELFCLASS64 => true, + else => return error.InvalidElfClass, + }; + const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); + var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); + const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); + const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); + var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; + if (sh_buf.len < shentsize) return error.InvalidElfFile; + + _ = try preadMin(file, &sh_buf, str_section_off, shentsize); + const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf)); + const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf)); + const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); + const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); + var strtab_buf: [4096:0]u8 = undefined; + const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len); + const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len); + const shstrtab = strtab_buf[0..shstrtab_read_len]; + const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); + var sh_i: u16 = 0; + const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { + // Reserve some bytes so that we can deref the 64-bit struct fields + // even when the ELF file is 32-bits. + const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); + const sh_read_byte_len = try preadMin( + file, + sh_buf[0 .. sh_buf.len - sh_reserve], + shoff, + shentsize, + ); + var sh_buf_i: usize = 0; + while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ + sh_i += 1; + shoff += shentsize; + sh_buf_i += shentsize; + }) { + const sh32 = @ptrCast( + *elf.Elf32_Shdr, + @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]), + ); + const sh64 = @ptrCast( + *elf.Elf64_Shdr, + @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]), + ); + const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); + // TODO this pointer cast should not be necessary + const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0); + if (mem.eql(u8, sh_name, ".dynstr")) { + break :find_dyn_str .{ + .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), + .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), + }; + } + } + } else return error.InvalidGnuLibCVersion; + + // Here we loop over all the strings in the dynstr string table, assuming that any + // strings that start with "GLIBC_2." indicate the existence of such a glibc version, + // and furthermore, that the system-installed glibc is at minimum that version. + + // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. + // Here I use this value plus some headroom. This makes it only need + // a single read syscall here. + var buf: [40000]u8 = undefined; + if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion; + + const dynstr_bytes = buf[0..dynstr.size]; + _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr.size); + var it = mem.split(u8, dynstr_bytes, &.{0}); + var max_ver: std.builtin.Version = .{ .major = 2, .minor = 2, .patch = 5 }; + while (it.next()) |s| { + if (mem.startsWith(u8, s, "GLIBC_2.")) { + const chopped = s["GLIBC_".len..]; + const ver = std.builtin.Version.parse(chopped) catch |err| switch (err) { + error.Overflow => return error.InvalidGnuLibCVersion, + error.InvalidCharacter => return error.InvalidGnuLibCVersion, + error.InvalidVersion => return error.InvalidGnuLibCVersion, + }; + switch (ver.order(max_ver)) { + .gt => max_ver = ver, + .lt, .eq => continue, + } + } + } + return max_ver; } fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) !std.builtin.Version { @@ -735,36 +867,60 @@ pub fn abiAndDynamicLinkerFromFile( }; defer dir.close(); - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlinkatZ( - dir.fd, - glibc_so_basename, - &link_buf, - ) catch |err| switch (err) { + // Now we have a candidate for the path to libc shared object. In + // the past, we used readlink() here because the link name would + // reveal the glibc version. However, in more recent GNU/Linux + // installations, there is no symlink. Thus we instead use a more + // robust check of opening the libc shared object and looking at the + // .dynstr section, and finding the max version number of symbols + // that start with "GLIBC_2.". + var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { error.NameTooLong => unreachable, error.InvalidUtf8 => unreachable, // Windows only error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only + error.PipeBusy => unreachable, // Windows-only + error.SharingViolation => unreachable, // Windows-only + error.FileLocksNotSupported => unreachable, // No lock requested. + error.NoSpaceLeft => unreachable, // read-only + error.PathAlreadyExists => unreachable, // read-only + error.DeviceBusy => unreachable, // read-only + error.FileBusy => unreachable, // read-only + error.InvalidHandle => unreachable, // should not be in the error set + error.WouldBlock => unreachable, // not using O_NONBLOCK + error.NoDevice => unreachable, // not asking for a special device error.AccessDenied, error.FileNotFound, - error.NotLink, error.NotDir, => continue, + error.IsDir => return error.InvalidElfFile, + error.FileTooBig => return error.Unexpected, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, error.SystemResources, - error.FileSystem, error.SymLinkLoop, error.Unexpected, => |e| return e, }; - result.target.os.version_range.linux.glibc = glibcVerFromLinkName( - link_name, - "libc-", - ) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName, + defer f.close(); + + result.target.os.version_range.linux.glibc = glibcVerFromSoFile(f) catch |err| switch (err) { + error.InvalidElfMagic, + error.InvalidElfEndian, + error.InvalidElfClass, + error.InvalidElfFile, + error.InvalidElfVersion, error.InvalidGnuLibCVersion, + error.UnexpectedEndOfFile, => continue, + + error.SystemResources, + error.UnableToReadElfFile, + error.Unexpected, + error.FileSystem, + => |e| return e, }; break; } -- 2.54.0 From 3ee01c14ee7ba42b484f15daeacb67da90a81c9e Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Sep 2022 18:27:26 -0700 Subject: [PATCH 2/7] std.zig.system.NativeTargetInfo: detection ignores self exe Before, native glibc and dynamic linker detection attempted to use the executable's own binary if it was dynamically linked to answer both the C ABI question and the dynamic linker question. However, this could be problematic on a system that uses a RUNPATH for the compiler binary, locking it to an older glibc version, while system binaries such as /usr/bin/env use a newer glibc version. The problem is that libc.so.6 glibc version will match that of the system while the dynamic linker will match that of the compiler binary. Executables with these versions mismatching will fail to run. Therefore, this commit changes the logic to be the same regardless of whether the compiler binary is dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the answer to these questions, or if there is a shebang line, then it chases the referenced file recursively. If that does not provide the answer, then the function falls back to defaults. This commit also solves a TODO to remove an Allocator parameter to the detect() function. --- doc/docgen.zig | 3 +- lib/std/build.zig | 4 +- lib/std/build/EmulatableRunStep.zig | 2 +- lib/std/zig/system/NativeTargetInfo.zig | 134 +++--------------------- src/main.zig | 17 ++- src/test.zig | 4 +- 6 files changed, 31 insertions(+), 133 deletions(-) diff --git a/doc/docgen.zig b/doc/docgen.zig index 0f0e212e3c244ce2ba86d0d6fc27a949d3fa5d99..50000da44c5548cc19c7c9999666e592cbbaa8fa 100644 --- a/doc/docgen.zig +++ b/doc/docgen.zig @@ -1210,7 +1210,7 @@ fn genHtml( var env_map = try process.getEnvMap(allocator); try env_map.put("ZIG_DEBUG_COLOR", "1"); - const host = try std.zig.system.NativeTargetInfo.detect(allocator, .{}); + const host = try std.zig.system.NativeTargetInfo.detect(.{}); const builtin_code = try getBuiltinCode(allocator, &env_map, zig_exe); for (toc.nodes) |node| { @@ -1474,7 +1474,6 @@ fn genHtml( .arch_os_abi = triple, }); const target_info = try std.zig.system.NativeTargetInfo.detect( - allocator, cross_target, ); switch (host.getExternalExecutor(target_info, .{ diff --git a/lib/std/build.zig b/lib/std/build.zig index 4c05586159c6e0aa3ac9afa70f1be5a712bbfb14..f11dba717d2b60de79a640acf9cc6c31b98babf6 100644 --- a/lib/std/build.zig +++ b/lib/std/build.zig @@ -171,7 +171,7 @@ pub const Builder = struct { const env_map = try allocator.create(EnvMap); env_map.* = try process.getEnvMap(allocator); - const host = try NativeTargetInfo.detect(allocator, .{}); + const host = try NativeTargetInfo.detect(.{}); const self = try allocator.create(Builder); self.* = Builder{ @@ -1798,7 +1798,7 @@ pub const LibExeObjStep = struct { } fn computeOutFileNames(self: *LibExeObjStep) void { - self.target_info = NativeTargetInfo.detect(self.builder.allocator, self.target) catch + self.target_info = NativeTargetInfo.detect(self.target) catch unreachable; const target = self.target_info.target; diff --git a/lib/std/build/EmulatableRunStep.zig b/lib/std/build/EmulatableRunStep.zig index 0479d3a2f0d5f43fd26e1889f1e52f9fb1265403..23bdf5e59561c6e44ca1cad621409bd67f2d1774 100644 --- a/lib/std/build/EmulatableRunStep.zig +++ b/lib/std/build/EmulatableRunStep.zig @@ -158,7 +158,7 @@ fn warnAboutForeignBinaries(step: *EmulatableRunStep) void { const host_name = builder.host.target.zigTriple(builder.allocator) catch unreachable; const foreign_name = artifact.target.zigTriple(builder.allocator) catch unreachable; - const target_info = std.zig.system.NativeTargetInfo.detect(builder.allocator, artifact.target) catch unreachable; + const target_info = std.zig.system.NativeTargetInfo.detect(artifact.target) catch unreachable; const need_cross_glibc = artifact.target.isGnuLibC() and artifact.is_linking_libc; switch (builder.host.getExternalExecutor(target_info, .{ .qemu_fixes_dl = need_cross_glibc and builder.glibc_runtimes_dir != null, diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 67092e17a9f609ecc8a144c6d236ee6e83f19be5..5ed4a02f745ff8274e878fec3bc981f21614568e 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -37,8 +37,7 @@ pub const DetectError = error{ /// relative to that. /// Any resources this function allocates are released before returning, and so there is no /// deinitialization method. -/// TODO Remove the Allocator requirement from this function. -pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!NativeTargetInfo { +pub fn detect(cross_target: CrossTarget) DetectError!NativeTargetInfo { var os = cross_target.getOsTag().defaultVersionRange(cross_target.getCpuArch()); if (cross_target.os_tag == null) { switch (builtin.target.os.tag) { @@ -199,7 +198,7 @@ pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!Nativ } orelse backup_cpu_detection: { break :backup_cpu_detection Target.Cpu.baseline(cpu_arch); }; - var result = try detectAbiAndDynamicLinker(allocator, cpu, os, cross_target); + var result = try detectAbiAndDynamicLinker(cpu, os, cross_target); // For x86, we need to populate some CPU feature flags depending on architecture // and mode: // * 16bit_mode => if the abi is code16 @@ -236,13 +235,20 @@ pub fn detect(allocator: Allocator, cross_target: CrossTarget) DetectError!Nativ return result; } -/// First we attempt to use the executable's own binary. If it is dynamically -/// linked, then it should answer both the C ABI question and the dynamic linker question. -/// If it is statically linked, then we try /usr/bin/env (or the file it references in shebang). If that does not provide the answer, then -/// we fall back to the defaults. -/// TODO Remove the Allocator requirement from this function. +/// In the past, this function attempted to use the executable's own binary if it was dynamically +/// linked to answer both the C ABI question and the dynamic linker question. However, this +/// could be problematic on a system that uses a RUNPATH for the compiler binary, locking +/// it to an older glibc version, while system binaries such as /usr/bin/env use a newer glibc +/// version. The problem is that libc.so.6 glibc version will match that of the system while +/// the dynamic linker will match that of the compiler binary. Executables with these versions +/// mismatching will fail to run. +/// +/// Therefore, this function works the same regardless of whether the compiler binary is +/// dynamically or statically linked. It inspects `/usr/bin/env` as an ELF file to find the +/// answer to these questions, or if there is a shebang line, then it chases the referenced +/// file recursively. If that does not provide the answer, then the function falls back to +/// defaults. fn detectAbiAndDynamicLinker( - allocator: Allocator, cpu: Target.Cpu, os: Target.Os, cross_target: CrossTarget, @@ -280,8 +286,8 @@ fn detectAbiAndDynamicLinker( const ofmt = cross_target.ofmt orelse Target.ObjectFormat.default(os.tag, cpu.arch); for (all_abis) |abi| { - // This may be a nonsensical parameter. We detect this with error.UnknownDynamicLinkerPath and - // skip adding it to `ld_info_list`. + // This may be a nonsensical parameter. We detect this with + // error.UnknownDynamicLinkerPath and skip adding it to `ld_info_list`. const target: Target = .{ .cpu = cpu, .os = os, @@ -301,62 +307,6 @@ fn detectAbiAndDynamicLinker( // Best case scenario: the executable is dynamically linked, and we can iterate // over our own shared objects and find a dynamic linker. - self_exe: { - const lib_paths = try std.process.getSelfExeSharedLibPaths(allocator); - defer { - for (lib_paths) |lib_path| { - allocator.free(lib_path); - } - allocator.free(lib_paths); - } - - var found_ld_info: LdInfo = undefined; - var found_ld_path: [:0]const u8 = undefined; - - // Look for dynamic linker. - // This is O(N^M) but typical case here is N=2 and M=10. - find_ld: for (lib_paths) |lib_path| { - for (ld_info_list) |ld_info| { - const standard_ld_basename = fs.path.basename(ld_info.ld.get().?); - if (std.mem.endsWith(u8, lib_path, standard_ld_basename)) { - found_ld_info = ld_info; - found_ld_path = lib_path; - break :find_ld; - } - } - } else break :self_exe; - - // Look for glibc version. - var os_adjusted = os; - if (builtin.target.os.tag == .linux and found_ld_info.abi.isGnu() and - cross_target.glibc_version == null) - { - for (lib_paths) |lib_path| { - if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) { - os_adjusted.version_range.linux.glibc = glibcVerFromSo(lib_path) catch |err| switch (err) { - error.GnuLibCVersionUnavailable => continue, - else => |e| return e, - }; - break; - } - } - } - - var result: NativeTargetInfo = .{ - .target = .{ - .cpu = cpu, - .os = os_adjusted, - .abi = cross_target.abi orelse found_ld_info.abi, - .ofmt = cross_target.ofmt orelse Target.ObjectFormat.default(os_adjusted.tag, cpu.arch), - }, - .dynamic_linker = if (cross_target.dynamic_linker.get() == null) - DynamicLinker.init(found_ld_path) - else - cross_target.dynamic_linker, - }; - return result; - } - const elf_file = blk: { // This block looks for a shebang line in /usr/bin/env, // if it finds one, then instead of using /usr/bin/env as the ELF file to examine, it uses the file it references instead, @@ -452,56 +402,6 @@ fn detectAbiAndDynamicLinker( const glibc_so_basename = "libc.so.6"; -fn glibcVerFromSo(so_path: [:0]const u8) !std.builtin.Version { - const file = fs.openFileAbsolute(so_path, .{}) catch |err| switch (err) { - // Contextually impossible errors. - error.NoSpaceLeft => unreachable, - error.NameTooLong => unreachable, - error.PathAlreadyExists => unreachable, - error.SharingViolation => unreachable, - error.InvalidUtf8 => unreachable, - error.BadPathName => unreachable, - error.PipeBusy => unreachable, - error.FileLocksNotSupported => unreachable, - error.WouldBlock => unreachable, - error.FileBusy => unreachable, // opened without write permissions - error.NoDevice => unreachable, // not accessing special device - error.InvalidHandle => unreachable, // should not be in the error set - error.DeviceBusy => unreachable, // read-only - - // Errors that indicate a false negative may occur if we treat this as - // not a libc shared object. - error.ProcessFdQuotaExceeded => return error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded => return error.SystemFdQuotaExceeded, - error.SystemResources => return error.SystemResources, - error.Unexpected => return error.Unexpected, - - // Errors that indicate this file is not a libc shared object. - error.SymLinkLoop => return error.GnuLibCVersionUnavailable, - error.IsDir => return error.GnuLibCVersionUnavailable, - error.AccessDenied => return error.GnuLibCVersionUnavailable, - error.FileNotFound => return error.GnuLibCVersionUnavailable, - error.FileTooBig => return error.GnuLibCVersionUnavailable, - error.NotDir => return error.GnuLibCVersionUnavailable, - }; - defer file.close(); - - return glibcVerFromSoFile(file) catch |err| switch (err) { - error.InvalidElfMagic => return error.GnuLibCVersionUnavailable, - error.InvalidElfEndian => return error.GnuLibCVersionUnavailable, - error.InvalidElfClass => return error.GnuLibCVersionUnavailable, - error.InvalidElfFile => return error.GnuLibCVersionUnavailable, - error.InvalidElfVersion => return error.GnuLibCVersionUnavailable, - error.InvalidGnuLibCVersion => return error.GnuLibCVersionUnavailable, - error.UnexpectedEndOfFile => return error.GnuLibCVersionUnavailable, - error.UnableToReadElfFile => return error.GnuLibCVersionUnavailable, - - error.SystemResources => return error.SystemResources, - error.FileSystem => return error.FileSystem, - error.Unexpected => return error.Unexpected, - }; -} - fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len); diff --git a/src/main.zig b/src/main.zig index 039dacc877b7e5cc2de0b6319fedc7fe1ee0d075..aaea682c7bd7793e519ce09d37a335d16b8f5aaa 100644 --- a/src/main.zig +++ b/src/main.zig @@ -268,7 +268,7 @@ pub fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi } else if (mem.eql(u8, cmd, "init-lib")) { return cmdInit(gpa, arena, cmd_args, .Lib); } else if (mem.eql(u8, cmd, "targets")) { - const info = try detectNativeTargetInfo(arena, .{}); + const info = try detectNativeTargetInfo(.{}); const stdout = io.getStdOut().writer(); return @import("print_targets.zig").cmdTargets(arena, cmd_args, stdout, info.target); } else if (mem.eql(u8, cmd, "version")) { @@ -2267,7 +2267,7 @@ fn buildOutputType( } const cross_target = try parseCrossTargetOrReportFatalError(arena, target_parse_options); - const target_info = try detectNativeTargetInfo(gpa, cross_target); + const target_info = try detectNativeTargetInfo(cross_target); if (target_info.target.os.tag != .freestanding) { if (ensure_libc_on_non_freestanding) @@ -3283,7 +3283,7 @@ fn runOrTest( if (std.process.can_execv and arg_mode == .run and !watch) { // execv releases the locks; no need to destroy the Compilation here. const err = std.process.execv(gpa, argv.items); - try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc); + try warnAboutForeignBinaries(arena, arg_mode, target_info, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed to execve with '{s}':\n{s}", .{ @errorName(err), cmd }); } else if (std.process.can_spawn) { @@ -3300,7 +3300,7 @@ fn runOrTest( } const term = child.spawnAndWait() catch |err| { - try warnAboutForeignBinaries(gpa, arena, arg_mode, target_info, link_libc); + try warnAboutForeignBinaries(arena, arg_mode, target_info, link_libc); const cmd = try std.mem.join(arena, " ", argv.items); fatal("the following command failed with '{s}':\n{s}", .{ @errorName(err), cmd }); }; @@ -3914,7 +3914,7 @@ pub fn cmdBuild(gpa: Allocator, arena: Allocator, args: []const []const u8) !voi gimmeMoreOfThoseSweetSweetFileDescriptors(); const cross_target: std.zig.CrossTarget = .{}; - const target_info = try detectNativeTargetInfo(gpa, cross_target); + const target_info = try detectNativeTargetInfo(cross_target); const exe_basename = try std.zig.binNameAlloc(arena, .{ .root_name = "build", @@ -4956,8 +4956,8 @@ test "fds" { gimmeMoreOfThoseSweetSweetFileDescriptors(); } -fn detectNativeTargetInfo(gpa: Allocator, cross_target: std.zig.CrossTarget) !std.zig.system.NativeTargetInfo { - return std.zig.system.NativeTargetInfo.detect(gpa, cross_target); +fn detectNativeTargetInfo(cross_target: std.zig.CrossTarget) !std.zig.system.NativeTargetInfo { + return std.zig.system.NativeTargetInfo.detect(cross_target); } /// Indicate that we are now terminating with a successful exit code. @@ -5320,14 +5320,13 @@ fn parseIntSuffix(arg: []const u8, prefix_len: usize) u64 { } fn warnAboutForeignBinaries( - gpa: Allocator, arena: Allocator, arg_mode: ArgMode, target_info: std.zig.system.NativeTargetInfo, link_libc: bool, ) !void { const host_cross_target: std.zig.CrossTarget = .{}; - const host_target_info = try detectNativeTargetInfo(gpa, host_cross_target); + const host_target_info = try detectNativeTargetInfo(host_cross_target); switch (host_target_info.getExternalExecutor(target_info, .{ .link_libc = link_libc })) { .native => return, diff --git a/src/test.zig b/src/test.zig index babded13f9d71d7324eba4eb0afc34240c804ddb..358b783148bc0c2acde1ade1d9f5bb6d80643a68 100644 --- a/src/test.zig +++ b/src/test.zig @@ -1211,7 +1211,7 @@ pub const TestContext = struct { } fn run(self: *TestContext) !void { - const host = try std.zig.system.NativeTargetInfo.detect(self.gpa, .{}); + const host = try std.zig.system.NativeTargetInfo.detect(.{}); var progress = std.Progress{}; const root_node = progress.start("compiler", self.cases.items.len); @@ -1300,7 +1300,7 @@ pub const TestContext = struct { global_cache_directory: Compilation.Directory, host: std.zig.system.NativeTargetInfo, ) !void { - const target_info = try std.zig.system.NativeTargetInfo.detect(allocator, case.target); + const target_info = try std.zig.system.NativeTargetInfo.detect(case.target); const target = target_info.target; var arena_allocator = std.heap.ArenaAllocator.init(allocator); -- 2.54.0 From 1b6fa1965a5472d9bc9e3140d052bde0fb949fe1 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Sep 2022 19:03:28 -0700 Subject: [PATCH 3/7] stage2: fix building for 32-bit targets --- lib/std/zig/system/NativeTargetInfo.zig | 5 +++-- src/codegen/llvm.zig | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 5ed4a02f745ff8274e878fec3bc981f21614568e..0129df302012a42f4b79705c737c72bc6b7ed193 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -485,8 +485,9 @@ fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { var buf: [40000]u8 = undefined; if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion; - const dynstr_bytes = buf[0..dynstr.size]; - _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr.size); + const dynstr_size = @intCast(usize, dynstr.size); + const dynstr_bytes = buf[0..dynstr_size]; + _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr_size); var it = mem.split(u8, dynstr_bytes, &.{0}); var max_ver: std.builtin.Version = .{ .major = 2, .minor = 2, .patch = 5 }; while (it.next()) |s| { diff --git a/src/codegen/llvm.zig b/src/codegen/llvm.zig index 043f0bbdc722281a25416f78bb95d15f65618bcc..360371319508f6fec61ed944d178eb911e1a794d 100644 --- a/src/codegen/llvm.zig +++ b/src/codegen/llvm.zig @@ -3870,7 +3870,7 @@ pub const DeclGen = struct { var b: usize = 0; for (parent_ty.structFields().values()[0..field_index]) |field| { if (field.is_comptime or !field.ty.hasRuntimeBitsIgnoreComptime()) continue; - b += field.ty.bitSize(target); + b += @intCast(usize, field.ty.bitSize(target)); } break :b b; }; -- 2.54.0 From c668396941f19a5c015014822d980bbe9f2bc585 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Sep 2022 20:26:58 -0700 Subject: [PATCH 4/7] std.zig.system.NativeTargetInfo: handle missing DT_RUNPATH This commit removes the check that takes advantage of when the dynamic linker is a symlink. Instead, it falls back on the same directory as the dynamic linker as a de facto runpath. Empirically, this gives correct results on Gentoo and NixOS. Unfortunately it is still falling short for Debian, which has libc.so.6 in a different directory as the dynamic linker. --- lib/std/zig/system/NativeTargetInfo.zig | 328 +++++++++++------------- 1 file changed, 150 insertions(+), 178 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 0129df302012a42f4b79705c737c72bc6b7ed193..7d626be994f42c6cd0e97630aa342c65bd448f95 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -400,7 +400,86 @@ fn detectAbiAndDynamicLinker( }; } -const glibc_so_basename = "libc.so.6"; +fn glibcVerFromRPath(rpath: []const u8) !std.builtin.Version { + var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, + error.BadPathName => unreachable, + error.DeviceBusy => unreachable, + + error.FileNotFound, + error.NotDir, + error.InvalidHandle, + error.AccessDenied, + error.NoDevice, + => return error.GLibCNotFound, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + defer dir.close(); + + // Now we have a candidate for the path to libc shared object. In + // the past, we used readlink() here because the link name would + // reveal the glibc version. However, in more recent GNU/Linux + // installations, there is no symlink. Thus we instead use a more + // robust check of opening the libc shared object and looking at the + // .dynstr section, and finding the max version number of symbols + // that start with "GLIBC_2.". + const glibc_so_basename = "libc.so.6"; + var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.PipeBusy => unreachable, // Windows-only + error.SharingViolation => unreachable, // Windows-only + error.FileLocksNotSupported => unreachable, // No lock requested. + error.NoSpaceLeft => unreachable, // read-only + error.PathAlreadyExists => unreachable, // read-only + error.DeviceBusy => unreachable, // read-only + error.FileBusy => unreachable, // read-only + error.InvalidHandle => unreachable, // should not be in the error set + error.WouldBlock => unreachable, // not using O_NONBLOCK + error.NoDevice => unreachable, // not asking for a special device + + error.AccessDenied, + error.FileNotFound, + error.NotDir, + error.IsDir, + => return error.GLibCNotFound, + + error.FileTooBig => return error.Unexpected, + + error.ProcessFdQuotaExceeded, + error.SystemFdQuotaExceeded, + error.SystemResources, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + defer f.close(); + + return glibcVerFromSoFile(f) catch |err| switch (err) { + error.InvalidElfMagic, + error.InvalidElfEndian, + error.InvalidElfClass, + error.InvalidElfFile, + error.InvalidElfVersion, + error.InvalidGnuLibCVersion, + error.UnexpectedEndOfFile, + => return error.GLibCNotFound, + + error.SystemResources, + error.UnableToReadElfFile, + error.Unexpected, + error.FileSystem, + => |e| return e, + }; +} fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined; @@ -507,23 +586,6 @@ fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { return max_ver; } -fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) !std.builtin.Version { - // example: "libc-2.3.4.so" - // example: "libc-2.27.so" - // example: "ld-2.33.so" - const suffix = ".so"; - if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) { - return error.UnrecognizedGnuLibCFileName; - } - // chop off "libc-" and ".so" - const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len]; - return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) { - error.Overflow => return error.InvalidGnuLibCVersion, - error.InvalidCharacter => return error.InvalidGnuLibCVersion, - error.InvalidVersion => return error.InvalidGnuLibCVersion, - }; -} - pub const AbiAndDynamicLinkerFromFileError = error{ FileSystem, SystemResources, @@ -674,65 +736,65 @@ pub fn abiAndDynamicLinkerFromFile( if (builtin.target.os.tag == .linux and result.target.isGnuLibC() and cross_target.glibc_version == null) { - if (rpath_offset) |rpoff| { - const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); + const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx); - var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); - const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); - const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); + var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff); + const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize); + const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx); - var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; - if (sh_buf.len < shentsize) return error.InvalidElfFile; + var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined; + if (sh_buf.len < shentsize) return error.InvalidElfFile; - _ = try preadMin(file, &sh_buf, str_section_off, shentsize); - const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf)); - const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf)); - const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); - const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); - var strtab_buf: [4096:0]u8 = undefined; - const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len); - const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len); - const shstrtab = strtab_buf[0..shstrtab_read_len]; + _ = try preadMin(file, &sh_buf, str_section_off, shentsize); + const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf)); + const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf)); + const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset); + const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size); + var strtab_buf: [4096:0]u8 = undefined; + const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len); + const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len); + const shstrtab = strtab_buf[0..shstrtab_read_len]; - const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); - var sh_i: u16 = 0; - const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { - // Reserve some bytes so that we can deref the 64-bit struct fields - // even when the ELF file is 32-bits. - const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); - const sh_read_byte_len = try preadMin( - file, - sh_buf[0 .. sh_buf.len - sh_reserve], - shoff, - shentsize, + const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum); + var sh_i: u16 = 0; + const dynstr: ?struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) { + // Reserve some bytes so that we can deref the 64-bit struct fields + // even when the ELF file is 32-bits. + const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr); + const sh_read_byte_len = try preadMin( + file, + sh_buf[0 .. sh_buf.len - sh_reserve], + shoff, + shentsize, + ); + var sh_buf_i: usize = 0; + while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ + sh_i += 1; + shoff += shentsize; + sh_buf_i += shentsize; + }) { + const sh32 = @ptrCast( + *elf.Elf32_Shdr, + @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]), ); - var sh_buf_i: usize = 0; - while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({ - sh_i += 1; - shoff += shentsize; - sh_buf_i += shentsize; - }) { - const sh32 = @ptrCast( - *elf.Elf32_Shdr, - @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]), - ); - const sh64 = @ptrCast( - *elf.Elf64_Shdr, - @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]), - ); - const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); - // TODO this pointer cast should not be necessary - const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0); - if (mem.eql(u8, sh_name, ".dynstr")) { - break :find_dyn_str .{ - .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), - .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), - }; - } + const sh64 = @ptrCast( + *elf.Elf64_Shdr, + @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]), + ); + const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name); + // TODO this pointer cast should not be necessary + const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0); + if (mem.eql(u8, sh_name, ".dynstr")) { + break :find_dyn_str .{ + .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset), + .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size), + }; } - } else null; + } + } else null; - if (dynstr) |ds| { + if (dynstr) |ds| { + if (rpath_offset) |rpoff| { // TODO this pointer cast should not be necessary const rpoff_usize = std.math.cast(usize, rpoff) orelse return error.InvalidElfFile; if (rpoff_usize > ds.size) return error.InvalidElfFile; @@ -746,116 +808,26 @@ pub fn abiAndDynamicLinkerFromFile( const rpath_list = mem.sliceTo(std.meta.assumeSentinel(strtab.ptr, 0), 0); var it = mem.tokenize(u8, rpath_list, ":"); while (it.next()) |rpath| { - var dir = fs.cwd().openDir(rpath, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, - error.BadPathName => unreachable, - error.DeviceBusy => unreachable, - - error.FileNotFound, - error.NotDir, - error.InvalidHandle, - error.AccessDenied, - error.NoDevice, - => continue, - - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - defer dir.close(); - - // Now we have a candidate for the path to libc shared object. In - // the past, we used readlink() here because the link name would - // reveal the glibc version. However, in more recent GNU/Linux - // installations, there is no symlink. Thus we instead use a more - // robust check of opening the libc shared object and looking at the - // .dynstr section, and finding the max version number of symbols - // that start with "GLIBC_2.". - var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.PipeBusy => unreachable, // Windows-only - error.SharingViolation => unreachable, // Windows-only - error.FileLocksNotSupported => unreachable, // No lock requested. - error.NoSpaceLeft => unreachable, // read-only - error.PathAlreadyExists => unreachable, // read-only - error.DeviceBusy => unreachable, // read-only - error.FileBusy => unreachable, // read-only - error.InvalidHandle => unreachable, // should not be in the error set - error.WouldBlock => unreachable, // not using O_NONBLOCK - error.NoDevice => unreachable, // not asking for a special device - - error.AccessDenied, - error.FileNotFound, - error.NotDir, - => continue, - - error.IsDir => return error.InvalidElfFile, - error.FileTooBig => return error.Unexpected, - - error.ProcessFdQuotaExceeded, - error.SystemFdQuotaExceeded, - error.SystemResources, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - defer f.close(); - - result.target.os.version_range.linux.glibc = glibcVerFromSoFile(f) catch |err| switch (err) { - error.InvalidElfMagic, - error.InvalidElfEndian, - error.InvalidElfClass, - error.InvalidElfFile, - error.InvalidElfVersion, - error.InvalidGnuLibCVersion, - error.UnexpectedEndOfFile, - => continue, - - error.SystemResources, - error.UnableToReadElfFile, - error.Unexpected, - error.FileSystem, - => |e| return e, - }; - break; + if (glibcVerFromRPath(rpath)) |ver| { + result.target.os.version_range.linux.glibc = ver; + break; + } else |err| switch (err) { + error.GLibCNotFound => continue, + else => |e| return e, + } + } + } else if (result.dynamic_linker.get()) |dl_path| { + // There is no DT_RUNPATH so we try to find libc.so.6 inside the same + // directory as the dynamic linker. + if (fs.path.dirname(dl_path)) |rpath| { + if (glibcVerFromRPath(rpath)) |ver| { + result.target.os.version_range.linux.glibc = ver; + } else |err| switch (err) { + error.GLibCNotFound => {}, + else => |e| return e, + } } } - } else if (result.dynamic_linker.get()) |dl_path| glibc_ver: { - // There is no DT_RUNPATH but we can try to see if the information is - // present in the symlink data for the dynamic linker path. - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlink(dl_path, &link_buf) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only - - error.AccessDenied, - error.FileNotFound, - error.NotLink, - error.NotDir, - => break :glibc_ver, - - error.SystemResources, - error.FileSystem, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - result.target.os.version_range.linux.glibc = glibcVerFromLinkName( - fs.path.basename(link_name), - "ld-", - ) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName, - error.InvalidGnuLibCVersion, - => break :glibc_ver, - }; } } -- 2.54.0 From 9f40f34501ef086d18e4570416c078a7ad508628 Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Thu, 8 Sep 2022 20:49:16 -0700 Subject: [PATCH 5/7] std.zig.system.NativeTargetInfo: restore symlink logic This is a partial revert of the previous commit, fixing a regression on Debian. However, the commit additionally improves the detectAbiAndDynamicLinker function to read more than 1 byte at a time when detecting a shebang line. --- lib/std/zig/system/NativeTargetInfo.zig | 73 +++++++++++++++++++------ 1 file changed, 57 insertions(+), 16 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 7d626be994f42c6cd0e97630aa342c65bd448f95..11b5ec4191d7d537aa37e488590e61a44c50de2e 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -347,26 +347,17 @@ fn detectAbiAndDynamicLinker( }; errdefer file.close(); - const line = file.reader().readUntilDelimiter(&buffer, '\n') catch |err| switch (err) { - error.IsDir => unreachable, // Handled before - error.AccessDenied => unreachable, - error.WouldBlock => unreachable, // Did not request blocking mode - error.OperationAborted => unreachable, // Windows-only - error.BrokenPipe => unreachable, - error.ConnectionResetByPeer => unreachable, - error.ConnectionTimedOut => unreachable, - error.InputOutput => unreachable, - error.Unexpected => unreachable, - - error.StreamTooLong, - error.EndOfStream, - error.NotOpenForReading, + const len = preadMin(file, &buffer, 0, buffer.len) catch |err| switch (err) { + error.UnexpectedEndOfFile, + error.UnableToReadElfFile, => break :blk file, else => |e| return e, }; + const newline = mem.indexOfScalar(u8, buffer[0..len], '\n') orelse break :blk file; + const line = buffer[0..newline]; if (!mem.startsWith(u8, line, "#!")) break :blk file; - var it = std.mem.tokenize(u8, line[2..], " "); + var it = mem.tokenize(u8, line[2..], " "); file_name = it.next() orelse return defaultAbiAndDynamicLinker(cpu, os, cross_target); file.close(); } @@ -375,6 +366,8 @@ fn detectAbiAndDynamicLinker( // If Zig is statically linked, such as via distributed binary static builds, the above // trick (block self_exe) won't work. The next thing we fall back to is the same thing, but for elf_file. + // TODO: inline this function and combine the buffer we already read above to find + // the possible shebang line with the buffer we use for the ELF header. return abiAndDynamicLinkerFromFile(elf_file, cpu, os, ld_info_list, cross_target) catch |err| switch (err) { error.FileSystem, error.SystemResources, @@ -586,6 +579,23 @@ fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { return max_ver; } +fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) !std.builtin.Version { + // example: "libc-2.3.4.so" + // example: "libc-2.27.so" + // example: "ld-2.33.so" + const suffix = ".so"; + if (!mem.startsWith(u8, link_name, prefix) or !mem.endsWith(u8, link_name, suffix)) { + return error.UnrecognizedGnuLibCFileName; + } + // chop off "libc-" and ".so" + const link_name_chopped = link_name[prefix.len .. link_name.len - suffix.len]; + return std.builtin.Version.parse(link_name_chopped) catch |err| switch (err) { + error.Overflow => return error.InvalidGnuLibCVersion, + error.InvalidCharacter => return error.InvalidGnuLibCVersion, + error.InvalidVersion => return error.InvalidGnuLibCVersion, + }; +} + pub const AbiAndDynamicLinkerFromFileError = error{ FileSystem, SystemResources, @@ -816,17 +826,48 @@ pub fn abiAndDynamicLinkerFromFile( else => |e| return e, } } - } else if (result.dynamic_linker.get()) |dl_path| { + } else if (result.dynamic_linker.get()) |dl_path| glibc_ver: { // There is no DT_RUNPATH so we try to find libc.so.6 inside the same // directory as the dynamic linker. if (fs.path.dirname(dl_path)) |rpath| { if (glibcVerFromRPath(rpath)) |ver| { result.target.os.version_range.linux.glibc = ver; + break :glibc_ver; } else |err| switch (err) { error.GLibCNotFound => {}, else => |e| return e, } } + + // So far, no luck. Next we try to see if the information is + // present in the symlink data for the dynamic linker path. + var link_buf: [std.os.PATH_MAX]u8 = undefined; + const link_name = std.os.readlink(dl_path, &link_buf) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only + + error.AccessDenied, + error.FileNotFound, + error.NotLink, + error.NotDir, + => break :glibc_ver, + + error.SystemResources, + error.FileSystem, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + result.target.os.version_range.linux.glibc = glibcVerFromLinkName( + fs.path.basename(link_name), + "ld-", + ) catch |err| switch (err) { + error.UnrecognizedGnuLibCFileName, + error.InvalidGnuLibCVersion, + => break :glibc_ver, + }; } } } -- 2.54.0 From c7d6048081053c2852d4d3af5d549d83473f808c Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 9 Sep 2022 00:03:53 -0700 Subject: [PATCH 6/7] std.zig.system.NativeTargetInfo: add fallback check After failing to find RUNPATH in the ELF of /usr/bin/env, not finding the answer in a symlink of the dynamic interpreter, and not finding libc.so.6 in the same directory as the dynamic interpreter, Zig will check `/lib/$triple`. This fixes incorrect native glibc version detected on Debian bookworm. --- lib/std/zig/system/NativeTargetInfo.zig | 116 +++++++++++++++--------- 1 file changed, 74 insertions(+), 42 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index 11b5ec4191d7d537aa37e488590e61a44c50de2e..a4d29c17b4629a125860ce5f9be6799027bd2c6b 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -820,55 +820,87 @@ pub fn abiAndDynamicLinkerFromFile( while (it.next()) |rpath| { if (glibcVerFromRPath(rpath)) |ver| { result.target.os.version_range.linux.glibc = ver; - break; + return result; } else |err| switch (err) { error.GLibCNotFound => continue, else => |e| return e, } } - } else if (result.dynamic_linker.get()) |dl_path| glibc_ver: { - // There is no DT_RUNPATH so we try to find libc.so.6 inside the same - // directory as the dynamic linker. - if (fs.path.dirname(dl_path)) |rpath| { - if (glibcVerFromRPath(rpath)) |ver| { - result.target.os.version_range.linux.glibc = ver; - break :glibc_ver; - } else |err| switch (err) { - error.GLibCNotFound => {}, - else => |e| return e, - } + } + } + + if (result.dynamic_linker.get()) |dl_path| glibc_ver: { + // There is no DT_RUNPATH so we try to find libc.so.6 inside the same + // directory as the dynamic linker. + if (fs.path.dirname(dl_path)) |rpath| { + if (glibcVerFromRPath(rpath)) |ver| { + result.target.os.version_range.linux.glibc = ver; + return result; + } else |err| switch (err) { + error.GLibCNotFound => {}, + else => |e| return e, } - - // So far, no luck. Next we try to see if the information is - // present in the symlink data for the dynamic linker path. - var link_buf: [std.os.PATH_MAX]u8 = undefined; - const link_name = std.os.readlink(dl_path, &link_buf) catch |err| switch (err) { - error.NameTooLong => unreachable, - error.InvalidUtf8 => unreachable, // Windows only - error.BadPathName => unreachable, // Windows only - error.UnsupportedReparsePointType => unreachable, // Windows only - - error.AccessDenied, - error.FileNotFound, - error.NotLink, - error.NotDir, - => break :glibc_ver, - - error.SystemResources, - error.FileSystem, - error.SymLinkLoop, - error.Unexpected, - => |e| return e, - }; - result.target.os.version_range.linux.glibc = glibcVerFromLinkName( - fs.path.basename(link_name), - "ld-", - ) catch |err| switch (err) { - error.UnrecognizedGnuLibCFileName, - error.InvalidGnuLibCVersion, - => break :glibc_ver, - }; } + + // So far, no luck. Next we try to see if the information is + // present in the symlink data for the dynamic linker path. + var link_buf: [std.os.PATH_MAX]u8 = undefined; + const link_name = std.os.readlink(dl_path, &link_buf) catch |err| switch (err) { + error.NameTooLong => unreachable, + error.InvalidUtf8 => unreachable, // Windows only + error.BadPathName => unreachable, // Windows only + error.UnsupportedReparsePointType => unreachable, // Windows only + + error.AccessDenied, + error.FileNotFound, + error.NotLink, + error.NotDir, + => break :glibc_ver, + + error.SystemResources, + error.FileSystem, + error.SymLinkLoop, + error.Unexpected, + => |e| return e, + }; + result.target.os.version_range.linux.glibc = glibcVerFromLinkName( + fs.path.basename(link_name), + "ld-", + ) catch |err| switch (err) { + error.UnrecognizedGnuLibCFileName, + error.InvalidGnuLibCVersion, + => break :glibc_ver, + }; + return result; + } + + // Nothing worked so far. Finally we fall back to hard-coded search paths. + // Some distros such as Debian keep their libc.so.6 in `/lib/$triple/`. + var path_buf: [std.os.PATH_MAX]u8 = undefined; + var index: usize = 0; + const prefix = "/lib/"; + const cpu_arch = @tagName(result.target.cpu.arch); + const os_tag = @tagName(result.target.os.tag); + const abi = @tagName(result.target.abi); + mem.copy(u8, path_buf[index..], prefix); + index += prefix.len; + mem.copy(u8, path_buf[index..], cpu_arch); + index += cpu_arch.len; + path_buf[index] = '-'; + index += 1; + mem.copy(u8, path_buf[index..], os_tag); + index += os_tag.len; + path_buf[index] = '-'; + index += 1; + mem.copy(u8, path_buf[index..], abi); + index += abi.len; + const rpath = path_buf[0..index]; + if (glibcVerFromRPath(rpath)) |ver| { + result.target.os.version_range.linux.glibc = ver; + return result; + } else |err| switch (err) { + error.GLibCNotFound => {}, + else => |e| return e, } } -- 2.54.0 From 68e61bbc0c3b896d5d549168ff8b88fe04e2269f Mon Sep 17 00:00:00 2001 From: Andrew Kelley Date: Fri, 9 Sep 2022 09:27:02 -0700 Subject: [PATCH 7/7] std.zig.system.NativeTargetInfo: more headroom for libc.so.6 .dynstr --- lib/std/zig/system/NativeTargetInfo.zig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/std/zig/system/NativeTargetInfo.zig b/lib/std/zig/system/NativeTargetInfo.zig index a4d29c17b4629a125860ce5f9be6799027bd2c6b..73f76b11b7a52b752b88b8b6a8cc812ab099e334 100644 --- a/lib/std/zig/system/NativeTargetInfo.zig +++ b/lib/std/zig/system/NativeTargetInfo.zig @@ -552,14 +552,14 @@ fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version { // and furthermore, that the system-installed glibc is at minimum that version. // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system. - // Here I use this value plus some headroom. This makes it only need + // Here I use double this value plus some headroom. This makes it only need // a single read syscall here. - var buf: [40000]u8 = undefined; + var buf: [80000]u8 = undefined; if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion; const dynstr_size = @intCast(usize, dynstr.size); const dynstr_bytes = buf[0..dynstr_size]; - _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr_size); + _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr_bytes.len); var it = mem.split(u8, dynstr_bytes, &.{0}); var max_ver: std.builtin.Version = .{ .major = 2, .minor = 2, .patch = 5 }; while (it.next()) |s| { -- 2.54.0