diff --git a/src/auditor/dynamic_linkage.jl b/src/auditor/dynamic_linkage.jl index b1fa3aa69..e47da428b 100644 --- a/src/auditor/dynamic_linkage.jl +++ b/src/auditor/dynamic_linkage.jl @@ -1,5 +1,30 @@ using ObjectFile.ELF +function os_from_elf_note(oh::ELFHandle) + for section in Sections(oh) + section_handle(section) == ELF.SHT_NOTE || continue + seek(oh, section_offset(section)) + name_length = read(oh, UInt32) + iszero(name_length) && continue + descriptor_length = read(oh, UInt32) + note_type = read(oh, UInt32) + name = String(read(oh, name_length - 1)) # skip trailing NUL + if note_type == 1 + # Technically it's part of the Linux specification that any executable should + # have an ELF note with type 1, name GNU, and descriptor length ≥4, but in + # practice I haven't observed that consistently, especially on musl. So for + # now, only bother checking FreeBSD, which uses an ELF note rather than OS/ABI + # to identify itself on AArch64 and RISC-V. + if name == "FreeBSD" && descriptor_length == 4 + return name + end + end + end + return nothing +end + +os_from_elf_note(::ObjectHandle) = nothing + """ platform_for_object(oh::ObjectHandle) @@ -39,7 +64,9 @@ function platform_for_object(oh::ObjectHandle) end end - if oh.ei.osabi == ELF.ELFOSABI_LINUX || oh.ei.osabi == ELF.ELFOSABI_NONE + if oh.ei.osabi == ELF.ELFOSABI_NONE + return Platform(arch, os_from_elf_note(oh) == "FreeBSD" ? "freebsd" : "linux") + elseif oh.ei.osabi == ELF.ELFOSABI_LINUX return Platform(arch, "linux") elseif oh.ei.osabi == ELF.ELFOSABI_FREEBSD return Platform(arch, "freebsd") @@ -108,6 +135,13 @@ function is_for_platform(h::ObjectHandle, platform::AbstractPlatform) else error("Unknown OS ABI type $(typeof(platform))") end + else + # If no OSABI, check whether it has a matching ELF note + if Sys.isfreebsd(platform) + if os_from_elf_note(h) != "FreeBSD" + return false + end + end end # Check that the ELF arch matches our own m = h.header.e_machine diff --git a/src/auditor/extra_checks.jl b/src/auditor/extra_checks.jl index e8935d6ad..319f5b269 100644 --- a/src/auditor/extra_checks.jl +++ b/src/auditor/extra_checks.jl @@ -5,7 +5,19 @@ using Dates: DateTime, datetime2unix function check_os_abi(oh::ObjectHandle, p::AbstractPlatform, rest...; verbose::Bool = false, kwargs...) if Sys.isfreebsd(p) - if oh.ei.osabi != ELF.ELFOSABI_FREEBSD + # On AArch64 and RISC-V, FreeBSD uses an ELF note section to identify itself rather + # than OS/ABI in the ELF header. In that case, the OS/ABI will be generic Unix (NONE). + # See https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=252490 and + # https://github.com/freebsd/freebsd-src/blob/main/lib/csu/common/crtbrand.S + if oh.ei.osabi == ELF.ELFOSABI_NONE + if os_from_elf_note(oh) != "FreeBSD" + if verbose + @warn("$(basename(path(oh))) does not have a FreeBSD-branded ELF note " * + "and may be unrecognized or unusable on $p") + end + return false + end + elseif oh.ei.osabi != ELF.ELFOSABI_FREEBSD # The dynamic loader should not have problems in this case, but the # linker may not appreciate. Let the user know about this. if verbose @@ -17,7 +29,8 @@ function check_os_abi(oh::ObjectHandle, p::AbstractPlatform, rest...; verbose::B end return false end - elseif call_abi(p) == "eabihf" + end + if call_abi(p) == "eabihf" # Make sure the object file has the hard-float ABI. See Table 4-2 of # "ELF for the ARM Architecture" document # (https://developer.arm.com/documentation/ihi0044/e/). Note: `0x000` diff --git a/test/auditing.jl b/test/auditing.jl index 07776f2ea..87fabb6cd 100644 --- a/test/auditing.jl +++ b/test/auditing.jl @@ -1,5 +1,6 @@ using BinaryBuilder.Auditor -using BinaryBuilder.Auditor: compatible_marchs, valid_library_path +using BinaryBuilder.Auditor: check_os_abi, compatible_marchs, is_for_platform, valid_library_path +using ObjectFile # Tests for our auditing infrastructure @@ -788,57 +789,113 @@ end end @testset "Auditor - other checks" begin - platform = Platform("armv7l", "linux"; call_abi = "eabihf", libc = "glibc") - mktempdir() do build_path - build_output_meta = @test_logs (:error, r"libsoft.so does not match the hard-float ABI") match_mode=:any begin - autobuild( - build_path, - "hard_float_ABI", - v"1.0.0", - # No sources - FileSource[], - # Build a library which doesn't link to the standard library and - # forces the soft-float ABI - raw""" - mkdir -p "${libdir}" "${bindir}" - # This library has hard-float ABI - echo 'int test() { return 0; }' | cc -shared -fPIC -o "${libdir}/libhard.${dlext}" -x c - - # This library has soft-float ABI - echo 'int _start() { return 0; }' | /opt/${target}/bin/${target}-gcc -nostdlib -shared -mfloat-abi=soft -o "${libdir}/libsoft.${dlext}" -x c - - # hello_world built by Go doesn't specify any float ABI - make -C /usr/share/testsuite/go/hello_world/ - cp "/tmp/testsuite/${target}/go/hello_world/hello_world" "${bindir}/hello_world" - """, - # Build for Linux armv7l hard-float - [platform], - # Ensure our library product is built - [ - LibraryProduct("libhard", :libhard), - LibraryProduct("libsoft", :libsoft), - ExecutableProduct("hello_world", :hello_world), - ], - # No dependencies - Dependency[]; - compilers = [:c, :go], - verbose = true, - require_license = false - ) + @testset "hard-float ABI" begin + platform = Platform("armv7l", "linux"; call_abi = "eabihf", libc = "glibc") + mktempdir() do build_path + build_output_meta = @test_logs (:error, r"libsoft.so does not match the hard-float ABI") match_mode=:any begin + autobuild( + build_path, + "hard_float_ABI", + v"1.0.0", + # No sources + FileSource[], + # Build a library which doesn't link to the standard library and + # forces the soft-float ABI + raw""" + mkdir -p "${libdir}" "${bindir}" + # This library has hard-float ABI + echo 'int test() { return 0; }' | cc -shared -fPIC -o "${libdir}/libhard.${dlext}" -x c - + # This library has soft-float ABI + echo 'int _start() { return 0; }' | /opt/${target}/bin/${target}-gcc -nostdlib -shared -mfloat-abi=soft -o "${libdir}/libsoft.${dlext}" -x c - + # hello_world built by Go doesn't specify any float ABI + make -C /usr/share/testsuite/go/hello_world/ + cp "/tmp/testsuite/${target}/go/hello_world/hello_world" "${bindir}/hello_world" + """, + # Build for Linux armv7l hard-float + [platform], + # Ensure our library product is built + [ + LibraryProduct("libhard", :libhard), + LibraryProduct("libsoft", :libsoft), + ExecutableProduct("hello_world", :hello_world), + ], + # No dependencies + Dependency[]; + compilers = [:c, :go], + verbose = true, + require_license = false + ) + end + + @test haskey(build_output_meta, platform) + tarball_path, tarball_hash = build_output_meta[platform][1:2] + @test isfile(tarball_path) + + # Unpack it somewhere else + @test verify(tarball_path, tarball_hash) + testdir = joinpath(build_path, "testdir") + mkdir(testdir) + unpack(tarball_path, testdir) + # Remove libsoft.so, we want to run audit only on the other products + rm(joinpath(testdir, "lib", "libsoft.so")) + # Make sure `hello_world` passes the float ABI check even if it doesn't + # set `EF_ARM_ABI_FLOAT_HARD`. + @test Auditor.audit(Prefix(testdir); platform=platform, require_license=false) end + end + @testset "OS/ABI: $platform" for platform in [Platform("x86_64", "freebsd"), + Platform("aarch64", "freebsd")] + mktempdir() do build_path + build_output_meta = @test_logs (:warn, r"libwrong.so has an ELF header OS/ABI value that is not set to FreeBSD") match_mode=:any begin + autobuild( + build_path, + "OSABI", + v"4.2.0", + # No sources + FileSource[], + # Build a library with a mismatched OS/ABI in the ELF header + raw""" + mkdir -p "${libdir}" + echo 'int wrong() { return 0; }' | cc -shared -fPIC -o "${libdir}/libwrong.${dlext}" -x c - + echo 'int right() { return 0; }' | cc -shared -fPIC -o "${libdir}/libright.${dlext}" -x c - + # NetBSD runs anywhere, which implies that anything that runs is for NetBSD, right? + patchelf --set-os-abi 2 "${libdir}/libwrong.${dlext}" + """, + # Build for Linux armv7l hard-float + [platform], + # Ensure our library product is built + [ + LibraryProduct("libwrong", :libwrong), + LibraryProduct("libright", :libright), + ], + # No dependencies + Dependency[]; + verbose=true, + require_license=false, + ) + end - @test haskey(build_output_meta, platform) - tarball_path, tarball_hash = build_output_meta[platform][1:2] - @test isfile(tarball_path) + @test haskey(build_output_meta, platform) + tarball_path, tarball_hash = build_output_meta[platform][1:2] + @test isfile(tarball_path) - # Unpack it somewhere else - @test verify(tarball_path, tarball_hash) - testdir = joinpath(build_path, "testdir") - mkdir(testdir) - unpack(tarball_path, testdir) - # Remove libsoft.so, we want to run audit only on the other products - rm(joinpath(testdir, "lib", "libsoft.so")) - # Make sure `hello_world` passes the float ABI check even if it doesn't - # set `EF_ARM_ABI_FLOAT_HARD`. - @test Auditor.audit(Prefix(testdir); platform=platform, require_license=false) + # Unpack it somewhere else + @test verify(tarball_path, tarball_hash) + testdir = joinpath(build_path, "testdir") + mkdir(testdir) + unpack(tarball_path, testdir) + readmeta(joinpath(testdir, "lib", "libright.so")) do oh + @test is_for_platform(oh, platform) + @test check_os_abi(oh, platform) + end + readmeta(joinpath(testdir, "lib", "libwrong.so")) do oh + @test !is_for_platform(oh, platform) + @test !check_os_abi(oh, platform) + end + # Only audit the library we didn't mess with in the recipe + rm(joinpath(testdir, "lib", "libwrong.so")) + @test Auditor.audit(Prefix(testdir); platform=platform, require_license=false) + end end end