Skip to content

Commit 6b2f596

Browse files
committed
Merge FEATURE-ci-freebsd-readelf-format: Parse native FreeBSD dependencies
2 parents e56a14f + e5b2e58 commit 6b2f596

4 files changed

Lines changed: 177 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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# FEATURE-ci-freebsd-readelf-format
2+
3+
Status: Complete (local validation/finalization; native CI pending).
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+
- [x] 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 passed and local finalization is recorded below. Native FreeBSD
51+
post-fix package/archive checks remain CI/v3 PR owned and incomplete.
52+
53+
Groundwork validation `ea1324dc403a04447c1cce7f522e9740` sealed PASS for
54+
`1172ebdcdc41fbec480f6fe3805b75d238b09eb1`: one iteration, nine independent
55+
approvals, zero fixes and one approved unworked finding persisted by the helper.
56+
The helper report is retained unread for separately owned work. Native FreeBSD
57+
post-fix package/archive validation remains incomplete and CI/v3 PR owned.
58+
Prior failed receipts and closed uncertainty remain preserved.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Unworked Review Issues
2+
3+
**Run:** 2026-10-23 23:56:07
4+
**Task:** FEATURE-ci-freebsd-readelf-format
5+
**Total:** 1 (0 critical, 1 major, 0 minor)
6+
7+
## Major
8+
9+
1. [ ] **security-reviewer** | `scripts/audit-v3-installed-package.py:206` | input-validation
10+
The new whitespace-excluding name pattern silently drops a recognized NEEDED entry whenever another entry matches. A bounded real-inspector probe with GNU rows for libc.so.6 and libunexpected name.so returns only libc.so.6, whereas the original parser raises missing resolution for libunexpected name.so. The omitted library is never visited, hashed or checked by graph_violations. This violates the accepted fail-closed recursive graph requirement and can conceal an undeclared runtime dependency (CWE-20).
11+
*Recommendation:* Fail closed when a recognized NEEDED row cannot be completely parsed or resolved; unsupported names need not be accepted, but must not disappear from an otherwise successful inspection. Add a mixed valid/unsupported NEEDED regression verifying rejection rather than a partial dependency list.

0 commit comments

Comments
 (0)