Skip to content

Check for a branded ELF note when OS/ABI is NONE #1344

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/auditor/dynamic_linkage.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
using ObjectFile.ELF

function os_from_elf_note(oh::ELFHandle)
for section in Sections(oh)
section_type(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

Check warning on line 27 in src/auditor/dynamic_linkage.jl

View check run for this annotation

Codecov / codecov/patch

src/auditor/dynamic_linkage.jl#L27

Added line #L27 was not covered by tests
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good to know a blank line was not covered by tests!

image

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@test isempty(readlines("src/auditor/dynamic_linkage.jl")[27])

"""
platform_for_object(oh::ObjectHandle)

Expand Down Expand Up @@ -39,7 +64,9 @@
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

Check warning on line 69 in src/auditor/dynamic_linkage.jl

View check run for this annotation

Codecov / codecov/patch

src/auditor/dynamic_linkage.jl#L69

Added line #L69 was not covered by tests
return Platform(arch, "linux")
elseif oh.ei.osabi == ELF.ELFOSABI_FREEBSD
return Platform(arch, "freebsd")
Expand Down Expand Up @@ -108,6 +135,13 @@
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
Expand Down
17 changes: 15 additions & 2 deletions src/auditor/extra_checks.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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`
Expand Down
176 changes: 127 additions & 49 deletions test/auditing.jl
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -788,57 +789,134 @@ 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"Skipping binary analysis of lib/lib(nonote|badosabi)\.so \(incorrect platform\)") 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"""
apk update
apk add binutils
mkdir -p "${libdir}"
cd "${libdir}"
echo 'int wrong() { return 0; }' | cc -shared -fPIC -o "libwrong.${dlext}" -x c -
echo 'int right() { return 0; }' | cc -shared -fPIC -o "libright.${dlext}" -x c -
cp "libwrong.${dlext}" "libnonote.${dlext}"
# We only check for a branded ELF note when the OS/ABI is 0
elfedit --output-osabi=none "libnonote.${dlext}"
strip --remove-section=.note.tag "libnonote.${dlext}"
mv "libwrong.${dlext}" "libbadosabi.${dlext}"
# NetBSD runs anywhere, which implies that anything that runs is for NetBSD, right?
elfedit --output-osabi=NetBSD "libbadosabi.${dlext}"
""",
[platform],
# Ensure our library product is built
[
LibraryProduct("libbadosabi", :libbadosabi),
LibraryProduct("libnonote", :libnonote),
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 ohs
oh = only(ohs)
@test is_for_platform(oh, platform)
@test check_os_abi(oh, platform)
end
readmeta(joinpath(testdir, "lib", "libnonote.so")) do ohs
oh = only(ohs)
@test !is_for_platform(oh, platform)
@test !check_os_abi(oh, platform)
@test_logs((:warn, r"libnonote.so does not have a FreeBSD-branded ELF note"),
match_mode=:any, check_os_abi(oh, platform; verbose=true))
end
readmeta(joinpath(testdir, "lib", "libbadosabi.so")) do ohs
oh = only(ohs)
@test !is_for_platform(oh, platform)
@test !check_os_abi(oh, platform)
@test_logs((:warn, r"libbadosabi.so has an ELF header OS/ABI value that is not set to FreeBSD"),
match_mode=:any, check_os_abi(oh, platform; verbose=true))
end
# Only audit the library we didn't mess with in the recipe
for bad in ("nonote", "badosabi")
rm(joinpath(testdir, "lib", "lib$bad.so"))
end
@test Auditor.audit(Prefix(testdir); platform=platform, require_license=false)
end
end
end

Expand Down
Loading