Skip to content

Commit f37e7c7

Browse files
committed
Merge FEATURE-ci-bash-empty-arrays: preserve optional flags on legacy Bash
2 parents 103b112 + 21abf19 commit f37e7c7

5 files changed

Lines changed: 167 additions & 20 deletions

File tree

‎scripts/ci-freebsd-toolchain.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ def write_compiler(prefix, cxx):
6363
-static) rpath=() ;;
6464
esac
6565
done
66+
# Bash 3.2 nounset needs guarded empty arrays; absent flags must add zero arguments.
6667
exec ''' + shlex.join([cxx, '-nostdinc++', '-isystem', str(prefix / 'include/c++/v1')])
67-
+ ' "$@" "${link_flags[@]}" "${rpath[@]}"\n')
68+
+ ' "$@" ${link_flags[@]+"${link_flags[@]}"} ${rpath[@]+"${rpath[@]}"}\n')
6869
wrapper.chmod(0o755)
6970
return wrapper
7071

‎scripts/run-v3-protocol-fuzz.sh‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,10 @@ PYTHON
196196
fi
197197
link_flags=()
198198
if [ "$target" = quic_tls_callbacks ]; then link_flags+=("${provider_libs[@]}"); fi
199-
printf '%q ' "$compiler" "${flags[@]}" "${target_flags[@]}" "${sources[@]}" "${link_flags[@]}" -o "$build/$target/fuzz" > "$build/$target/compile.sh"
199+
# Bash 3.2 nounset needs guarded empty arrays; absent flags must add zero arguments.
200+
printf '%q ' "$compiler" "${flags[@]}" ${target_flags[@]+"${target_flags[@]}"} "${sources[@]}" ${link_flags[@]+"${link_flags[@]}"} -o "$build/$target/fuzz" > "$build/$target/compile.sh"
200201
printf '\n' >> "$build/$target/compile.sh"
201-
"$compiler" "${flags[@]}" "${target_flags[@]}" "${sources[@]}" "${link_flags[@]}" -o "$build/$target/fuzz" > "$build/$target/build.log" 2>&1
202+
"$compiler" "${flags[@]}" ${target_flags[@]+"${target_flags[@]}"} "${sources[@]}" ${link_flags[@]+"${link_flags[@]}"} -o "$build/$target/fuzz" > "$build/$target/build.log" 2>&1
202203
printf '%q ' "$build/$target/fuzz" "$target_corpus" -runs="$runs" -max_total_time="$seconds" -max_len="$max_len" -timeout=5 -rss_limit_mb=512 -seed="$seed" -artifact_prefix="$build/$target/artifacts/" > "$build/$target/reproduce.sh"
203204
printf '\n' >> "$build/$target/reproduce.sh"
204205
"$build/$target/fuzz" "$target_corpus" -runs="$runs" -max_total_time="$seconds" -max_len="$max_len" -timeout=5 -rss_limit_mb=512 -seed="$seed" -artifact_prefix="$build/$target/artifacts/" > "$build/$target/run.log" 2>&1 || { echo "fuzz failure: $target; see $build/$target/run.log and artifacts" >&2; exit 1; }

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

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -77,27 +77,25 @@ def test_compiler_wrapper_keeps_headers_and_runtime_through_clean_environment(se
7777
selector = self.selector()
7878
with tempfile.TemporaryDirectory() as directory:
7979
root = Path(directory)
80-
compiler = root / 'compiler'
80+
compiler = root / 'compiler with spaces'
8181
compiler.write_text('#!' + sys.executable + '\nimport json, sys\nprint(json.dumps(sys.argv[1:]))\n')
8282
compiler.chmod(0o755)
8383
prefix = root / 'selected sdk'
8484
wrapper = selector.write_compiler(prefix, str(compiler))
85-
for mode in ('', '-c', '-E', '-S', '-fsyntax-only', '-r', '-Wl,-r', '-static'):
85+
# The wrapper's env shebang must use the selected Bash even in the clean environment.
86+
environment = {'PATH': str(Path(shutil.which('bash')).parent) + os.pathsep + os.defpath}
87+
for mode in ('', '-c', '-E', '-S', '-fsyntax-only', '-r', '-Wl,-r', '--version', '-static', '-shared'):
8688
with self.subTest(mode=mode):
8789
args = ([mode] if mode else []) + ['path with spaces.cpp', '-o', 'path with spaces.o']
88-
result = subprocess.run([str(wrapper)] + args, env={'PATH': os.defpath}, capture_output=True, text=True)
90+
result = subprocess.run([str(wrapper)] + args, env=environment, capture_output=True, text=True)
8991
self.assertEqual(result.returncode, 0, result.stderr)
9092
actual = json.loads(result.stdout)
91-
self.assertIn('-nostdinc++', actual)
92-
self.assertEqual(actual[actual.index('-isystem') + 1], str(prefix / 'include/c++/v1'))
93-
self.assertIn('path with spaces.cpp', actual)
94-
if mode in ('-c', '-E', '-S', '-fsyntax-only', '-r', '-Wl,-r'):
95-
self.assertNotIn('-lc++', actual)
96-
self.assertFalse(any(flag.startswith('-Wl,-rpath,') for flag in actual))
97-
else:
98-
self.assertIn('-L' + str(prefix / 'lib'), actual)
99-
self.assertIn('-lc++', actual)
100-
self.assertEqual(any(flag.startswith('-Wl,-rpath,') for flag in actual), mode != '-static')
93+
expected = ['-nostdinc++', '-isystem', str(prefix / 'include/c++/v1')] + args
94+
if mode not in ('-c', '-E', '-S', '-fsyntax-only', '-r', '-Wl,-r', '--version'):
95+
expected += ['-nostdlib++', '-L' + str(prefix / 'lib'), '-lc++']
96+
if mode != '-static':
97+
expected += ['-Wl,-rpath,' + str(prefix / 'lib')]
98+
self.assertEqual(actual, expected)
10199

102100

103101
class SelectedPlatformEnvironment(unittest.TestCase):
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# FEATURE-ci-bash-empty-arrays: preserve optional compiler arguments on legacy Bash
2+
3+
Status: Complete — local implementation and validation complete; native confirmation remains CI/v3-PR owned.
4+
Base: `v3` at `103b11222e547f60471f6d43daad2759b08a8922`.
5+
Plan: `.groundwork-plans/FEATURE-ci-bash-empty-arrays-plan.md` in the primary checkout.
6+
Journal: `.debug/ci-bash-empty-arrays-q9c5zit7/journal.md` in the primary checkout.
7+
8+
Native Darwin archive run `40030063567`, job `120676108141`, demonstrates
9+
`link_flags[@]: unbound variable` in the protocol fuzz runner and the generated
10+
FreeBSD compiler wrapper, plus `rpath[@]: unbound variable` for static wrapper
11+
invocation. Both owners enable nounset and expand optional empty arrays.
12+
13+
Action items:
14+
15+
- [x] Read the assigned plan, literal journal, primary CLAUDE.md, applicable
16+
AGENTS and v3 architecture; isolate from the exact local v3 base.
17+
- [x] Establish actual Bash 3.2-family RED with the production fuzz runner and
18+
generated wrapper; pin the official source and retain argument evidence.
19+
- [x] Handle absent optional arrays portably while retaining zero optional
20+
arguments, quoted paths, ordering, mode selection and failure propagation.
21+
- [x] Verify empty target flags, empty link flags and populated provider flags;
22+
preserve fuzz instrumentation, bounds, corpus security and metadata.
23+
- [x] Run focused tests with legacy/current Bash and real SDK compile/link/run
24+
cancellation checks using the existing private libc++20 SDK.
25+
- [x] Check shell/Python syntax and whitespace; review the complete owned diff.
26+
- [x] Caller: perform full Groundwork validation, finalization and merge.
27+
- [ ] Native Darwin/FreeBSD post-repair execution: CI/v3-PR owned, not local.
28+
29+
Other closed or queued Darwin fixture and C++ failures remain outside this
30+
repair. No broad baseline, CI control, merge or publication belongs to this task.
31+
32+
Implementation and evidence (2026-10-23):
33+
34+
The fuzz runner guards optional `target_flags` and `link_flags` in both the
35+
recorded compile command and actual compiler invocation. The FreeBSD wrapper
36+
guards optional `link_flags` and `rpath`. Bash's conditional array expansion
37+
adds zero arguments for absent flags and retains each populated element as one
38+
argument. Nounset, all existing mode decisions, provider selection, source
39+
selection, instrumentation, limits, corpus validation and exit handling remain
40+
in place. No dependency version or host Bash changes are introduced.
41+
42+
The task-local interpreter was built from the official GNU Bash 3.2.57 source,
43+
SHA256 `3fa9daf85ebf35068f090ce51283ddeeb3c75eb5bc70b1a4a7cb05868bfe06a4`.
44+
The GNU detached signature verifies with the maintainer's HTTPS-served key,
45+
fingerprint `7C0135FB088AAF6C66C650B9BB5869F064EA74AB` (no configured web-of-trust
46+
certification). The source needed yacc to regenerate its patched parser;
47+
Ubuntu bison was extracted under the task proof directory, without installation.
48+
Actual runtime: GNU Bash `3.2.57(2)-release` on Linux; comparison runtime:
49+
`5.2.21(1)-release`. Native Darwin's exact interpreter patch level was not
50+
captured, so this establishes the real 3.2-family behavior, not native success.
51+
52+
Before production edits, legacy execution failed the H3/QPACK invocation at
53+
`link_flags[@]` and the HTTP/1 corpus invocation at `target_flags[@]`. The
54+
generated wrapper failed eight mode cases at empty `link_flags` or `rpath`,
55+
including `--version`. The current interpreter passed before the change.
56+
After the repair:
57+
58+
- Protocol fuzz runner: 13 tests pass with each interpreter, covering empty
59+
flags, populated provider flags with spaces, exact compile metadata, limits,
60+
configuration failures, restored corpora and symlink rejection.
61+
- FreeBSD toolchain: 4 tests pass with each interpreter. Exact argument vectors
62+
cover final/shared links, compile/preprocess/assembly/syntax/relocatable modes,
63+
version and static modes, using compiler and SDK paths containing spaces.
64+
- Real local Clang18 builds and runs the production HTTP/1 head fuzzer with
65+
unchanged sanitizer flags, runs/time bounds and a spaced build path under
66+
each interpreter. The initial sandboxed capability run failed because
67+
LeakSanitizer cannot run under ptrace; authorized execution outside that
68+
sandbox preserves the sanitizer gate and reaches the expected legacy RED,
69+
then GREEN. The failure receipt is retained.
70+
- Existing private libc++20 SDK: newly generated production wrappers compile,
71+
link and run the production cancellation probe under each interpreter;
72+
real compile/preprocess/assembly/syntax/version and relocatable checks pass.
73+
Raw compiler arguments, dependencies and source hashes are retained. Linux's
74+
forwarded `-Wl,-r` required caller flags `-no-pie -nostdlib` to suppress
75+
executable defaults; the two initial task-probe setup failures are retained.
76+
This is local Linux SDK selection proof, not native FreeBSD/libcxxrt ABI proof.
77+
- Shell syntax under both interpreters, Python compilation and
78+
`git diff --check` pass. CLAUDE.md's C++ cpplint command does not apply to these
79+
shell/Python changes; no Python/shell linter is configured there.
80+
81+
Durable source pin, signature, interpreter, RED/GREEN logs, raw argument
82+
receipts, exact source snapshots and task-executor notes are retained in the
83+
primary checkout's `.debug/ci-bash-empty-arrays-q9c5zit7/implementation/`.
84+
The task-local build and SDK probes remain in `/tmp/ci-bash-empty-arrays-proof`.
85+
All other failure assessments remain closed with their prior uncertainty.
86+
Post-repair native execution belongs to CI/v3 PR; overall CI is not green.
87+
88+
Final local validation/finalization (2026-10-23):
89+
90+
Groundwork sealed exact clean implementation `118db3bf3ba3eed0dcfb2b202ba76a2323f99dbe` / tree `ea89ccad7217f2861864c91d98bb052741c39dcd`: nine fresh independent profiles approved, 1 iteration, 0 fixed, 0 unworked. Durable receipt: `.git/groundwork/validation/cdb4ee2aea69cc6a/e0be2ef7dab5001c/groundwork-validation-3cefc1a6aa3b8eb2c15f656700685e35`. Existing hash-matched interpreter, regression, real fuzz and SDK receipts were reused; no broader suite or unrelated diagnostic was repeated. The initial spec-artifact automatic approval rejection was resolved with an evidence-based approval explicitly limited to local acceptance, preserving historical failures and native limits; no approval blocker remains. Caller changes only this bookkeeping before serial merge/cleanup. Native Darwin/FreeBSD remains unexecuted after this repair and assigned to CI/v3 PR. Other established selected-tool/fixture-path setup repairs follow serially; the five C++ causes remain uncertain and closed to further probes pending a later decision after independent work. Overall CI is not green.

‎test/integ/protocol_fuzz_runner_test.py‎

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,10 @@ def test_http3_qpack_build_and_invocation_contract(self):
9696
# This probe verifies shell arguments; the real sanitizer runs are separate.
9797
import json
9898
import os
99+
import shlex
99100
with tempfile.TemporaryDirectory() as directory:
100101
location = pathlib.Path(directory)
101-
compiler = location / 'compiler.py'
102+
compiler = location / 'compiler with spaces.py'
102103
compiler.write_text('''#!/usr/bin/env python3
103104
import json, os, pathlib, sys
104105
if '--version' in sys.argv:
@@ -110,15 +111,15 @@ def test_http3_qpack_build_and_invocation_contract(self):
110111
output.chmod(0o755)
111112
''')
112113
compiler.chmod(0o755)
113-
config = location / 'native'
114+
config = location / 'native config'
114115
config.mkdir()
115116
(config / 'config.h').write_text('/* TLS disabled */\n')
116-
build = location / 'fuzz'
117+
build = location / 'fuzz output'
117118
environment = dict(os.environ, FUZZ_ARGUMENT_LOG=str(location / 'compile.jsonl'),
118119
FUZZ_RUN_LOG=str(location / 'run.jsonl'))
119120
result = subprocess.run([
120121
'bash', str(ROOT / 'scripts/run-v3-protocol-fuzz.sh'),
121-
'--build-dir', str(build), '--targets', 'http3_engine qpack',
122+
'--build-dir', str(build), '--targets', 'http3_engine qpack http1_head',
122123
'--compiler', str(compiler), '--native-config-dir', str(config),
123124
'--runs', '9', '--seconds', '2'], env=environment,
124125
capture_output=True, text=True, timeout=15)
@@ -128,8 +129,13 @@ def test_http3_qpack_build_and_invocation_contract(self):
128129
for target, macro, length in [('http3_engine', '-DHTTP3_ENGINE_LIBFUZZER', 4096),
129130
('qpack', '-DQPACK_LIBFUZZER', 512)]:
130131
arguments = next(args for args in builds if macro in args)
132+
self.assertNotIn('', arguments)
131133
self.assertIn('-fsanitize=fuzzer,address,undefined', arguments)
134+
self.assertIn('-I' + str(config), arguments)
132135
self.assertIn(str(ROOT / 'src/detail/http3_connection.cpp'), arguments)
136+
self.assertEqual(arguments[-2:], ['-o', str(build / target / 'fuzz')])
137+
self.assertEqual(shlex.split((build / target / 'compile.sh').read_text()),
138+
[str(compiler)] + arguments)
133139
self.assertTrue((build / target / 'sources.list').is_file())
134140
invocation = next(args for args in runs if args[0] == str(build / target / 'fuzz'))
135141
for flag in ('-runs=9', '-max_total_time=2', '-timeout=5', '-rss_limit_mb=512',
@@ -138,6 +144,57 @@ def test_http3_qpack_build_and_invocation_contract(self):
138144
self.assertEqual(invocation[1], str(build / target / 'seeds'))
139145
self.assertTrue(list((build / target / 'seeds').glob('*.seed')))
140146
self.assertTrue((build / target / 'reproduce.sh').is_file())
147+
head = next(args for args in builds if str(ROOT / 'test/fuzz/http1_head_fuzz.cpp') in args)
148+
self.assertNotIn('', head)
149+
self.assertIn('-fsanitize=fuzzer,address,undefined', head)
150+
self.assertEqual(head[-2:], ['-o', str(build / 'http1_head/fuzz')])
151+
self.assertEqual(shlex.split((build / 'http1_head/compile.sh').read_text()),
152+
[str(compiler)] + head)
153+
self.assertEqual(len(builds), 4) # Instrumentation probe and three selected targets.
154+
155+
def test_callback_provider_arguments_preserve_boundaries_and_link_order(self):
156+
import json
157+
import os
158+
import shlex
159+
with tempfile.TemporaryDirectory() as directory:
160+
location = pathlib.Path(directory)
161+
compiler = location / 'compiler with spaces'
162+
compiler.write_text('''#!/usr/bin/env python3
163+
import json, os, pathlib, sys
164+
if '--version' in sys.argv:
165+
print('test argument recorder'); sys.exit(0)
166+
with open(os.environ['FUZZ_ARGUMENT_LOG'], 'a') as log:
167+
log.write(json.dumps(sys.argv[1:]) + '\\n')
168+
output = pathlib.Path(sys.argv[sys.argv.index('-o') + 1])
169+
output.write_text('#!/bin/sh\\nexit 0\\n')
170+
output.chmod(0o755)
171+
''')
172+
compiler.chmod(0o755)
173+
config = location / 'native config'
174+
(config / 'src').mkdir(parents=True)
175+
(config / 'config.h').write_text('#define NATIVE_V3_TLS 1\n')
176+
cflags = ['-I' + str(location / 'provider headers'), '-DPROVIDER_VALUE=two words']
177+
libs = ['-L' + str(location / 'provider libs'), '-lssl', '-lcrypto']
178+
(config / 'src/Makefile').write_text('V3_TLS_CFLAGS = ' + shlex.join(cflags)
179+
+ '\nV3_TLS_LIBS = ' + shlex.join(libs) + '\n')
180+
build = location / 'fuzz output'
181+
log = location / 'compile.jsonl'
182+
result = subprocess.run(['bash', str(ROOT / 'scripts/run-v3-protocol-fuzz.sh'),
183+
'--build-dir', str(build), '--targets', 'quic_tls_callbacks',
184+
'--compiler', str(compiler), '--native-config-dir', str(config),
185+
'--runs', '1', '--seconds', '1'],
186+
env=dict(os.environ, FUZZ_ARGUMENT_LOG=str(log)),
187+
capture_output=True, text=True, timeout=15)
188+
self.assertEqual(result.returncode, 0, result.stderr)
189+
arguments = json.loads(log.read_text().splitlines()[-1])
190+
self.assertNotIn('', arguments)
191+
start = arguments.index('-DQUIC_TLS_CALLBACKS_LIBFUZZER')
192+
self.assertEqual(arguments[start:start + 5],
193+
['-DQUIC_TLS_CALLBACKS_LIBFUZZER', '-I' + str(ROOT / 'test'), '-I' + str(config)] + cflags)
194+
self.assertEqual(arguments[-5:], libs + ['-o', str(build / 'quic_tls_callbacks/fuzz')])
195+
self.assertEqual(shlex.split((build / 'quic_tls_callbacks/compile.sh').read_text()),
196+
[str(compiler)] + arguments)
197+
self.assertEqual((build / 'quic_tls_callbacks/provider.log').read_text().splitlines(), cflags + libs)
141198

142199
def test_seed_and_corpus_arguments_reject_invalid_inputs_before_compilation(self):
143200
for seed in ('0', '-1', '4294967296', '$(touch marker)', '12x'):

0 commit comments

Comments
 (0)