Skip to content

Commit 1172ebd

Browse files
committed
FEATURE-ci-freebsd-readelf-format: recognize native FreeBSD ELF needed rows
Accept captured numeric/bare NEEDED rows alongside GNU parenthesized tags, matching complete shared-library entries to exclude malformed tags and diagnostic noise. Preserve all dynamic-section, resolution, runtime and graph policy checks. Retain exact native stdout with origin and hash. Verify 53 installed-package tests, four consumer failure contracts, package/archive evidence recovery, a real GNU ELF graph, Python compilation and diff checks. Native FreeBSD post-fix execution remains CI owned and incomplete.
1 parent e56a14f commit 1172ebd

3 files changed

Lines changed: 159 additions & 1 deletion

File tree

‎scripts/audit-v3-installed-package.py‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ def platform_inspector(path):
200200
dynamic = run(['readelf', '-d', path])
201201
if 'Dynamic section' not in dynamic:
202202
raise AuditError('missing ELF dynamic section: ' + path)
203-
names = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic)
203+
# FreeBSD readelf uses bare tags; GNU readelf parenthesizes them.
204+
names = re.findall(
205+
r'^[ \t]*(?:0x[0-9a-fA-F]+[ \t]+(?:NEEDED|\(NEEDED\))|\(NEEDED\))'
206+
r'[ \t]+Shared library:[ \t]+\[([^\]\s]+)\][ \t]*$', dynamic, re.M)
204207
if not names:
205208
if runtime(path):
206209
return []

‎scripts/test-v3-installed-package.py‎

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,110 @@ def test_current_umbrella_has_no_backend_types(self):
643643
self.assertEqual([], audit.header_violations(result, []))
644644

645645

