Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
76 changes: 75 additions & 1 deletion .github/buildcheck/nativedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@
and the directories in GLIBC_CEILING, built in a manylinux image or against its sysroot, may not
reference a glibc symbol version above the floor they promise, nor a libstdc++ one (GLIBCXX_*,
CXXABI_*) above LIBSTDCXX_CEILING. Android libraries must also have every LOAD segment 16 KB
aligned.
aligned, and on s390x every typeinfo name (_ZTS*) of the static symbol table must sit at the even
address the ABI promises (TYPEINFO_NAME_ALIGNMENT; a jllama library without any is not examined,
which fails too).
That every listed build arrived is merge-native-artifacts.sh's check.

Exit codes: 0 clean, 1 violation, 2 nothing found to check.
Expand Down Expand Up @@ -262,6 +264,72 @@ def elf_exports(data):
return out


SHT_SYMTAB = 2
SHN_LORESERVE = 0xFF00


def elf_symbols(data):
"""(name, address) of every defined symbol of the static symbol table (.symtab), whatever its
binding: the export list and -fvisibility=hidden make almost every symbol of a shipped library
local there. A stripped library has no .symtab, and the list is empty."""
is64, end, sections = _elf_sections(data)
out = []
for sh_type, sh_offset, sh_size, sh_link, _ in sections:
if sh_type != SHT_SYMTAB:
continue
strtab = sections[sh_link][1]
entry = 24 if is64 else 16
for off in range(sh_offset, sh_offset + sh_size, entry):
if is64:
st_name, _, _, st_shndx, st_value = struct.unpack_from(end + "IBBHQ", data, off)
else:
st_name, st_value, _, _, _, st_shndx = struct.unpack_from(end + "IIIBBH", data, off)
if st_name == 0 or st_shndx == 0 or st_shndx >= SHN_LORESERVE:
continue
out.append((_cstring(data, strtab + st_name), st_value))
return out


# The s390x ELF ABI gives every global symbol an even address, and GCC relies on it: it loads an
# address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits typeinfo name
# strings (_ZTS*) with the alignment of char (ItaniumCXXABI.cpp, GetAddrOfTypeName), and the
# s390x build mixes clang's objects with GCC's -- gcc-toolset-14's libstdc++_nonshared.a, which
# loads _ZTSSt19_Sp_make_shared_tag with LARL. Whether a link then fails ("misaligned symbol")
# depends on where the copy it keeps lands: green on main, red in the next PR's jllama_test link
# (#577). .github/manylinux/s390x-typeinfo-names.ld (SUBALIGN(2) over every .rodata._ZTS*
# section) and -fdata-sections (which gives every typeinfo name such a section, the ones of
# classes with a key function included) make it hold for all of them; this check is what notices
# when they stop being in effect, in every build rather than in the one whose layout comes out odd.
TYPEINFO_NAME_ALIGNMENT = {"s390x": 2}


def typeinfo_name_alignment(rel):
"""The alignment the ABI of the directory of `rel` (.../<OS>/<ARCH>/<backend>/<library>)
requires for typeinfo names, or None."""
parts = rel.split("/")
return TYPEINFO_NAME_ALIGNMENT.get(parts[-3]) if len(parts) >= 4 else None


def typeinfo_name_violations(rel, symbols, alignment, required=True):
"""Typeinfo names (_ZTS*) among `symbols` ((name, address) pairs) at an address that is not a
multiple of `alignment`. A library that must carry them (`required`) and carries none was not
examined -- stripped, or its table unreadable -- which is never a pass."""
names = [(n, v) for n, v in symbols if n.startswith("_ZTS")]
if not names:
if not required:
return []
return [f"{rel}: no typeinfo name (_ZTS*) in its symbol table ({len(symbols)} symbols) -- stripped, or the "
f"table could not be read, so their {alignment}-byte alignment cannot be checked"]
odd = [n for n, v in names if v % alignment]
if not odd:
return []
shown = ", ".join(odd[:8]) + (f", ... ({len(odd)} in all)" if len(odd) > 8 else "")
return [f"{rel}: {len(odd)} of {len(names)} typeinfo names (_ZTS*) are not {alignment}-byte aligned: {shown} -- "
f"GCC code (gcc-toolset's libstdc++_nonshared.a) loads them with LARL, so a link that keeps such a "
f"copy fails with 'misaligned symbol'; .github/manylinux/s390x-typeinfo-names.ld or -fdata-sections "
f"(s390x-toolchain.cmake) is not in effect"]


