Skip to content

Commit 54fa6aa

Browse files
committed
Merge FEATURE-ci-macos-unwind: Declare selected compiler unwind runtime
2 parents 6a1709d + a2ef6ed commit 54fa6aa

7 files changed

Lines changed: 195 additions & 4 deletions

‎.github/workflows/v3-package-validation.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ jobs:
7272
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
7373
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")
7474
fi
75-
"${prefix[@]}" python3 scripts/check-v3-installed-consumer.py --output "$RUNNER_TEMP/package-evidence/matrix" --provider-prefix "$RUNNER_TEMP/package-evidence/prerequisites/provider" --pkg-config-dir "$(brew --prefix)/share/pkgconfig" --client-python "$RUNNER_TEMP/package-evidence/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --jobs 2
75+
"${prefix[@]}" python3 scripts/check-v3-installed-consumer.py --output "$RUNNER_TEMP/package-evidence/matrix" --provider-prefix "$RUNNER_TEMP/package-evidence/prerequisites/provider" --pkg-config-dir "$(brew --prefix)/share/pkgconfig" --client-python "$RUNNER_TEMP/package-evidence/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --jobs 2
7676
- name: Make privileged proof readable by artifact upload
7777
if: always()
7878
run: |

‎.github/workflows/v3-release-acceptance.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ jobs:
122122
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
123123
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")
124124
fi
125-
"${prefix[@]}" python3 scripts/run-v3-release-ci.py platform --selection "$RUNNER_TEMP/selected/selected.json" --archive "$RUNNER_TEMP/selected/"libhttpserver-*.tar.gz --provider-prefix "$RUNNER_TEMP/prerequisites/provider" --client-python "$RUNNER_TEMP/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --output "$RUNNER_TEMP/platform" --pkg-config-dir "$(brew --prefix)/share/pkgconfig"
125+
"${prefix[@]}" python3 scripts/run-v3-release-ci.py platform --selection "$RUNNER_TEMP/selected/selected.json" --archive "$RUNNER_TEMP/selected/"libhttpserver-*.tar.gz --provider-prefix "$RUNNER_TEMP/prerequisites/provider" --client-python "$RUNNER_TEMP/clients/bin/python" --node node --compiler "$V3_CXX" --cc "$V3_CC" --toolchain-prefix "$V3_PACKAGE_TOOLCHAIN" --output "$RUNNER_TEMP/platform" --pkg-config-dir "$(brew --prefix)/share/pkgconfig"
126126
- name: Make privileged proof readable by artifact upload
127127
if: always()
128128
run: |

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,12 @@ def runtime(path):
7272
return bool(toolchain and location.is_relative_to(Path(toolchain).resolve()) and
7373
any(fnmatch.fnmatch(location.name.lower(), pattern) for pattern in
7474
('libstdc++-*.dll', 'libgcc_s_*.dll', 'libwinpthread-*.dll')))
75+
if platform.system() == 'Darwin':
76+
toolchain = os.environ.get('V3_PACKAGE_TOOLCHAIN')
77+
location = Path(path).resolve()
78+
if (toolchain and location.parent == Path(toolchain).resolve() / 'lib/unwind'
79+
and re.fullmatch(r'libunwind(?:\.[0-9]+)+\.dylib', location.name)):
80+
return True
7581
trusted_roots = ('/libexec', '/usr/lib', '/lib', '/lib64', '/usr/lib64', '/usr/local/lib/gcc', '/opt/homebrew/Cellar/gcc', '/opt/homebrew/Cellar/llvm')
7682
return (any(fnmatch.fnmatch(Path(path).name, pattern) for pattern in RUNTIME)
7783
and any(Path(path).is_relative_to(Path(root)) for root in trusted_roots))

