Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e005b8b
feat(arm64e): add Apple visionOS target
cypherair Nov 10, 2026
474fdfe
feat(arm64e): list the pauth features in the arm64e-apple-darwin spec
cypherair Nov 10, 2026
37ba468
feat(arm64e): turn on upstream's pointer authentication for arm64e Ap…
cypherair Nov 10, 2026
bd42d02
feat(arm64e): sign every function pointer that becomes a value
cypherair Nov 10, 2026
7fe49fb
feat(arm64e): leave Mach-O initializer pointers unsigned, as clang does
cypherair Nov 10, 2026
cc7aa33
fix(arm64e): reject flags that would change the arm64e pointer-authen…
cypherair Nov 10, 2026
79c51fa
ci(arm64e): run the fork's validation on pull requests again
cypherair Nov 10, 2026
c580abd
test(arm64e): check that disabling v8.3a keeps pointer authentication
cypherair Nov 10, 2026
1022ec9
ci(arm64e): run the validation on the xcode-27 image
cypherair Nov 10, 2026
d1c10fa
test(arm64e): fix four codegen tests for LLVM 23 and Rust 1.100
cypherair Nov 10, 2026
12272e2
ci(arm64e): run every test suite even when one fails
cypherair Nov 10, 2026
a4e38bc
ci(arm64e): give the tidy step an explicit stage
cypherair Nov 10, 2026
e43b12f
ci(arm64e): keep later validation steps running after a failure
cypherair Nov 10, 2026
22cec46
ci(arm64e): fix tidy's format check, and smoke-test unwinding on its own
cypherair Nov 10, 2026
2c3d2be
fix(arm64e): sign landing pads for the arm64e unwinder
cypherair Nov 10, 2026
e40632f
ci(arm64e): check unwinding at runtime, and block on it
cypherair Nov 10, 2026
4c2dc2d
ci(arm64e): run tidy on the stable channel, as upstream does
cypherair Nov 10, 2026
55ef406
fix(arm64e): make the scope checks a match, and other review notes
cypherair Nov 10, 2026
57169cf
test(arm64e): cover the direct callees, a reified call and plain arm64
cypherair Nov 10, 2026
5605432
ci(arm64e): require pointer authentication, check std, run at -O0
cypherair Nov 10, 2026
e31c9c9
test(arm64e): fix the direct-callees test and the tidy line-length op…
cypherair Nov 10, 2026
3351bf7
ci(arm64e): make every runtime case blocking
cypherair Nov 10, 2026
76fbeb9
test(arm64e): declare the marker lang items the direct-callees test n…
cypherair Nov 10, 2026
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
117 changes: 117 additions & 0 deletions .github/arm64e/check_objects.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Checks every object in the given rlibs or archives for code that pointer authentication misses.

Fails on:
- a function that saves its return address (x30) without signing it with `pacibsp`;
- an indirect call with a plain `blr`;
- an indirect branch with a plain `br`, unless it is a jump-table dispatch;
- a data relocation that stores a function's address unsigned.

Usage: check_objects.py LLVM_BIN_DIR ARCHIVE...
"""

import pathlib
import re
import subprocess
import sys
import tempfile

# Sections whose relocations to code are not function pointers that code calls through.
UNSIGNED_OK_SECTIONS = {
"__text",
"__compact_unwind",
"__eh_frame",
"__gcc_except_tab",
"__mod_init_func", # left unsigned on purpose, like clang's own constructors
"__mod_term_func",
"__init_offsets",
"__thread_vars", # TLV descriptors name `_tlv_bootstrap`; dyld binds them
}

FUNCTION_HEADER = re.compile(r"^[0-9a-f]+ <(.+)>:$")
INSTRUCTION = re.compile(r"^\s+[0-9a-f]+:\s+(\S+)\s*(.*)$")
SECTION_HEADER = re.compile(r"^RELOCATION RECORDS FOR \[(.+)\]:$")
RELOCATION = re.compile(r"^[0-9a-f]+\s+(\S+)\s+(\S+)$")
SYMBOL = re.compile(r"^[0-9a-f]+\s+[lg ]\s+F\s+__TEXT,__text\s+(\S+)$")


def run(tool, *args):
command = [str(tool), *map(str, args)]
return subprocess.run(command, check=True, capture_output=True, text=True).stdout


def function_symbols(objdump, obj):
lines = run(objdump, "-t", obj).splitlines()
return {m.group(1) for line in lines if (m := SYMBOL.match(line))}


def check_code(objdump, obj, problems):
functions = []
for line in run(objdump, "-d", "--no-show-raw-insn", obj).splitlines():
if m := FUNCTION_HEADER.match(line):
functions.append((m.group(1), []))
elif (m := INSTRUCTION.match(line)) and functions:
functions[-1][1].append((m.group(1), m.group(2)))
for name, insns in functions:
mnemonics = [m for m, _ in insns]
# The registers stored, not the address: `stp x29, x30, [sp, #-0x10]!`.
stored = [ops.split("[")[0] for m, ops in insns if m in ("stp", "str", "stur")]
saves_lr = any("x30" in registers for registers in stored)
signs_lr = "pacibsp" in mnemonics
if saves_lr and not signs_lr:
problems.append(f"{obj.name}: {name} saves x30 without pacibsp")
for i, (m, ops) in enumerate(insns):
if m == "blr":
problems.append(f"{obj.name}: {name} calls through a plain blr {ops}")
elif m == "br" and "adr" not in mnemonics[max(0, i - 6) : i]:
problems.append(f"{obj.name}: {name} branches through a plain br {ops}")
return len(functions)


def check_data(objdump, obj, functions, problems):
authenticated = 0
section = None
for line in run(objdump, "-r", obj).splitlines():
if m := SECTION_HEADER.match(line):
section = m.group(1)
elif (m := RELOCATION.match(line)) and section is not None:
kind, target = m.groups()
if kind == "ARM64_RELOC_AUTHENTICATED_POINTER":
authenticated += 1
elif (
kind == "ARM64_RELOC_UNSIGNED"
and section not in UNSIGNED_OK_SECTIONS
and not section.startswith(("__debug", "__apple")) # DWARF names code addresses
):
if target in functions or target == "__text":
problems.append(f"{obj.name}: [{section}] stores {target} unsigned")
return authenticated


def main():
llvm_bin = pathlib.Path(sys.argv[1])
archives = [pathlib.Path(a) for a in sys.argv[2:]]
objdump, ar = llvm_bin / "llvm-objdump", llvm_bin / "llvm-ar"
if not archives:
sys.exit("no archives given")
problems = []
with tempfile.TemporaryDirectory() as tmp:
objects = []
for archive in archives:
out = pathlib.Path(tmp) / archive.stem
out.mkdir()
run(ar, "x", f"--output={out}", archive)
objects += sorted(out.glob("*.o"))
functions = set().union(*(function_symbols(objdump, o) for o in objects))
n_functions = sum(check_code(objdump, o, problems) for o in objects)
n_auth = sum(check_data(objdump, o, functions, problems) for o in objects)
print(f"{len(archives)} archives, {len(objects)} objects, {n_functions} functions, "
f"{n_auth} authenticated pointers in data, {len(problems)} problems")
for problem in problems[:200]:
print(problem)
# The data check needs the function symbols to recognise a relocation to a function.
if not objects or not functions or not n_functions or not n_auth or problems:
sys.exit(1)


if __name__ == "__main__":
main()
Loading