authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-09-08 18:02:38-07:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2022-09-08 20:52:49-07:00
logfa940bafa2720f49ee249eda1ee4cf26a247172a
treeab82a24917a27755897fb863bb0773ff723d75ca
parenta833bdcd7e6fcfee6e9cc33a3f7de78b16a36941

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

1 files changed, 192 insertions(+), 36 deletions(-)

lib/std/zig/system/NativeTargetInfo.zig+192-36
......@@ -28,6 +28,7 @@ pub const DetectError = error{
2828 SystemFdQuotaExceeded,
2929 DeviceBusy,
3030 OSVersionDetectionFail,
31 Unexpected,
3132};
3233
3334/// Given a `CrossTarget`, which specifies in detail which parts of the target should be detected
......@@ -332,9 +333,7 @@ fn detectAbiAndDynamicLinker(
332333 {
333334 for (lib_paths) |lib_path| {
334335 if (std.mem.endsWith(u8, lib_path, glibc_so_basename)) {
335 os_adjusted.version_range.linux.glibc = glibcVerFromSO(lib_path) catch |err| switch (err) {
336 error.UnrecognizedGnuLibCFileName => continue,
337 error.InvalidGnuLibCVersion => continue,
336 os_adjusted.version_range.linux.glibc = glibcVerFromSo(lib_path) catch |err| switch (err) {
338337 error.GnuLibCVersionUnavailable => continue,
339338 else => |e| return e,
340339 };
......@@ -369,7 +368,7 @@ fn detectAbiAndDynamicLinker(
369368 // #! (2) + 255 (max length of shebang line since Linux 5.1) + \n (1)
370369 var buffer: [258]u8 = undefined;
371370 while (true) {
372 const file = std.fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) {
371 const file = fs.openFileAbsolute(file_name, .{}) catch |err| switch (err) {
373372 error.NoSpaceLeft => unreachable,
374373 error.NameTooLong => unreachable,
375374 error.PathAlreadyExists => unreachable,
......@@ -396,6 +395,7 @@ fn detectAbiAndDynamicLinker(
396395
397396 else => |e| return e,
398397 };
398 errdefer file.close();
399399
400400 const line = file.reader().readUntilDelimiter(&buffer, '\n') catch |err| switch (err) {
401401 error.IsDir => unreachable, // Handled before
......@@ -413,15 +413,12 @@ fn detectAbiAndDynamicLinker(
413413 error.NotOpenForReading,
414414 => break :blk file,
415415
416 else => |e| {
417 file.close();
418 return e;
419 },
416 else => |e| return e,
420417 };
421418 if (!mem.startsWith(u8, line, "#!")) break :blk file;
422419 var it = std.mem.tokenize(u8, line[2..], " ");
423 file.close();
424420 file_name = it.next() orelse return defaultAbiAndDynamicLinker(cpu, os, cross_target);
421 file.close();
425422 }
426423 };
427424 defer elf_file.close();
......@@ -455,23 +452,158 @@ fn detectAbiAndDynamicLinker(
455452
456453const glibc_so_basename = "libc.so.6";
457454
458fn glibcVerFromSO(so_path: [:0]const u8) !std.builtin.Version {
459 var link_buf: [std.os.PATH_MAX]u8 = undefined;
460 const link_name = std.os.readlinkZ(so_path.ptr, &link_buf) catch |err| switch (err) {
461 error.AccessDenied => return error.GnuLibCVersionUnavailable,
462 error.FileSystem => return error.FileSystem,
463 error.SymLinkLoop => return error.SymLinkLoop,
455fn glibcVerFromSo(so_path: [:0]const u8) !std.builtin.Version {
456 const file = fs.openFileAbsolute(so_path, .{}) catch |err| switch (err) {
457 // Contextually impossible errors.
458 error.NoSpaceLeft => unreachable,
464459 error.NameTooLong => unreachable,
465 error.NotLink => return error.GnuLibCVersionUnavailable,
466 error.FileNotFound => return error.GnuLibCVersionUnavailable,
460 error.PathAlreadyExists => unreachable,
461 error.SharingViolation => unreachable,
462 error.InvalidUtf8 => unreachable,
463 error.BadPathName => unreachable,
464 error.PipeBusy => unreachable,
465 error.FileLocksNotSupported => unreachable,
466 error.WouldBlock => unreachable,
467 error.FileBusy => unreachable, // opened without write permissions
468 error.NoDevice => unreachable, // not accessing special device
469 error.InvalidHandle => unreachable, // should not be in the error set
470 error.DeviceBusy => unreachable, // read-only
471
472 // Errors that indicate a false negative may occur if we treat this as
473 // not a libc shared object.
474 error.ProcessFdQuotaExceeded => return error.ProcessFdQuotaExceeded,
475 error.SystemFdQuotaExceeded => return error.SystemFdQuotaExceeded,
467476 error.SystemResources => return error.SystemResources,
477 error.Unexpected => return error.Unexpected,
478
479 // Errors that indicate this file is not a libc shared object.
480 error.SymLinkLoop => return error.GnuLibCVersionUnavailable,
481 error.IsDir => return error.GnuLibCVersionUnavailable,
482 error.AccessDenied => return error.GnuLibCVersionUnavailable,
483 error.FileNotFound => return error.GnuLibCVersionUnavailable,
484 error.FileTooBig => return error.GnuLibCVersionUnavailable,
468485 error.NotDir => return error.GnuLibCVersionUnavailable,
469 error.Unexpected => return error.GnuLibCVersionUnavailable,
470 error.InvalidUtf8 => unreachable, // Windows only
471 error.BadPathName => unreachable, // Windows only
472 error.UnsupportedReparsePointType => unreachable, // Windows only
473486 };
474 return glibcVerFromLinkName(link_name, "libc-");
487 defer file.close();
488
489 return glibcVerFromSoFile(file) catch |err| switch (err) {
490 error.InvalidElfMagic => return error.GnuLibCVersionUnavailable,
491 error.InvalidElfEndian => return error.GnuLibCVersionUnavailable,
492 error.InvalidElfClass => return error.GnuLibCVersionUnavailable,
493 error.InvalidElfFile => return error.GnuLibCVersionUnavailable,
494 error.InvalidElfVersion => return error.GnuLibCVersionUnavailable,
495 error.InvalidGnuLibCVersion => return error.GnuLibCVersionUnavailable,
496 error.UnexpectedEndOfFile => return error.GnuLibCVersionUnavailable,
497 error.UnableToReadElfFile => return error.GnuLibCVersionUnavailable,
498
499 error.SystemResources => return error.SystemResources,
500 error.FileSystem => return error.FileSystem,
501 error.Unexpected => return error.Unexpected,
502 };
503}
504
505fn glibcVerFromSoFile(file: fs.File) !std.builtin.Version {
506 var hdr_buf: [@sizeOf(elf.Elf64_Ehdr)]u8 align(@alignOf(elf.Elf64_Ehdr)) = undefined;
507 _ = try preadMin(file, &hdr_buf, 0, hdr_buf.len);
508 const hdr32 = @ptrCast(*elf.Elf32_Ehdr, &hdr_buf);
509 const hdr64 = @ptrCast(*elf.Elf64_Ehdr, &hdr_buf);
510 if (!mem.eql(u8, hdr32.e_ident[0..4], elf.MAGIC)) return error.InvalidElfMagic;
511 const elf_endian: std.builtin.Endian = switch (hdr32.e_ident[elf.EI_DATA]) {
512 elf.ELFDATA2LSB => .Little,
513 elf.ELFDATA2MSB => .Big,
514 else => return error.InvalidElfEndian,
515 };
516 const need_bswap = elf_endian != native_endian;
517 if (hdr32.e_ident[elf.EI_VERSION] != 1) return error.InvalidElfVersion;
518
519 const is_64 = switch (hdr32.e_ident[elf.EI_CLASS]) {
520 elf.ELFCLASS32 => false,
521 elf.ELFCLASS64 => true,
522 else => return error.InvalidElfClass,
523 };
524 const shstrndx = elfInt(is_64, need_bswap, hdr32.e_shstrndx, hdr64.e_shstrndx);
525 var shoff = elfInt(is_64, need_bswap, hdr32.e_shoff, hdr64.e_shoff);
526 const shentsize = elfInt(is_64, need_bswap, hdr32.e_shentsize, hdr64.e_shentsize);
527 const str_section_off = shoff + @as(u64, shentsize) * @as(u64, shstrndx);
528 var sh_buf: [16 * @sizeOf(elf.Elf64_Shdr)]u8 align(@alignOf(elf.Elf64_Shdr)) = undefined;
529 if (sh_buf.len < shentsize) return error.InvalidElfFile;
530
531 _ = try preadMin(file, &sh_buf, str_section_off, shentsize);
532 const shstr32 = @ptrCast(*elf.Elf32_Shdr, @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf));
533 const shstr64 = @ptrCast(*elf.Elf64_Shdr, @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf));
534 const shstrtab_off = elfInt(is_64, need_bswap, shstr32.sh_offset, shstr64.sh_offset);
535 const shstrtab_size = elfInt(is_64, need_bswap, shstr32.sh_size, shstr64.sh_size);
536 var strtab_buf: [4096:0]u8 = undefined;
537 const shstrtab_len = std.math.min(shstrtab_size, strtab_buf.len);
538 const shstrtab_read_len = try preadMin(file, &strtab_buf, shstrtab_off, shstrtab_len);
539 const shstrtab = strtab_buf[0..shstrtab_read_len];
540 const shnum = elfInt(is_64, need_bswap, hdr32.e_shnum, hdr64.e_shnum);
541 var sh_i: u16 = 0;
542 const dynstr: struct { offset: u64, size: u64 } = find_dyn_str: while (sh_i < shnum) {
543 // Reserve some bytes so that we can deref the 64-bit struct fields
544 // even when the ELF file is 32-bits.
545 const sh_reserve: usize = @sizeOf(elf.Elf64_Shdr) - @sizeOf(elf.Elf32_Shdr);
546 const sh_read_byte_len = try preadMin(
547 file,
548 sh_buf[0 .. sh_buf.len - sh_reserve],
549 shoff,
550 shentsize,
551 );
552 var sh_buf_i: usize = 0;
553 while (sh_buf_i < sh_read_byte_len and sh_i < shnum) : ({
554 sh_i += 1;
555 shoff += shentsize;
556 sh_buf_i += shentsize;
557 }) {
558 const sh32 = @ptrCast(
559 *elf.Elf32_Shdr,
560 @alignCast(@alignOf(elf.Elf32_Shdr), &sh_buf[sh_buf_i]),
561 );
562 const sh64 = @ptrCast(
563 *elf.Elf64_Shdr,
564 @alignCast(@alignOf(elf.Elf64_Shdr), &sh_buf[sh_buf_i]),
565 );
566 const sh_name_off = elfInt(is_64, need_bswap, sh32.sh_name, sh64.sh_name);
567 // TODO this pointer cast should not be necessary
568 const sh_name = mem.sliceTo(std.meta.assumeSentinel(shstrtab[sh_name_off..].ptr, 0), 0);
569 if (mem.eql(u8, sh_name, ".dynstr")) {
570 break :find_dyn_str .{
571 .offset = elfInt(is_64, need_bswap, sh32.sh_offset, sh64.sh_offset),
572 .size = elfInt(is_64, need_bswap, sh32.sh_size, sh64.sh_size),
573 };
574 }
575 }
576 } else return error.InvalidGnuLibCVersion;
577
578 // Here we loop over all the strings in the dynstr string table, assuming that any
579 // strings that start with "GLIBC_2." indicate the existence of such a glibc version,
580 // and furthermore, that the system-installed glibc is at minimum that version.
581
582 // Empirically, glibc 2.34 libc.so .dynstr section is 32441 bytes on my system.
583 // Here I use this value plus some headroom. This makes it only need
584 // a single read syscall here.
585 var buf: [40000]u8 = undefined;
586 if (buf.len < dynstr.size) return error.InvalidGnuLibCVersion;
587
588 const dynstr_bytes = buf[0..dynstr.size];
589 _ = try preadMin(file, dynstr_bytes, dynstr.offset, dynstr.size);
590 var it = mem.split(u8, dynstr_bytes, &.{0});
591 var max_ver: std.builtin.Version = .{ .major = 2, .minor = 2, .patch = 5 };
592 while (it.next()) |s| {
593 if (mem.startsWith(u8, s, "GLIBC_2.")) {
594 const chopped = s["GLIBC_".len..];
595 const ver = std.builtin.Version.parse(chopped) catch |err| switch (err) {
596 error.Overflow => return error.InvalidGnuLibCVersion,
597 error.InvalidCharacter => return error.InvalidGnuLibCVersion,
598 error.InvalidVersion => return error.InvalidGnuLibCVersion,
599 };
600 switch (ver.order(max_ver)) {
601 .gt => max_ver = ver,
602 .lt, .eq => continue,
603 }
604 }
605 }
606 return max_ver;
475607}
476608
477609fn glibcVerFromLinkName(link_name: []const u8, prefix: []const u8) !std.builtin.Version {
......@@ -735,36 +867,60 @@ pub fn abiAndDynamicLinkerFromFile(
735867 };
736868 defer dir.close();
737869
738 var link_buf: [std.os.PATH_MAX]u8 = undefined;
739 const link_name = std.os.readlinkatZ(
740 dir.fd,
741 glibc_so_basename,
742 &link_buf,
743 ) catch |err| switch (err) {
870 // Now we have a candidate for the path to libc shared object. In
871 // the past, we used readlink() here because the link name would
872 // reveal the glibc version. However, in more recent GNU/Linux
873 // installations, there is no symlink. Thus we instead use a more
874 // robust check of opening the libc shared object and looking at the
875 // .dynstr section, and finding the max version number of symbols
876 // that start with "GLIBC_2.".
877 var f = dir.openFile(glibc_so_basename, .{}) catch |err| switch (err) {
744878 error.NameTooLong => unreachable,
745879 error.InvalidUtf8 => unreachable, // Windows only
746880 error.BadPathName => unreachable, // Windows only
747 error.UnsupportedReparsePointType => unreachable, // Windows only
881 error.PipeBusy => unreachable, // Windows-only
882 error.SharingViolation => unreachable, // Windows-only
883 error.FileLocksNotSupported => unreachable, // No lock requested.
884 error.NoSpaceLeft => unreachable, // read-only
885 error.PathAlreadyExists => unreachable, // read-only
886 error.DeviceBusy => unreachable, // read-only
887 error.FileBusy => unreachable, // read-only
888 error.InvalidHandle => unreachable, // should not be in the error set
889 error.WouldBlock => unreachable, // not using O_NONBLOCK
890 error.NoDevice => unreachable, // not asking for a special device
748891
749892 error.AccessDenied,
750893 error.FileNotFound,
751 error.NotLink,
752894 error.NotDir,
753895 => continue,
754896
897 error.IsDir => return error.InvalidElfFile,
898 error.FileTooBig => return error.Unexpected,
899
900 error.ProcessFdQuotaExceeded,
901 error.SystemFdQuotaExceeded,
755902 error.SystemResources,
756 error.FileSystem,
757903 error.SymLinkLoop,
758904 error.Unexpected,
759905 => |e| return e,
760906 };
761 result.target.os.version_range.linux.glibc = glibcVerFromLinkName(
762 link_name,
763 "libc-",
764 ) catch |err| switch (err) {
765 error.UnrecognizedGnuLibCFileName,
907 defer f.close();
908
909 result.target.os.version_range.linux.glibc = glibcVerFromSoFile(f) catch |err| switch (err) {
910 error.InvalidElfMagic,
911 error.InvalidElfEndian,
912 error.InvalidElfClass,
913 error.InvalidElfFile,
914 error.InvalidElfVersion,
766915 error.InvalidGnuLibCVersion,
916 error.UnexpectedEndOfFile,
767917 => continue,
918
919 error.SystemResources,
920 error.UnableToReadElfFile,
921 error.Unexpected,
922 error.FileSystem,
923 => |e| return e,
768924 };
769925 break;
770926 }