def elf_load_alignments(data):
"""p_align of every PT_LOAD program header."""
if data[:4] != b"\x7fELF":
Expand Down Expand Up @@ -611,6 +679,7 @@ def main(argv):
rel = os.path.relpath(path, root).replace(os.sep, "/")
siblings = {n for n in os.listdir(os.path.dirname(path)) if n != os.path.basename(path)}
alignments, runpath, glibc, glibcxx, cxxabi, note = (), None, None, None, None, ""
typeinfo = []
try:
if path.endswith(".so"):
with open(path, "rb") as f:
Expand All @@ -620,6 +689,10 @@ def main(argv):
glibcxx, cxxabi = info["glibcxx"], info["cxxabi"]
if "/Linux-Android/" in "/" + rel:
alignments = elf_load_alignments(data)
symbol_alignment = typeinfo_name_alignment(rel)
if symbol_alignment:
typeinfo = typeinfo_name_violations(rel, elf_symbols(data), symbol_alignment,
required=os.path.basename(path) in JLLAMA_LIBRARIES)
note = (f" [runpath={runpath}]" if runpath else "") + "".join(
f" [{glibc_name(v, family)}]" for family, v in (("GLIBC", glibc), ("GLIBCXX", glibcxx),
("CXXABI", cxxabi)) if v)
Expand All @@ -630,6 +703,7 @@ def main(argv):
continue
print(f"{rel}: {' '.join(deps)}{note}")
failures += violations(rel, deps, alignments, siblings, runpath, glibc, glibcxx, cxxabi)
failures += typeinfo
if os.path.basename(path) in JLLAMA_LIBRARIES:
try:
names = exports(path)
Expand Down
121 changes: 114 additions & 7 deletions .github/buildcheck/tests/test_nativedeps.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@
from buildcheck import nativedeps


