Skip to content

Commit bcd8f61

Browse files
committed
FEATURE-ci-darwin-test-contracts: honor native fixture and peer setup contracts
Check returned continuation behavior with a retained coroutine, exercise five actual QPACK allocations and blocked-request nonpublication, and derive the shared output fixture budget from real retained capacity. Prepare only the selected Darwin archive proof's missing loopback alias and clean up owned state on success, failures and signals. Focused GNU and actual libc++20 Linux runs each pass 47 cases / 2901 checks. Full-check Clang ASan/UBSan lifetime run passes 1816 checks outside sandbox ptrace. Workflow suite (28 tests), changed C++ cpplint, actionlint, Python/shell syntax and diff checks pass. Native post-fix platform checks remain CI/v3 PR owned; caller validation/finalization pending.
1 parent a5a7235 commit bcd8f61

6 files changed

Lines changed: 283 additions & 11 deletions

File tree

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,43 @@ jobs:
118118
"$RUNNER_TEMP/clients/bin/python" -m pip install -r test/integ/http2-client-requirements.txt
119119
- name: Complete native and installed TLS off/on proof
120120
run: |
121+
peer_alias_owned=false
122+
cleanup_peer_alias() {
123+
status=$?
124+
trap - EXIT HUP INT TERM
125+
if "$peer_alias_owned"; then
126+
if ! sudo ifconfig lo0 inet 127.0.0.2 -alias; then
127+
echo 'peer-policy: failed to remove owned lo0 alias 127.0.0.2' >&2
128+
if [ "$status" -eq 0 ]; then status=1; fi
129+
fi
130+
fi
131+
exit "$status"
132+
}
133+
trap cleanup_peer_alias EXIT
134+
trap 'exit 129' HUP
135+
trap 'exit 130' INT
136+
trap 'exit 143' TERM
137+
if ! loopback=$(ifconfig lo0 inet); then
138+
echo 'peer-policy: failed to query lo0 IPv4 addresses' >&2
139+
exit 1
140+
fi
141+
if ! awk '$1 == "inet" && $2 == "127.0.0.2" { found=1 } END { exit !found }' <<< "$loopback"; then
142+
if ! sudo ifconfig lo0 inet 127.0.0.2 netmask 255.255.255.255 alias; then
143+
echo 'peer-policy: failed to create lo0 alias 127.0.0.2' >&2
144+
exit 1
145+
fi
146+
peer_alias_owned=true
147+
fi
148+
if ! python3 - <<'PY'
149+
import socket
150+
for address in ('127.0.0.1', '127.0.0.2'):
151+
with socket.socket() as peer:
152+
peer.bind((address, 0))
153+
PY
154+
then
155+
echo 'peer-policy: both loopback source addresses must bind before proof' >&2
156+
exit 1
157+
fi
121158
prefix=()
122159
if ! python3 -c 'import socket; s=socket.socket(); s.bind(("127.0.0.1",443)); s.close()'; then
123160
prefix=(sudo env "PATH=$PATH" GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=safe.directory "GIT_CONFIG_VALUE_0=$PWD")

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

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,5 +530,149 @@ def test_all_modified_workflows_pin_actions(self):
530530
self.assertRegex(use, r'@[0-9a-f]{40}$')
531531

532532

