[BUG] Socket fixture startup hides worker failures and skips partial cleanup #42

Open
opened 2026-09-13 04:26:41 +02:00 by erikinkinen · 0 comments
Owner

Summary

The host FakeGuest fixture does not unwind partial startup and can replace an earlier worker failure with a generic readiness timeout. Callers start the fixture before entering their cleanup blocks, so a failed start skips their join() calls. This affects fixture diagnostics and thread ownership, independently of the serial write-timeout defect in #41.

Reproduction

The retained local validation invoked python3 -B -Werror tests/interactive_shell_transport_test.py on a frozen test-only candidate based on Integration 9f004461b284493b7b53d386c8727435b02abe0f. Four serial cases reported AssertionError: fake COM1 socket did not become ready; the other three cases passed. The file ran seven tests in 5.438 seconds and exited 1.

No new reproduction was run for this report. The original published startup implementation has identical control flow to the observed candidate, now published at fe8c8103719af9c127dd769f4427d2a346e1ffd9. A longer temporary directory is a source-supported explanation for a socket setup failure, but the actual worker exception was not retained, so that explanation is not established.

Expected and actual behavior

A startup failure should preserve the first worker or thread-start error, stop and account for every successfully started worker, and retain cleanup failures separately. A readiness timeout should be reported when no earlier startup failure is available.

Currently guarded() stores worker exceptions and sets the stop event, while start() waits for readiness and raises a new generic assertion without consulting those exceptions or unwinding owned workers. Each of the three transport call sites calls start() before its try/finally. The observed failures therefore bypassed the caller's join() path. The retained evidence does not establish a surviving thread or an unclosed socket; the enclosing command owner reported no cleanup failure.

Environment and identity

  • Component: Integration, published base 9f004461b284493b7b53d386c8727435b02abe0f.
  • Original tests/interactive_shell_transport_test.py SHA-256: 7828ead8ae37e1ae231fa81d3f4715968d96eb09e97eca0cd8564f5abdbab811.
  • Observed frozen candidate file SHA-256: 8b20cb4bc36fcdc4e7a00fbf8822c2cec176fe77a6b9ff08b3ccc03e1a1e1c8c.
  • Host Python fixture, local AF_UNIX sockets and daemon threads; no guest image, firmware, compiler or real VM was used in this observation.
  • Per-command owner: 90-second hard bound, 30-second output-stall bound and 4-MiB output bound. Existing fixture readiness waits are one second; the candidate's join budget is two seconds total.
  • Last known working revision for failure unwinding is not established.

Evidence and prior checks

The four failing cases were test_com1_second_command_stall_is_not_masked, test_com1_three_lines_and_three_new_prompts, test_factory_failure_still_quits_private_vm, and test_sequence_factory_reuses_qmp_without_physical_events_on_com1. Each retained traceback stops at the readiness assertion; none exposes the worker's original error.

Original source anchors are guarded() at lines 137–142, start() at 144–157, and starts before cleanup at 173, 200 and 216. The candidate preserves that startup control flow. The nine preceding socket-I/O controls passed; their scope is established connections and write/stop behavior, not partial-start unwinding.

Future regression coverage should exercise an explicit worker failure before readiness, QMP startup failure after serial readiness, a later Thread.start() failure after one successful start, and readiness timeout without a recorded earlier failure. Each case must verify all actually started workers are accounted for and cleanup cannot replace the primary error. A successful startup control must remain. The existing five-second listener accept() wait also needs explicit retirement ownership; merely moving start() inside caller finally does not make a two-second join sufficient.

The signed correction is 37299e719d7662698593c6d3be24752309bed7ec. Startup is single-use and reserves every attempted worker before calling Thread.start. Worker, launch and initial-output failures use one ordered error record; every startup exception requests stop and attempts all worker joins before preserving the primary failure. The powerbox/editor listeners use the same stoppable accept helper. A separate startup commit gate holds early clients until initial output and the final failure check complete. One original absolute five- or ten-second allowance covers both gate waiting and acceptance. Existing connected QMP protocol deadlines remain separate; incomplete cleanup is still an error.

All 122 cases across 15 affected host suites pass, including 20 startup controls. Actual early connections, partial launches, failed initial output, listener retirement and stop are covered. The first candidate's runtime-errors failure remains retained: an independent runner may connect as soon as the path appears, before the completion hook. The corrected gate passes both runtime-errors transports and the complete affected suite. All 116 Markdown files, 71 workflow shell bodies, source contracts and whitespace checks pass. The new checkpoint's CI remains pending; no production observer limit or command assertion changed.

Tracking

Related but distinct: Integration #41, which concerns established fake-guest serial writes inheriting the receive polling timeout. This report concerns startup failure attribution and partial-start cleanup. No runtime defect, exploit, shared root cause or guest acceptance claim is made.