‎scripts/ci-macos-toolchain.sh‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,4 @@ CPP
3636
"$toolchain_dir/clang++" --version > "$toolchain_dir/compiler.log"
3737
"$toolchain_dir/clang++" -std=c++20 "$toolchain_dir/cancellation.cpp" -o "$toolchain_dir/cancellation"
3838
"$toolchain_dir/cancellation"
39-
printf 'V3_CXX=%s\nV3_CC=%s\n' "$toolchain_dir/clang++" "$llvm_prefix/bin/clang" >> "$GITHUB_ENV"
39+
printf 'V3_CXX=%s\nV3_CC=%s\nV3_PACKAGE_TOOLCHAIN=%s\n' "$toolchain_dir/clang++" "$llvm_prefix/bin/clang" "$llvm_prefix" >> "$GITHUB_ENV"

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ def setUp(self):
8585
environment.start()
8686
self.addCleanup(environment.stop)
8787
os.environ.pop('V3_PACKAGE_PREREQUISITES', None)
88+
os.environ.pop('V3_PACKAGE_TOOLCHAIN', None)
8889
self.tmp = tempfile.TemporaryDirectory()
8990
self.addCleanup(self.tmp.cleanup)
9091
self.root = Path(self.tmp.name)
@@ -212,6 +213,84 @@ def test_selected_native_provider_only(self):
212213
self.assertTrue(self.violations(graph, tls='no', surface='native'))
213214
self.assertTrue(audit.graph_violations(graph, 'strict', 'yes', 'native', self.policy, self.root / 'other-provider'))
214215

216+
def test_selected_darwin_unwind_accepts_observed_llvm_closure_only(self):
217+
prefix = Path('/opt/homebrew/Cellar/llvm/23.1.0')
218+
cxx = str(prefix / 'lib/c++/libc++.1.0.dylib')
219+
abi = str(prefix / 'lib/c++/libc++abi.1.0.dylib')
220+
unwind = str(prefix / 'lib/unwind/libunwind.1.0.dylib')
221+
graph = {'nodes': [{'id': path} for path in ('consumer', cxx, abi, unwind)],
222+
'edges': [{'parent': parent, 'name': child, 'child': child} for parent, child in
223+
[('consumer', cxx), (cxx, abi), (cxx, unwind), (abi, unwind)]]}
224+
with patch.object(audit.platform, 'system', return_value='Darwin'):
225+
for selected in ('', str(prefix.parent / '22.0.0'), str(prefix)):
226+
with self.subTest(selected=selected), patch.dict(os.environ, V3_PACKAGE_TOOLCHAIN=selected):
227+
errors = self.violations(graph)
228+
if selected == str(prefix):
229+
self.assertEqual([], errors)
230+
else:
231+
self.assertEqual([f'undeclared dependency (aggregate/strict): {parent} -> {unwind}'
232+
for parent in (cxx, abi)], errors)
233+
234+
def test_selected_darwin_unwind_requires_canonical_directory_and_numeric_name(self):
235+
prefix = self.root / 'llvm/23.1.0'
236+
directory = prefix / 'lib/unwind'
237+
directory.mkdir(parents=True)
238+
target = directory / 'libunwind.1.0.dylib'
239+
target.write_text('selected runtime')
240+
alias = self.root / 'selected llvm'
241+
alias.symlink_to(prefix, target_is_directory=True)
242+
link = directory / 'libunwind.dylib'
243+
link.symlink_to(target.name)
244+
with patch.object(audit.platform, 'system', return_value='Darwin'), \
245+
patch.dict(os.environ, V3_PACKAGE_TOOLCHAIN=str(alias)):
246+
self.assertTrue(audit.runtime(str(target)))
247+
self.assertTrue(audit.runtime(str(link)))
248+
for path in (prefix / 'lib/libunwind.1.0.dylib', prefix / 'lib/c++/libunwind.1.0.dylib',
249+
prefix.parent / 'other/lib/unwind/libunwind.1.0.dylib',
250+
directory / '../libunwind.1.0.dylib', '/usr/lib/libunwind.1.dylib',
251+
directory / 'libunwind.bad.dylib',
252+
directory / 'libunwind.1.dylib.extra', directory / 'libunwind.1..0.dylib',
253+
directory / 'libcurl.4.dylib'):
254+
with self.subTest(path=str(path)):
255+
self.assertFalse(audit.runtime(str(path)))
256+
outside = self.root / 'libunwind.2.dylib'
257+
outside.touch()
258+
escape = directory / 'libunwind.2.dylib'
259+
escape.symlink_to(outside)
260+
self.assertFalse(audit.runtime(str(escape)))
261+
target.unlink()
262+
directory.rename(prefix / 'lib/sibling')
263+
directory.symlink_to(prefix / 'lib/sibling', target_is_directory=True)
264+
(directory / target.name).touch()
265+
self.assertFalse(audit.runtime(str(directory / target.name)))
266+
267+
def test_selected_unwind_still_inspects_hashes_and_rejects_transitive_provider(self):
268+
prefix = self.root / 'llvm'
269+
directory = prefix / 'lib/unwind'
270+
directory.mkdir(parents=True)
271+
consumer, unwind, forbidden = self.root / 'consumer', directory / 'libunwind.1.dylib', self.root / 'libcrypto.dylib'
272+
links = {str(consumer): [str(unwind)], str(unwind): [str(forbidden)], str(forbidden): []}
273+
for path in links:
274+
Path(path).write_text('inspected ' + path)
275+
inspected = []
276+
def inspect(path):
277+
inspected.append(path)
278+
return [(child, child) for child in links[path]]
279+
with patch.object(audit.platform, 'system', return_value='Darwin'), \
280+
patch.dict(os.environ, V3_PACKAGE_TOOLCHAIN=str(prefix)):
281+
graph = audit.dependency_graph([str(consumer)], inspect)
282+
self.assertEqual(set(links), set(inspected))
283+
self.assertEqual({path: hashlib.sha256(Path(path).read_bytes()).hexdigest() for path in links},
284+
{node['id']: node['sha256'] for node in graph['nodes']})
285+
self.assertEqual(['unselected native provider: ' + str(forbidden)], audit.graph_violations(
286+
graph, 'strict', 'yes', 'aggregate', self.policy, self.root / 'provider'))
287+
forbidden.rename(self.root / 'libmicrohttpd.dylib')
288+
with self.assertRaisesRegex(audit.AuditError, 'unresolved dependency'):
289+
audit.dependency_graph([str(consumer)], inspect)
290+
with patch.object(audit.platform, 'system', return_value='Linux'), \
291+
patch.dict(os.environ, V3_PACKAGE_TOOLCHAIN=str(prefix)):
292+
self.assertFalse(audit.runtime(str(unwind)))
293+
215294
def test_unknown_system_library_is_not_allowed(self):
216295
graph = {'nodes': [{'id': 'consumer'}, {'id': '/usr/lib/libsurprise.dylib'}],
217296
'edges': [{'parent': 'consumer', 'name': '/usr/lib/libsurprise.dylib', 'child': '/usr/lib/libsurprise.dylib'}]}