def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
def elf64(needed, runpath=None, glibc=(), exports=(), undefined=(), symtab=()):
"""A minimal little-endian ELF64 with a .dynstr and a .dynamic naming `needed` (and a
DT_RUNPATH when given), plus a .gnu.version_r requiring the `glibc` symbol versions, plus a
.dynsym defining `exports` (global) and referencing `undefined`."""
.dynsym defining `exports` (global) and referencing `undefined`, plus a .symtab defining the
`symtab` (name, address) pairs as local objects."""
strtab = b"\0"
offsets = {}
for name in (list(needed) + ([runpath] if runpath is not None else []) + (["libc.so.6"] if glibc else [])
+ list(glibc) + list(exports) + list(undefined)):
+ list(glibc) + list(exports) + list(undefined) + [n for n, _ in symtab]):
offsets[name] = len(strtab)
strtab += name.encode() + b"\0"
dynamic = b"".join(struct.pack("<qQ", 1, offsets[n]) for n in needed)
Expand All @@ -36,16 +37,20 @@ def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
dynsym += struct.pack("<IBBHQQ", offsets[name], (1 << 4) | 2, 0, 1, 0x1000, 8)
for name in undefined: # SHN_UNDEF: a reference, not an export
dynsym += struct.pack("<IBBHQQ", offsets[name], (1 << 4) | 2, 0, 0, 0, 0)
statics = bytes(24) # the null symbol
for name, address in symtab: # STB_LOCAL (0) << 4 | STT_OBJECT (1), defined in section 1
statics += struct.pack("<IBBHQQ", offsets[name], 1, 0, 1, address, 8)
header_size, section_size = 64, 64
strtab_off = header_size
dynamic_off = strtab_off + len(strtab)
verneed_off = dynamic_off + len(dynamic)
dynsym_off = verneed_off + len(verneed)
shoff = dynsym_off + len(dynsym)
symtab_off = dynsym_off + len(dynsym)
shoff = symtab_off + (len(statics) if symtab else 0)
header = bytearray(header_size)
header[:6] = b"\x7fELF\x02\x01"
struct.pack_into("<Q", header, 0x28, shoff)
nsections = 3 + (1 if glibc else 0) + (1 if exports or undefined else 0)
nsections = 3 + (1 if glibc else 0) + (1 if exports or undefined else 0) + (1 if symtab else 0)
struct.pack_into("<HH", header, 0x3A, section_size, nsections)
sections = bytes(section_size) # SHT_NULL
sections += struct.pack("<IIQQQQIIQQ", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0) # SHT_STRTAB
Expand All @@ -54,7 +59,47 @@ def elf64(needed, runpath=None, glibc=(), exports=(), undefined=()):
sections += struct.pack("<IIQQQQIIQQ", 0, 0x6FFFFFFE, 0, 0, verneed_off, len(verneed), 1, 1, 4, 0)
if exports or undefined:
sections += struct.pack("<IIQQQQIIQQ", 0, 11, 0, 0, dynsym_off, len(dynsym), 1, 1, 8, 24) # SHT_DYNSYM
return bytes(header) + strtab + dynamic + verneed + dynsym + sections
if symtab:
sections += struct.pack("<IIQQQQIIQQ", 0, 2, 0, 0, symtab_off, len(statics), 1, 1, 8, 24) # SHT_SYMTAB
return (bytes(header) + strtab + dynamic + verneed + dynsym + (statics if symtab else b"") + sections)


def elf_symtab(symbols, big_endian=False, is64=True):
"""A minimal ELF with only a .strtab and a .symtab: the (name, address, shndx) triples as local
objects -- in either byte order and class, s390x being big-endian ELF64."""
end = ">" if big_endian else "<"
strtab = b"\0"
offsets = {}
for name, _, _ in symbols:
offsets[name] = len(strtab)
strtab += name.encode() + b"\0"
entry = 24 if is64 else 16
table = bytes(entry)
for name, address, shndx in symbols:
if is64:
table += struct.pack(end + "IBBHQQ", offsets[name], 1, 0, shndx, address, 1)
else:
table += struct.pack(end + "IIIBBH", offsets[name], address, 1, 1, 0, shndx)
header_size = 64 if is64 else 52
section_size = 64 if is64 else 40
strtab_off = header_size
table_off = strtab_off + len(strtab)
shoff = table_off + len(table)
header = bytearray(header_size)
header[:6] = b"\x7fELF" + bytes([2 if is64 else 1, 2 if big_endian else 1])
if is64:
struct.pack_into(end + "Q", header, 0x28, shoff)
struct.pack_into(end + "HH", header, 0x3A, section_size, 3)
sections = bytes(section_size)
sections += struct.pack(end + "IIQQQQIIQQ", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0)
sections += struct.pack(end + "IIQQQQIIQQ", 0, 2, 0, 0, table_off, len(table), 1, 1, 8, entry)
else:
struct.pack_into(end + "I", header, 0x20, shoff)
struct.pack_into(end + "HH", header, 0x2E, section_size, 3)
sections = bytes(section_size)
sections += struct.pack(end + "IIIIIIIIII", 0, 3, 0, 0, strtab_off, len(strtab), 0, 0, 1, 0)
sections += struct.pack(end + "IIIIIIIIII", 0, 2, 0, 0, table_off, len(table), 1, 1, 4, entry)
return bytes(header) + strtab + table + sections


def elf64_segments(alignments):
Expand Down Expand Up @@ -455,6 +500,68 @@ def test_the_runtimes_the_jvm_itself_needs_stay_allowed(self):
self.assertIn("libgcc_s.so.1", nativedeps.ALLOWED[directory], directory)


class TypeinfoNameAlignmentTest(unittest.TestCase):
"""s390x: every typeinfo name (_ZTS*) at an even address, the ABI's promise GCC's LARL relies on
(.github/manylinux/s390x-typeinfo-names.ld)."""

def test_elf_symbols_reads_the_static_table_in_both_byte_orders(self):
symbols = [("_ZTS1X", 0x2001, 5), ("undefined", 0, 0), ("absolute", 0x10, 0xFFF1), ("_ZTSSt9exception", 0x40, 7)]
for big_endian in (False, True):
for is64 in (True, False):
data = elf_symtab(symbols, big_endian, is64)
self.assertEqual(nativedeps.elf_symbols(data), [("_ZTS1X", 0x2001), ("_ZTSSt9exception", 0x40)],
(big_endian, is64))
# the dynamic table is not the static one, and a library without .symtab has no symbols
self.assertEqual(nativedeps.elf_symbols(elf64(["libc.so.6"], exports=["JNI_OnLoad"])), [])
self.assertEqual(nativedeps.elf_symbols(elf64(["libc.so.6"], symtab=[("_ZTS1K", 0x30)])), [("_ZTS1K", 0x30)])

def test_only_s390x_asks_for_it(self):
self.assertEqual(nativedeps.typeinfo_name_alignment("x/Linux/s390x/cpu/libjllama.so"), 2)
for rel in ("x/Linux/x86_64/cpu/libjllama.so", "x/Linux/aarch64/cpu/libjllama.so",
"x/Linux-Android/aarch64/cpu/libjllama.so", "libjllama.so"):
self.assertIsNone(nativedeps.typeinfo_name_alignment(rel), rel)

def test_an_odd_typeinfo_name_is_named_and_an_even_one_passes(self):
rel = "x/Linux/s390x/cpu/libjllama.so"
self.assertEqual(nativedeps.typeinfo_name_violations(rel, [("_ZTS1X", 0x2000), ("odd_but_no_typeinfo", 0x3)], 2), [])
found = nativedeps.typeinfo_name_violations(
rel, [("_ZTSSt19_Sp_make_shared_tag", 0x12419CF), ("_ZTS1X", 0x2000), ("data", 0x1)], 2)
self.assertEqual(len(found), 1, found)
self.assertIn("1 of 2 typeinfo names", found[0])
self.assertIn("_ZTSSt19_Sp_make_shared_tag", found[0])
self.assertIn("s390x-typeinfo-names.ld", found[0])
many = [(f"_ZTS{i}X", 2 * i + 1) for i in range(10)]
self.assertIn("(10 in all)", nativedeps.typeinfo_name_violations(rel, many, 2)[0])

def test_a_library_without_typeinfo_names_is_never_a_pass_where_it_must_have_them(self):
found = nativedeps.typeinfo_name_violations("x/Linux/s390x/cpu/libjllama.so", [("JNI_OnLoad", 0x1000)], 2)
self.assertEqual(len(found), 1, found)
self.assertIn("no typeinfo name", found[0])
self.assertEqual(nativedeps.typeinfo_name_violations("x/Linux/s390x/cpu/libother.so", [], 2, required=False), [])

def _run(self, arch, symtab):
with tempfile.TemporaryDirectory() as root:
d = os.path.join(root, "net/ladenthin/llama/Linux", arch, "cpu")
os.makedirs(d)
with open(os.path.join(d, "libjllama.so"), "wb") as f:
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"], symtab=symtab))
out, err = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
rc = nativedeps.main(["x", root])
return rc, err.getvalue()

def test_main_holds_the_s390x_library_to_it_and_no_other(self):
self.assertEqual(self._run("s390x", [("_ZTS1X", 0x2000), ("_ZTSSt9exception", 0x2010)])[0], 0)
rc, err = self._run("s390x", [("_ZTS1X", 0x2000), ("_ZTSSt19_Sp_make_shared_tag", 0x2011)])
self.assertEqual(rc, 1)
self.assertIn("_ZTSSt19_Sp_make_shared_tag", err)
rc, err = self._run("s390x", [])
self.assertEqual(rc, 1, "a stripped s390x library is not examined, which is never a pass")
self.assertIn("no typeinfo name", err)
# x86-64's ABI promises no such thing, and its library is not held to it
self.assertEqual(self._run("x86_64", [("_ZTS1X", 0x2001)])[0], 0)


class MainTest(unittest.TestCase):

def test_checks_a_tree_and_reports_violations(self):
Expand All @@ -464,7 +571,7 @@ def test_checks_a_tree_and_reports_violations(self):
os.makedirs(ok)
os.makedirs(bad)
with open(os.path.join(ok, "libjllama.so"), "wb") as f:
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"]))
f.write(elf64(["libc.so.6"], exports=["JNI_OnLoad", "Java_a"], symtab=[("_ZTS1X", 0x2000)]))
self.assertEqual(nativedeps.main(["x", root]), 0)
with open(os.path.join(bad, "jllama.dll"), "wb") as f:
f.write(pe64(["kernel32.dll", "libomp140.x86_64.dll"], exports=["JNI_OnLoad", "Java_a"]))
Expand Down
13 changes: 10 additions & 3 deletions .github/manylinux/s390x-toolchain.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,17 @@ set(CMAKE_C_COMPILER_TARGET s390x-linux-gnu)
set(CMAKE_CXX_COMPILER_TARGET s390x-linux-gnu)

set(_jllama_s390x_gcc "${JLLAMA_S390X_SYSROOT}/opt/rh/gcc-toolset-14/root/usr/lib/gcc/s390x-redhat-linux/14")
set(CMAKE_C_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13")
set(CMAKE_CXX_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13")
# -fdata-sections: every typeinfo name gets a .rodata._ZTS* section of its own, so the linker
# script below reaches all of them -- without it the names of classes with a key function and of
# internal types (lambdas) stay in plain .rodata, 444 of 1833 of them at an odd address (measured).
set(CMAKE_C_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13 -fdata-sections")
set(CMAKE_CXX_FLAGS_INIT "--gcc-install-dir=${_jllama_s390x_gcc} -march=z13 -fdata-sections")
# s390x-typeinfo-names.ld: clang aligns typeinfo name strings like char, the GCC code of
# libstdc++_nonshared.a loads them with LARL, which needs an even address (see the script);
# verify-native-deps.py holds every typeinfo name of the library to it.
foreach(_kind EXE SHARED MODULE)
set(CMAKE_${_kind}_LINKER_FLAGS_INIT "--ld-path=/usr/bin/s390x-linux-gnu-ld")
set(CMAKE_${_kind}_LINKER_FLAGS_INIT
"--ld-path=/usr/bin/s390x-linux-gnu-ld -Wl,-T,${CMAKE_CURRENT_LIST_DIR}/s390x-typeinfo-names.ld")
endforeach()

set(CMAKE_CROSSCOMPILING_EMULATOR /usr/bin/env "QEMU_LD_PREFIX=${JLLAMA_S390X_SYSROOT}" /usr/bin/qemu-s390x-static)
Expand Down
33 changes: 33 additions & 0 deletions .github/manylinux/s390x-typeinfo-names.ld
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2026 Bernard Ladenthin <bernard.ladenthin@gmail.com>
*
* SPDX-License-Identifier: MIT OR Apache-2.0
*/

/*
* Augments GNU ld's default script for the s390x cross build (s390x-toolchain.cmake passes it with
* -T; INSERT keeps the default script in force): every typeinfo name string (_ZTS*) lands 2-byte
* aligned.
*
* The s390x ABI guarantees every global symbol an even address, and GCC relies on it: it loads a
* symbol's address with LARL, whose R_390_PC32DBL relocation counts in halfwords. clang emits the
* typeinfo name strings with the alignment of char instead (ItaniumCXXABI.cpp, GetAddrOfTypeName,
* unchanged on LLVM main in 2026-10), and as COMDAT, so the copy the link keeps is the first one --
* clang's, from our objects. gcc-toolset-14's libstdc++_nonshared.a, which the toolset's libstdc++.so
* script adds to every link, is GCC code: fs_path80.o's _M_get_deleter loads
* _ZTSSt19_Sp_make_shared_tag with LARL, and the link fails with "misaligned symbol ... for
* relocation R_390_PC32DBL" whenever the sizes of the strings before it add up to an odd number. A
* layout lottery: green on main, red on the next change (PR #577, the jllama_test link).
*
* SUBALIGN(2) sets the alignment of every input section of this output section to 2. Only typeinfo
* name strings are matched; they are char arrays read byte-wise, and 2 is all the ABI promises
* for them (GCC emits them with 2). INSERT BEFORE .rodata puts the statement ahead of the default
* .rodata pattern (.rodata.*), so it is the first to match these sections. -fdata-sections (in the
* toolchain) gives every typeinfo name such a section, those of classes with a key function and of
* internal types included, and verify-native-deps.py fails a library with one at an odd address.
*/
SECTIONS
{
.rodata.jllama_typeinfo_names : SUBALIGN(2) { *(.rodata._ZTS*) }
}
INSERT BEFORE .rodata;
Loading
Loading