533+
@unittest.skipIf(os.name == 'nt', 'Darwin proof uses a POSIX shell')
534+
class DarwinPeerAliasLifecycle(unittest.TestCase):
535+
def execute(self, *, preexisting=False, failure='', signal_name=''):
536+
with tempfile.TemporaryDirectory() as directory:
537+
root = Path(directory)
538+
tools = root / 'tools'
539+
tools.mkdir()
540+
(root / 'scripts').mkdir()
541+
state = root / 'alias'
542+
if preexisting:
543+
state.touch()
544+
capture = root / 'calls.jsonl'
545+
boundary = '''import json, os, pathlib, sys
546+
sys.excepthook = sys.__excepthook__
547+
def record(value):
548+
with open(os.environ['CAPTURE'], 'a') as stream:
549+
stream.write(json.dumps(value) + '\\n')
550+
state = pathlib.Path(os.environ['ALIAS_STATE'])
551+
failure = os.environ['FAILURE']
552+
'''
553+
scripts = {
554+
'ifconfig': boundary + '''
555+
args = sys.argv[1:]
556+
record(['ifconfig'] + args)
557+
if args == ['lo0', 'inet']:
558+
if failure == 'query': sys.exit(43)
559+
print('lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST>')
560+
print(' inet 127.0.0.1 netmask 0xff000000')
561+
print(' inet 127.0.0.20 netmask 0xffffffff')
562+
if state.exists(): print(' inet 127.0.0.2 netmask 0xffffffff')
563+
elif args == ['lo0', 'inet', '127.0.0.2', 'netmask', '255.255.255.255', 'alias']:
564+
assert not state.exists(), 'preexisting alias must be preserved'
565+
if failure == 'create': sys.exit(47)
566+
state.touch()
567+
elif args == ['lo0', 'inet', '127.0.0.2', '-alias']:
568+
assert state.exists(), 'cleanup needs an owned alias'
569+
if 'cleanup' in failure: sys.exit(53)
570+
state.unlink()
571+
else:
572+
raise AssertionError(args)
573+
''',
574+
'sudo': 'import os, sys\nos.execvp(sys.argv[1], sys.argv[1:])\n',
575+
'brew': "print('/fixture/homebrew')\n",
576+
}
577+
for name, code in scripts.items():
578+
tool = tools / name
579+
tool.write_text('#!' + sys.executable + '\n' + code)
580+
tool.chmod(0o755)
581+
(tools / 'python3').symlink_to(sys.executable)
582+
# Execute the workflow's actual preflight Python, replacing only
583+
# the OS socket boundary so Linux cannot mask Darwin's bind refusal.
584+
(root / 'socket.py').write_text(boundary + '''
585+
class socket:
586+
def __enter__(self): return self
587+
def __exit__(self, *args): self.close()
588+
def close(self): pass
589+
def bind(self, address):
590+
record(['bind', *address])
591+
if address[0] == '127.0.0.2' and (not state.exists() or failure == 'preflight'):
592+
raise OSError(49, 'Cannot assign requested address')
593+
''')
594+
(root / 'scripts/run-v3-release-ci.py').write_text(boundary + '''
595+
import signal
596+
record(['proof'] + sys.argv[1:])
597+
assert state.exists(), 'proof requires the second peer source address'
598+
if os.environ['SIGNAL_NAME']:
599+
os.kill(os.getppid(), getattr(signal, os.environ['SIGNAL_NAME']))
600+
if 'proof' in failure: sys.exit(61)
601+
''')
602+
lane = workflow('v3-release-acceptance.yml')
603+
job = lane['jobs']['darwin']
604+
step = next(step for step in job['steps']
605+
if step.get('name') == 'Complete native and installed TLS off/on proof')
606+
env = dict(os.environ, PATH=str(tools) + os.pathsep + os.environ['PATH'],
607+
PYTHONPATH=str(root), CAPTURE=str(capture), ALIAS_STATE=str(state),
608+
FAILURE=failure, SIGNAL_NAME=signal_name, RUNNER_TEMP=str(root),
609+
V3_CXX='/selected/clang++', V3_CC='/selected/clang', V3_PACKAGE_TOOLCHAIN='/selected')
610+
result = subprocess.run(workflow_shell(lane, job, step) + [step['run']], cwd=root,
611+
env=env, capture_output=True, text=True, timeout=10)
612+
calls = [json.loads(line) for line in capture.read_text().splitlines()] if capture.exists() else []
613+
return result, calls, state.exists()
614+
615+
def assert_preflight(self, calls):
616+
proof = next(i for i, call in enumerate(calls) if call[0] == 'proof')
617+
for address in ('127.0.0.1', '127.0.0.2'):
618+
self.assertIn(['bind', address, 0], calls[:proof])
619+
620+
def test_owned_alias_surrounds_proof_and_both_actual_bind_preflights(self):
621+
result, calls, remains = self.execute()
622+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
623+
self.assert_preflight(calls)
624+
self.assertEqual(calls[0], ['ifconfig', 'lo0', 'inet'])
625+
self.assertEqual(calls[-1], ['ifconfig', 'lo0', 'inet', '127.0.0.2', '-alias'])
626+
self.assertFalse(remains)
627+
proof = next(call for call in calls if call[0] == 'proof')
628+
self.assertIn('/selected/clang++', proof)
629+
630+
def test_preexisting_alias_survives_success_and_proof_failure(self):
631+
for failure, status in (('', 0), ('proof', 61)):
632+
with self.subTest(failure=failure):
633+
result, calls, remains = self.execute(preexisting=True, failure=failure)
634+
self.assertEqual(result.returncode, status, result.stdout + result.stderr)
635+
self.assert_preflight(calls)
636+
self.assertEqual([call for call in calls if call[0] == 'ifconfig'], [['ifconfig', 'lo0', 'inet']])
637+
self.assertTrue(remains)
638+
639+
def test_setup_and_preflight_fail_visibly_before_proof(self):
640+
for failure in ('query', 'create', 'preflight'):
641+
with self.subTest(failure=failure):
642+
result, calls, remains = self.execute(failure=failure)
643+
self.assertNotEqual(result.returncode, 0)
644+
self.assertIn('peer-policy', result.stderr)
645+
self.assertFalse(any(call[0] == 'proof' for call in calls))
646+
self.assertFalse(remains)
647+
cleanup = ['ifconfig', 'lo0', 'inet', '127.0.0.2', '-alias']
648+
self.assertEqual(cleanup in calls, failure == 'preflight')
649+
650+
def test_preexisting_alias_survives_failed_bind_preflight(self):
651+
result, calls, remains = self.execute(preexisting=True, failure='preflight')
652+
self.assertNotEqual(result.returncode, 0)
653+
self.assertIn('peer-policy', result.stderr)
654+
self.assertFalse(any(call[0] == 'proof' for call in calls))
655+
self.assertEqual([call for call in calls if call[0] == 'ifconfig'], [['ifconfig', 'lo0', 'inet']])
656+
self.assertTrue(remains)
657+
658+
def test_owned_alias_is_cleaned_after_proof_failure_and_signals(self):
659+
for failure, signal_name, status in (('proof', '', 61), ('', 'SIGHUP', 129),
660+
('', 'SIGINT', 130), ('', 'SIGTERM', 143)):
661+
with self.subTest(failure=failure, signal=signal_name):
662+
result, calls, remains = self.execute(failure=failure, signal_name=signal_name)
663+
self.assertEqual(result.returncode, status, result.stdout + result.stderr)
664+
self.assertEqual(calls[-1], ['ifconfig', 'lo0', 'inet', '127.0.0.2', '-alias'])
665+
self.assertFalse(remains)
666+
667+
def test_cleanup_failure_is_visible_and_preserves_prior_failure(self):
668+
for failure, status in (('cleanup', 1), ('proof-cleanup', 61)):
669+
with self.subTest(failure=failure):
670+
result, calls, remains = self.execute(failure=failure)
671+
self.assertEqual(result.returncode, status, result.stdout + result.stderr)
672+
self.assertIn('peer-policy', result.stderr)
673+
self.assertEqual(calls[-1], ['ifconfig', 'lo0', 'inet', '127.0.0.2', '-alias'])
674+
self.assertTrue(remains)
675+
676+
533677
if __name__ == '__main__':
534678
unittest.main()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# FEATURE-ci-darwin-test-contracts
2+
3+
Status: In Progress (implementation ready; caller validation/finalization pending)
4+
Base: local v3 `a5a72356741300e5da16b94090f0f26a67a2d1f7`
5+
6+
Repair four native-reproduced test/setup assumptions without changing production
7+
runtime, quota accounting, APIs, peer addresses or unrelated workflows.
8+
9+
- [x] Test returned continuation behavior with an explicitly owned coroutine frame.
10+
- [x] Inject all five actual QPACK precommit allocations and prove blocked request nonpublication, with a successful control.
11+
- [x] Size the shared response fixture budget from actual reserved string capacity.
12+
- [x] Prepare and clean up only the Darwin selected-archive proof's owned loopback alias; execute lifecycle regressions.
13+
- [x] Run focused local checks and record native verification limits.
14+
- [ ] Caller validation and finalization.
15+
16+
Evidence: primary `.debug/ci-darwin-test-contracts-8m2spsq4/journal.md`.
17+
Native RED batch 40055653509 is closed. Diagnostic refs f6475daf/88cd82fd
18+
must never merge or cherry-pick into v3. Header/outbox incidents remain closed
19+
with uncertain cause; this task does not retry or repair them. Native post-fix
20+
Darwin, Windows and BSD checks remain incomplete and CI/v3 PR owned.
21+
22+
Local verification: each of the three focused C++ targets passes with GNU and
23+
actual libc++20 on Linux (47 cases / 2901 checks per standard library, no skips).
24+
The lifetime target and concurrency implementation pass Clang ASan/UBSan with
25+
leak and stack checking enabled (3 cases / 1816 checks), using supported execution
26+
outside sandbox ptrace. Workflow tests pass (28 tests, including 6 lifecycle tests
27+
covering 13 absent/preexisting/failure/signal scenarios). Changed-file cpplint,
28+
actionlint, workflow Bash syntax, Python compilation and diff whitespace pass.
29+
30+
TDD uses the retained native RED for the three platform-specific fixture failures;
31+
new workflow lifecycle regressions failed before the setup change and pass after it.
32+
Original GCC sized-delete sanitizer setup and sandbox LSan failures are preserved
33+
in the journal; neither is reported as a passing run. Two Automake wrappers were
34+
interrupted during unrelated prerequisite compilation before tests; verification
35+
then executed only the three requested binaries. No production owner expansion,
36+
full baseline suite, native rerun, CI control or publication occurred.