‎scripts/test-v3-platform-ci.py‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import argparse
55
import importlib.util
66
import os
7+
import re
78
import shlex
89
import shutil
910
import sys
1011
from pathlib import Path
1112
import subprocess
1213
import tempfile
14+
import textwrap
1315
import unittest
1416
from unittest.mock import patch
1517

@@ -31,7 +33,7 @@ def test_native_platform_matrix_receives_matching_provider_cli(self):
3133
args = argparse.Namespace(selection=selection, output=root / 'output',
3234
archive=root / 'source.tar.gz', provider_prefix=root / 'provider',
3335
compiler='clang++', cc='clang', make='make', jobs=2,
34-
client_python='selected-python', node='node', pkg_config_dir=[], toolchain_prefix=None)
36+
client_python='selected-python', node='node', pkg_config_dir=[], toolchain_prefix=root / 'selected llvm')
3537
observed = []
3638

3739
class MatrixBoundary(Exception):
@@ -51,6 +53,8 @@ def command(argv, cwd, env, **kwargs):
5153
with self.assertRaises(MatrixBoundary):
5254
entrypoint.platform_proof(args)
5355
self.assertEqual(len(observed), 1)
56+
argv = observed[0][0]
57+
self.assertEqual(argv[argv.index('--toolchain-prefix') + 1], root / 'selected llvm')
5458
cli = 'openssl.exe' if os.name == 'nt' else 'openssl'
5559
self.assertEqual(observed[0][1]['V3_OPENSSL'], str(root / 'provider/bin' / cli))
5660
self.assertEqual(observed[0][1]['LD_LIBRARY_PATH'], str(root / 'provider/lib'))
@@ -97,6 +101,7 @@ def test_wrapper_selects_matching_library_and_preserves_arguments(self):
97101
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
98102
selected = dict(line.split('=', 1) for line in Path(env['GITHUB_ENV']).read_text().splitlines())
99103
self.assertEqual(selected['V3_CC'], str(llvm / 'bin/clang'))
104+
self.assertEqual(selected.get('V3_PACKAGE_TOOLCHAIN'), str(llvm))
100105
result = subprocess.run([selected['V3_CXX'], 'path with spaces.cpp', '-o', str(root / 'consumer')], env=env)
101106
self.assertEqual(result.returncode, 0)
102107
calls = [json.loads(line) for line in Path(env['INVOCATIONS']).read_text().splitlines()]
@@ -110,6 +115,47 @@ def test_wrapper_selects_matching_library_and_preserves_arguments(self):
110115
self.assertIn('-L' + str(llvm / 'lib/c++'), calls[-1])
111116
self.assertIn('-Wl,-rpath,' + str(llvm / 'lib/c++'), calls[-1])
112117