646+
# Exact /usr/bin/readelf -d stdout from FreeBSD package job 120773457461,
647+
# run 40060736308, artifact 12550179871; SHA256 1f60a5922ad44f8a14c69bed7b378558f0f3c02056597781045b37e9610a4a45.
648+
FREEBSD_NATIVE_DYNAMIC = (
649+
'\n'
650+
'Dynamic section at offset 0x27530 contains 30 entries:\n'
651+
' Tag Type Name/Value\n'
652+
' 0x000000000000001d RUNPATH Library runpath: [/tmp/v3-package-40060736308-1/package-evidence/matrix/tls-no/prefix/lib:/tmp/v3-package-40060736308-1/package-evidence/prerequisites/provider/lib:/tmp/v3-package-40060736308-1/package-evidence/toolchain/lib]\n'
653+
' 0x0000000000000001 NEEDED Shared library: [libhttpserver.so.3]\n'
654+
' 0x0000000000000001 NEEDED Shared library: [libc++.so.1]\n'
655+
' 0x0000000000000001 NEEDED Shared library: [libcxxrt.so.1]\n'
656+
' 0x0000000000000001 NEEDED Shared library: [libm.so.5]\n'
657+
' 0x0000000000000001 NEEDED Shared library: [libgcc_s.so.1]\n'
658+
' 0x0000000000000001 NEEDED Shared library: [libthr.so.3]\n'
659+
' 0x0000000000000001 NEEDED Shared library: [libc.so.7]\n'
660+
' 0x0000000000000015 DEBUG 0x0\n'
661+
' 0x0000000000000007 RELA 0x205d40\n'
662+
' 0x0000000000000008 RELASZ 432 (bytes)\n'
663+
' 0x0000000000000009 RELAENT 24 (bytes)\n'
664+
' 0x0000000000000017 JMPREL 0x205ef0\n'
665+
' 0x0000000000000002 PLTRELSZ 2040 (bytes)\n'
666+
' 0x0000000000000003 PLTGOT 0x22a970\n'
667+
' 0x0000000000000014 PLTREL RELA\n'
668+
' 0x0000000000000006 SYMTAB 0x200308\n'
669+
' 0x000000000000000b SYMENT 24 (bytes)\n'
670+
' 0x0000000000000005 STRTAB 0x2021b8\n'
671+
' 0x000000000000000a STRSZ 15233 (bytes)\n'
672+
' 0x000000006ffffef5 GNU_HASH 0x201808\n'
673+
' 0x0000000000000004 HASH 0x201b70\n'
674+
' 0x0000000000000019 INIT_ARRAY \n'
675+
' 0x000000000000001b INIT_ARRAYSZ 8 (bytes)\n'
676+
' 0x000000000000000c INIT 0x227f80\n'
677+
' 0x000000000000000d FINI 0x227f8c\n'
678+
' 0x000000006ffffff0 VERSYM 0x2015c8\n'
679+
' 0x000000006ffffffe VERNEED 0x201758\n'
680+
' 0x000000006fffffff VERNEEDNUM 4\n'
681+
' 0x0000000000000000 NULL 0x0\n'
682+
)
683+
684+
685+
class ElfInspectorTest(unittest.TestCase):
686+
def inspect(self, dynamic, resolved='', host='FreeBSD'):
687+
def command(argv, **kwargs):
688+
output = {'readelf': dynamic, 'ldd': resolved}[argv[0]]
689+
return subprocess.CompletedProcess(argv, 0, output, '')
690+
audit.COMMANDS.clear()
691+
with patch.object(audit.platform, 'system', return_value=host), \
692+
patch.object(subprocess, 'run', side_effect=command):
693+
return audit.platform_inspector('/package/consumer')
694+
695+
def test_captured_freebsd_needed_rows_resolve_all_seven_dependencies(self):
696+
names = ['libhttpserver.so.3', 'libc++.so.1', 'libcxxrt.so.1', 'libm.so.5',
697+
'libgcc_s.so.1', 'libthr.so.3', 'libc.so.7']
698+
resolved = ''.join(f' {name} => /selected/lib/{name} (0x1)\n' for name in names)
699+
700+
dependencies = self.inspect(FREEBSD_NATIVE_DYNAMIC, resolved)
701+
702+
self.assertEqual([(name, '/selected/lib/' + name) for name in names], dependencies)
703+
self.assertEqual(FREEBSD_NATIVE_DYNAMIC, audit.COMMANDS[0]['stdout'])
704+
self.assertEqual(0, audit.COMMANDS[0]['returncode'])
705+
self.assertEqual('', audit.COMMANDS[0]['stderr'])
706+
707+
def test_elf_inspection_requires_dynamic_section_and_nonempty_dependencies(self):
708+
for host in ('FreeBSD', 'Linux'):
709+
for dynamic, error in (
710+
('0x1 NEEDED Shared library: [libc.so.7]\n', 'missing ELF dynamic section'),
711+
('Dynamic section at offset 0 contains 1 entry:\n 0x0 NULL 0x0\n',
712+
'empty ELF dependency inspection')):
713+
with self.subTest(host=host, dynamic=dynamic), self.assertRaisesRegex(audit.AuditError, error):
714+
self.inspect(dynamic, host=host)
715+
716+
def test_needed_dependencies_require_successful_ldd_resolution(self):
717+
for host in ('FreeBSD', 'Linux'):
718+
for resolved, error in (
719+
('libc++.so.1 => not found\n', 'unresolved ELF dependency'),
720+
('libhttpserver.so.3 => /package/lib/libhttpserver.so.3 (0x1)\n',
721+
r'missing resolution for libc\+\+\.so\.1')):
722+
with self.subTest(host=host, resolved=resolved), self.assertRaisesRegex(audit.AuditError, error):
723+
self.inspect(FREEBSD_NATIVE_DYNAMIC, resolved, host)
724+
725+
def test_malformed_tags_and_non_needed_noise_cannot_supply_dependencies(self):
726+
for row in (
727+
'0x1 NOTNEEDED Shared library: [libnoise.so]',
728+
'0x1 NEEDED_EXTRA Shared library: [libnoise.so]',
729+
'0x1 (NEEDED Shared library: [libnoise.so]',
730+
'0x1 NEEDED) Shared library: [libnoise.so]',
731+
'diagnostic (NEEDED) Shared library: [libnoise.so]',
732+
'0x1 RUNPATH Shared library: [libnoise.so]',
733+
'0x1 NEEDED Library runpath: [libnoise.so]',
734+
'0x1 NEEDED Shared library: []',
735+
'0x1 NEEDED Shared library: [libnoise.so',
736+
'0x1 (NEEDED) Shared library: [libnoise.so] trailing noise'):
737+
with self.subTest(row=row), self.assertRaisesRegex(audit.AuditError, 'empty ELF dependency inspection'):
738+
self.inspect('Dynamic section at offset 0 contains 1 entry:\n ' + row + '\n',
739+
'libnoise.so => /selected/lib/libnoise.so (0x1)\n')
740+
741+
def test_non_needed_rows_do_not_add_edges_to_valid_gnu_inspection(self):
742+
dynamic = ('Dynamic section at offset 0 contains 3 entries:\n'
743+
' 0x1 (NEEDED) Shared library: [libc.so.6]\n'
744+
' 0x1 NOTNEEDED Shared library: [libnoise.so]\n'
745+
' diagnostic (NEEDED) Shared library: [libnoise.so]\n')
746+
dependencies = self.inspect(dynamic, 'libc.so.6 => /lib/libc.so.6 (0x1)\n', 'Linux')
747+
self.assertEqual([('libc.so.6', '/lib/libc.so.6')], dependencies)
748+
749+
646750
class AuditFixturePathTest(unittest.TestCase):
647751
def test_unwind_contract_under_symlinked_temporary_root(self):
648752
original_tempdir = tempfile.tempdir
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# FEATURE-ci-freebsd-readelf-format
2+
3+
Status: In Progress.
4+
Base: local v3 `e56a14f0f6748209ac39c1319bc1c894529ab2a4`.
5+
Plan: primary `.groundwork-plans/FEATURE-ci-freebsd-readelf-format-plan.md`.
6+
Journal: primary `.debug/ci-7880-freebsd-readelf-6x0vi2k0/journal.md`.
7+
8+
Accept the captured FreeBSD bare NEEDED dynamic tag column in the installed
9+
package auditor, preserving GNU parenthesized tags and fail-closed inspection,
10+
resolution, recursive graph evidence, runtime classification and package policy.
11+
The native receipt supplies new evidence for this repair; prior closed unknown
12+
assessments remain closed. Scope is the ELF needed-format parser and its tests.
13+
14+
- [x] Read accepted plan, primary instructions, native stdout and existing owner/tests; isolate from exact local v3.
15+
- [x] Record meaningful RED through the real inspector with exact native stdout and all seven expected names.
16+
- [x] Make the minimal parser repair with GNU compatibility and strict malformed/noise controls.
17+
- [x] Verify affected suite, direct consumer/failure evidence contracts, real local GNU ELF, Python compilation and diff checks.
18+
- [ ] Caller Groundwork validation/finalization.
19+
- [ ] Native FreeBSD post-fix package/archive checks: CI/v3 PR owned, incomplete; not a local acceptance gate.
20+
21+
No runtime leaves, whitelists, loader/policy relaxation, inspector retries or new
22+
native format assumptions are authorized. No broad baseline/SDK/native probe,
23+
CI control, merge, publication or cleanup belongs to this implementation.
24+
25+
Local implementation evidence (2026-10-23):
26+
27+
The native stdout is retained literally in the existing distributed test module,
28+
with its command/job/artifact origin and SHA256. The real platform inspector and
29+
command recorder, with only subprocess output controlled, resolve all seven
30+
expected names to exact paths. The old parser raised `empty ELF dependency
31+
inspection` on this regression. The replacement recognizes full bare/parenthesized
32+
NEEDED rows; malformed tags, wrong labels, empty names and surrounding diagnostic
33+
noise cannot supply dependency entries. Existing GNU fixtures remain unchanged.
34+
Dynamic-section, nonruntime empty-result, unresolved/missing ldd resolution,
35+
recursive provenance/hash and runtime/TLS/legacy policy requirements remain.
36+
37+
Verification: all 53 installed-package tests pass (48 existing, five new); all
38+
four installed-consumer inspection-failure tests and the direct package/archive
39+
receipt-recovery test pass. A small real local GNU C++ ELF goes through real
40+
readelf/ldd and recursive graph inspection: six hashed nodes, 11 edges, strict
41+
TLS-off policy passes. This proves local GNU compatibility, not native FreeBSD
42+
loader behavior. Python compilation and diff whitespace checks pass; there is
43+
no documented Python lint command. Original 54 existing helper/test method ASTs
44+
are unchanged, and the embedded native stdout matches all 1968 captured bytes.
45+
46+
Logs, source hashes, local GNU graph/commands and limits are retained in primary
47+
`.debug/ci-7880-freebsd-readelf-6x0vi2k0/implementation/`. One mistyped recovery
48+
test selector failed at discovery; its log is retained separately, the exact
49+
existing selector was then used and passed. No subsystem failure was observed.
50+
Caller Groundwork validation/finalization remains pending. Native FreeBSD
51+
post-fix package/archive checks remain CI/v3 PR owned and incomplete.

0 commit comments

Comments
 (0)