## Summary The host `FakeGuest` fixture does not unwind partial startup and can replace an earlier worker failure with a generic readiness timeout. Callers start the fixture before entering their cleanup blocks, so a failed start skips their `join()` calls. This affects fixture diagnostics and thread ownership, independently of the serial write-timeout defect in #41. ## Reproduction The retained local validation invoked `python3 -B -Werror tests/interactive_shell_transport_test.py` on a frozen test-only candidate based on Integration `9f004461b284493b7b53d386c8727435b02abe0f`. Four serial cases reported `AssertionError: fake COM1 socket did not become ready`; the other three cases passed. The file ran seven tests in 5.438 seconds and exited 1. No new reproduction was run for this report. The [original published startup implementation](https://git.erikinkinen.fi/erix/integration/src/commit/9f004461b284493b7b53d386c8727435b02abe0f/tests/interactive_shell_transport_test.py#L144) has identical control flow to the observed candidate, now published at `fe8c8103719af9c127dd769f4427d2a346e1ffd9`. A longer temporary directory is a source-supported explanation for a socket setup failure, but the actual worker exception was not retained, so that explanation is not established. ## Expected and actual behavior A startup failure should preserve the first worker or thread-start error, stop and account for every successfully started worker, and retain cleanup failures separately. A readiness timeout should be reported when no earlier startup failure is available. Currently `guarded()` stores worker exceptions and sets the stop event, while `start()` waits for readiness and raises a new generic assertion without consulting those exceptions or unwinding owned workers. Each of the three transport call sites calls `start()` before its `try/finally`. The observed failures therefore bypassed the caller's `join()` path. The retained evidence does not establish a surviving thread or an unclosed socket; the enclosing command owner reported no cleanup failure. ## Environment and identity - Component: Integration, published base `9f004461b284493b7b53d386c8727435b02abe0f`. - Original `tests/interactive_shell_transport_test.py` SHA-256: `7828ead8ae37e1ae231fa81d3f4715968d96eb09e97eca0cd8564f5abdbab811`. - Observed frozen candidate file SHA-256: `8b20cb4bc36fcdc4e7a00fbf8822c2cec176fe77a6b9ff08b3ccc03e1a1e1c8c`. - Host Python fixture, local AF_UNIX sockets and daemon threads; no guest image, firmware, compiler or real VM was used in this observation. - Per-command owner: 90-second hard bound, 30-second output-stall bound and 4-MiB output bound. Existing fixture readiness waits are one second; the candidate's join budget is two seconds total. - Last known working revision for failure unwinding is not established. ## Evidence and prior checks The four failing cases were `test_com1_second_command_stall_is_not_masked`, `test_com1_three_lines_and_three_new_prompts`, `test_factory_failure_still_quits_private_vm`, and `test_sequence_factory_reuses_qmp_without_physical_events_on_com1`. Each retained traceback stops at the readiness assertion; none exposes the worker's original error. Original source anchors are `guarded()` at lines 137–142, `start()` at 144–157, and starts before cleanup at 173, 200 and 216. The candidate preserves that startup control flow. The nine preceding socket-I/O controls passed; their scope is established connections and write/stop behavior, not partial-start unwinding. Future regression coverage should exercise an explicit worker failure before readiness, QMP startup failure after serial readiness, a later `Thread.start()` failure after one successful start, and readiness timeout without a recorded earlier failure. Each case must verify all actually started workers are accounted for and cleanup cannot replace the primary error. A successful startup control must remain. The existing five-second listener `accept()` wait also needs explicit retirement ownership; merely moving `start()` inside caller `finally` does not make a two-second join sufficient. The signed correction is `37299e719d7662698593c6d3be24752309bed7ec`. Startup is single-use and reserves every attempted worker before calling Thread.start. Worker, launch and initial-output failures use one ordered error record; every startup exception requests stop and attempts all worker joins before preserving the primary failure. The powerbox/editor listeners use the same stoppable accept helper. A separate startup commit gate holds early clients until initial output and the final failure check complete. One original absolute five- or ten-second allowance covers both gate waiting and acceptance. Existing connected QMP protocol deadlines remain separate; incomplete cleanup is still an error. All 122 cases across 15 affected host suites pass, including 20 startup controls. Actual early connections, partial launches, failed initial output, listener retirement and stop are covered. The first candidate's runtime-errors failure remains retained: an independent runner may connect as soon as the path appears, before the completion hook. The corrected gate passes both runtime-errors transports and the complete affected suite. All 116 Markdown files, 71 workflow shell bodies, source contracts and whitespace checks pass. The new checkpoint's CI remains pending; no production observer limit or command assertion changed. ## Tracking Related but distinct: [Integration #41](https://git.erikinkinen.fi/erix/integration/issues/41), which concerns established fake-guest serial writes inheriting the receive polling timeout. This report concerns startup failure attribution and partial-start cleanup. No runtime defect, exploit, shared root cause or guest acceptance claim is made.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
erix/integration#42
No description provided.