118+
def test_workflows_pass_selected_prefix_even_when_sudo_cleans_environment(self):
119+
for workflow, job, entrypoint in (
120+
('v3-package-validation.yml', 'macos', 'scripts/check-v3-installed-consumer.py'),
121+
('v3-release-acceptance.yml', 'darwin', 'scripts/run-v3-release-ci.py')):
122+
text = (ROOT / '.github/workflows' / workflow).read_text()
123+
job_text = re.split(r'^ [a-z0-9_-]+:', text.split(' ' + job + ':', 1)[1], maxsplit=1, flags=re.M)[0]
124+
lines = job_text.splitlines()
125+
invocation = next(i for i, line in enumerate(lines) if entrypoint in line and '${prefix[@]}' in line)
126+
start = next(i for i in range(invocation, -1, -1) if 'run: |' in lines[i])
127+
block = textwrap.dedent('\n'.join(lines[start + 1:invocation + 1]))
128+
for privileged in (False, True):
129+
with self.subTest(workflow=workflow, privileged=privileged), tempfile.TemporaryDirectory() as directory:
130+
root = Path(directory)
131+
result, env, llvm = self.prepare(root)
132+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
133+
selected = dict(line.split('=', 1) for line in Path(env['GITHUB_ENV']).read_text().splitlines())
134+
# Exercise the workflow's shell and argv; only native tools are substituted.
135+
python = root / 'tools/python3'
136+
python.write_text('#!' + sys.executable + '\n' + textwrap.dedent("""\
137+
import json, os, sys
138+
if sys.argv[1] == '-c':
139+
sys.exit(int(os.environ['PRIVILEGED']))
140+
with open(os.environ['CAPTURE'], 'w') as stream:
141+
json.dump({'argv': sys.argv[1:], 'toolchain_environment': os.environ.get('V3_PACKAGE_TOOLCHAIN')}, stream)
142+
"""))
143+
python.chmod(0o755)
144+
sudo = root / 'tools/sudo'
145+
sudo.write_text('#!/bin/sh\nexec env -i CAPTURE="$CAPTURE" "$@"\n')
146+
sudo.chmod(0o755)
147+
result = subprocess.run(['bash', '-e', '-c', block], cwd=ROOT,
148+
env=dict(env, **selected, PRIVILEGED=str(int(privileged)), CAPTURE=str(root / 'captured.json')),
149+
capture_output=True, text=True)
150+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
151+
captured = json.loads((root / 'captured.json').read_text())
152+
argv = captured['argv']
153+
self.assertEqual(argv[argv.index('--toolchain-prefix') + 1] if '--toolchain-prefix' in argv else None, str(llvm))
154+
self.assertEqual(argv[argv.index('--compiler') + 1], selected['V3_CXX'])
155+
self.assertEqual(argv[argv.index('--cc') + 1], selected['V3_CC'])
156+
if privileged:
157+
self.assertIsNone(captured['toolchain_environment'])
158+
113159
def test_nonfinal_modes_do_not_receive_runtime_linker_flags(self):
114160
with tempfile.TemporaryDirectory() as directory:
115161
root = Path(directory)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# FEATURE-ci-macos-unwind: selected Darwin LLVM unwind runtime
2+
3+
Status: Complete (local implementation, Groundwork validation and finalization). Native macOS package/archive execution remains
4+
CI / v3 PR owned. Base: `v3` at
5+
`6a1709daf6d5b51dac18a85c50030aeddd4cc570`.
6+
7+
Mac package job120396472543 in run39948363870 resolved LLVM23 libc++ and
8+
libc++abi dependencies on `lib/unwind/libunwind.1.0.dylib`, then rejected both
9+
edges as undeclared during the first private strict aggregate audit. The
10+
original bounded diagnosis and native log remain unchanged at root
11+
`.debug/FEATURE-ci-macos-unwind/run-taafd3p6/`.
12+
13+
The compiler selector now exports `V3_PACKAGE_TOOLCHAIN` from the same LLVM
14+
prefix that supplies its compiler and C++ runtime. Both Mac workflows pass
15+
that selection explicitly via the existing `--toolchain-prefix` option, so
16+
privileged execution retains it even when sudo filters the environment.
17+
Darwin runtime classification accepts only numerically versioned libunwind
18+
dylibs whose resolved parent is the canonical selected prefix's `lib/unwind`.
19+
Selected-prefix symlinks and dylib aliases to those files work; other directories,
20+
versions, malformed names, unrelated libraries and symlink escapes reject.
21+
Global runtime patterns, dyld-cache leaves and graph inspection remain unchanged.
22+
23+
Action items:
24+
25+
- [x] Create the isolated Groundwork worktree from the recorded v3 head;
26+
preserve the Windows entropy repair, bauth worktree and other prior fixes.
27+
- [x] Capture failing actual graph-policy tests reproducing the two native edges
28+
and failing compiler/workflow propagation tests before production edits.
29+
- [x] Export the selected LLVM prefix and pass it explicitly through both Mac
30+
package and selected-archive workflow commands, including their sudo paths.
31+
- [x] Restrict Darwin unwind classification to the canonical selected directory
32+
and numeric versioned basename; preserve strict provider and forbidden-library
33+
checks, real-file inspection, hashes and transitive traversal.
34+
- [x] Refute trust-boundary doubts using canonicalization/escape/negative controls,
35+
executed workflow shell blocks with a cleaned privileged environment, actual
36+
release dispatcher forwarding, and graph/hash/provider regressions.
37+
- [x] Run affected audit, platform, package-workflow and release-workflow tests;
38+
actionlint, Python compilation, Bash syntax and diff checks pass.
39+
- [x] Retain raw RED/GREEN logs, corrected fixture setup, doubts and command
40+
results under root `.debug/FEATURE-ci-macos-unwind/repair-selected-prefix/`.
41+
- [x] Caller: Groundwork validation and local finalization into v3; five reviewers approved with zero findings.
42+
- [ ] CI / v3 PR: genuine native macOS package and selected-archive execution.
43+
44+
Local verification: 40 installed-package policy/provenance tests, 7 platform
45+
execution tests, 4 package-workflow tests and 14 release-workflow contract tests
46+
pass. The new workflow test executes the actual Mac shell command blocks in
47+
both direct and simulated environment-cleaning sudo paths, with substituted
48+
native tool boundaries; it checks selected compiler and prefix arguments.
49+
These are policy/propagation fixtures, not native macOS installed-package proof.
50+
No full native baseline, CI rerun, Linux parity investigation, validation loop,
51+
merge or publication was performed. Native gates remain incomplete until CI.
52+
53+
Project notes: Autotools/C++20 uses bootstrap, out-of-tree configure and make.
54+
This scripts-only task needs no dependency installation; focused tests use
55+
Python unittest and the workflow suite's existing PyYAML dependency. Platform
56+
fixtures substitute only native executable/process boundaries. Python files
57+
have no documented standalone linter; changed-file `py_compile`, Bash syntax,
58+
existing actionlint1.7.7 and `git diff --check` passed. Worktree branch/path came
59+
verbatim from the Groundwork identity helper; Git metadata writes required
60+
sandbox escalation. Agent memory was initially empty.

0 commit comments

Comments
 (0)