‎test/unit/http3_qpack_test.cpp‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
#include <cstdlib>
44
#include <new>
55
#include <limits>
6+
#include <string>
67
#include "./littletest.hpp"
78
namespace {
89
int allocation_countdown = -1;
10+
bool allocation_failed = false;
911
} // namespace
1012
void* operator new(std::size_t size) {
1113
if (allocation_countdown == 0) {
14+
allocation_failed = true;
1215
allocation_countdown = -1; throw std::bad_alloc();
1316
}
1417
if (allocation_countdown > 0) --allocation_countdown;
@@ -125,17 +128,37 @@ LT_BEGIN_AUTO_TEST(http3_qpack_suite, local_settings_advertise_admitted_limits)
125128
LT_CHECK_EQ(capacity, 128U); LT_CHECK_EQ(blocked, 2U); LT_CHECK_EQ(connect, 1U);
126129
LT_END_AUTO_TEST(local_settings_advertise_admitted_limits)
127130
LT_BEGIN_AUTO_TEST(http3_qpack_suite, instruction_allocation_failure_is_internal_and_does_not_publish)
128-
for (int stage = 0; stage < 3; ++stage) {
131+
// Exceed supported string inline capacities for both decode and table copies.
132+
const std::string name(32, 'a'), value(32, 'b');
133+
LT_ASSERT(name.size() > std::string().capacity());
134+
const auto encoded = hd::qpack_encode_encoder_instruction(
135+
{hd::qpack_encoder_instruction_kind::insert_literal, 0, false, name, value}, {100, 100, 100});
136+
LT_ASSERT(encoded.status.ok());
137+
const auto wire = std::as_bytes(std::span(encoded.value));
138+
// Control, decoded name/value, table name/value copies, prepared table node.
139+
for (int stage = -1; stage < 5; ++stage) {
129140
auto root = budget(); hd::quic_storage_pool pool(10000, 30000, root); auto limits = dynamic_limits(); limits.qpack_capacity = 256;
130-
hd::http3_connection c(pool.data(), pool.critical(), limits); c.attach_stream(2);
141+
hd::http3_connection c(pool.data(), pool.critical(), limits); c.attach_stream(0); c.attach_stream(2);
131142
LT_CHECK(!feed(c, 2, bytes({2, 0x3f, 0xe1, 1})).error);
132-
auto wire = bytes({0x54, 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 20,
133-
'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b', 'b'});
143+
LT_CHECK(!feed(c, 0, bytes({1, 3, 2, 0, 0x80})).error);
144+
LT_CHECK_EQ(root.in_use(hs::resource::blocked_headers), 1U);
145+
LT_CHECK(!c.event(0));
134146
LT_CHECK(!feed(c, 2, std::span(wire).first(wire.size() - 1)).error);
147+
allocation_failed = false;
135148
allocation_countdown = stage;
136149
auto result = feed(c, 2, std::span(wire).last(1)); allocation_countdown = -1;
137-
LT_ASSERT(result.error); LT_CHECK_EQ(result.error->wire_code, 0x102U);
138-
LT_CHECK(!c.event(0));
150+
LT_CHECK_EQ(allocation_failed, stage >= 0);
151+
if (stage < 0) {
152+
LT_CHECK(!result.error);
153+
LT_ASSERT(c.event(0));
154+
LT_ASSERT_EQ(c.event(0)->fields.size(), 1U);
155+
LT_CHECK_EQ(c.event(0)->fields[0].name, name);
156+
LT_CHECK_EQ(c.event(0)->fields[0].value, value);
157+
LT_CHECK_EQ(root.in_use(hs::resource::blocked_headers), 0U);
158+
} else {
159+
LT_ASSERT(result.error); LT_CHECK_EQ(result.error->wire_code, 0x102U);
160+
LT_CHECK(!c.event(0));
161+
}
139162
}
140163
LT_END_AUTO_TEST(instruction_allocation_failure_is_internal_and_does_not_publish)
141164
LT_BEGIN_AUTO_TEST(http3_qpack_suite, encoded_byte_limit_persists_until_event_release)

‎test/unit/resource_limits_test.cpp‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,10 @@ LT_BEGIN_AUTO_TEST(resource_limits_suite, h1_output_retirement_restores_shared_c
179179
LT_CHECK_EQ(budget.in_use(hs::resource::response_queue_bytes), 0u);
180180
LT_END_AUTO_TEST(h1_output_retirement_restores_shared_capacity_for_a_long_response)
181181
LT_BEGIN_AUTO_TEST(resource_limits_suite, h1_partial_output_keeps_retained_prefix_charged_until_refill_or_drain)
182-
auto budget = root(hs::resource::response_queue_bytes, 512);
182+
std::string allocation;
183+
allocation.reserve(512);
184+
const auto retained_capacity = allocation.capacity();
185+
auto budget = root(hs::resource::response_queue_bytes, retained_capacity);
183186
hd::http1_response_outbox outbox({512, 512}, budget);
184187
auto& sink = outbox.open(1, {});
185188
http::request_head request; request.request_protocol = http::protocol::http_1_1;
@@ -188,10 +191,10 @@ LT_BEGIN_AUTO_TEST(resource_limits_suite, h1_partial_output_keeps_retained_prefi
188191
outbox.consume_front(outbox.queued_bytes());
189192
const std::array<std::byte, 512> payload{};
190193
LT_ASSERT(sink.push(payload).kind == hd::body_push::accepted);
191-
LT_CHECK_EQ(budget.in_use(hs::resource::response_queue_bytes), 512u);
194+
LT_CHECK_EQ(budget.in_use(hs::resource::response_queue_bytes), retained_capacity);
192195
LT_CHECK_EQ(outbox.consume_front(511), 511u);
193196
LT_CHECK_EQ(outbox.queued_bytes(), 1u);
194-
LT_CHECK_EQ(budget.in_use(hs::resource::response_queue_bytes), 512u);
197+
LT_CHECK_EQ(budget.in_use(hs::resource::response_queue_bytes), retained_capacity);
195198
hd::http1_response_outbox sibling({512, 512}, budget);
196199
auto& excess = sibling.open(1, {});
197200
LT_CHECK(excess.start(request, http::status::from_code(200), fields).code() == http::outcome_code::limit_exceeded);

‎test/unit/task_resume_signal_lifetime_test.cpp‎

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,28 @@ namespace h = httpserver;
3838
#endif
3939
#ifndef LHS_SKIP_NEW_OVERRIDE
4040
namespace {
41+
struct suspended_probe {
42+
struct promise_type {
43+
suspended_probe get_return_object() {
44+
return suspended_probe(std::coroutine_handle<promise_type>::from_promise(*this));
45+
}
46+
std::suspend_always initial_suspend() noexcept { return {}; }
47+
std::suspend_always final_suspend() noexcept { return {}; }
48+
void return_void() noexcept {}
49+
void unhandled_exception() { std::terminate(); }
50+
};
51+
explicit suspended_probe(std::coroutine_handle<promise_type> frame) : frame(frame) {}
52+
suspended_probe(const suspended_probe&) = delete;
53+
suspended_probe& operator=(const suspended_probe&) = delete;
54+
std::coroutine_handle<promise_type> frame;
55+
~suspended_probe() { if (frame) frame.destroy(); }
56+
};
57+
suspended_probe count_resumptions(int* count) {
58+
for (;;) {
59+
++*count;
60+
co_await std::suspend_always{};
61+
}
62+
}
4163
thread_local void (*allocation_hook)() = nullptr;
4264
std::shared_ptr<h::detail::resume_state> state;
4365
std::unique_ptr<h::resume_waiter> waiter;
@@ -106,6 +128,8 @@ LT_BEGIN_AUTO_TEST(resume_lifetime_suite, published_waiter_can_die_during_timeou
106128
static_cast<void>(h::detail::default_timer_queue());
107129
for (int winner : {1, 2}) {
108130
trigger = winner; deliveries = 0; hook_calls = 0;
131+
int resumptions = 0;
132+
auto probe = count_resumptions(&resumptions);
109133
observed = h::resume_outcome::timeout;
110134
state = std::make_shared<h::detail::resume_state>();
111135
waiter = std::make_unique<h::resume_waiter>(state,
@@ -114,15 +138,20 @@ LT_BEGIN_AUTO_TEST(resume_lifetime_suite, published_waiter_can_die_during_timeou
114138
auto* previous = h::detail::current_executor_slot();
115139
h::detail::current_executor_slot() = &target;
116140
allocation_hook = reclaim_on_timer_allocation;
117-
auto continuation = waiter->await_suspend(std::noop_coroutine());
141+
auto continuation = waiter->await_suspend(probe.frame);
118142
h::detail::current_executor_slot() = previous;
119143
allocation_hook = nullptr;
120-
LT_CHECK(continuation == std::noop_coroutine());
144+
LT_CHECK_EQ(resumptions, 1);
145+
LT_ASSERT(continuation);
146+
continuation.resume();
147+
LT_CHECK_EQ(resumptions, 1);
121148
LT_CHECK_EQ(hook_calls, 1);
122149
LT_CHECK_EQ(deliveries, 1);
123150
LT_CHECK(observed == (winner == 1 ? h::resume_outcome::resumed : h::resume_outcome::cancelled));
124151
LT_CHECK(!waiter);
125152
LT_CHECK(!state->head);
153+
probe.frame.destroy();
154+
probe.frame = {};
126155
}
127156
#endif
128157
LT_END_AUTO_TEST(published_waiter_can_die_during_timeout_registration)

0 commit comments

Comments
 (0)