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.
This commit is contained in:
Andrew Kelley 2022-09-08 18:27:26 -07:00
parent fa940bafa2
commit 3ee01c14ee
6 changed files with 31 additions and 133 deletions

View file

@ -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, .{

View file

@ -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;

View file

@ -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,

View file

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

View file

@ -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,

View file

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