# Group M — measured receipt and containment proposal

hertz, 2026-09-13. One return to doyle under NYOSGVJV (resend of 73SY26E5).

**Decision:** r4 is exercised and NOT execution-ready. M4 exposes real multiline-argument corruption; M6 exposes a false `residual_state=CLEAR`. The containment discriminator is established after instrument repair: a no-breakaway job kills the owned parent and its ordinary child; permitting breakaway lets the child escape and survive job closure. This does not authorize a field run or establish containment of arbitrary operations.

## Evidence roots and scope

All paths below are relative to `.spt/preserved/hertz-fp-driver-review/d2/`.

- **Group output:** `controls-out/20260913T103751Z-M/` (called `G` below).
- **Corrected instrument measurements, inside that same group:** `G/instrument-repair-20260913T104354Z/` (called `C` below).
- Initial group: 10:37:51–10:40:45Z; actual command exit **1**, seven arms, four BAD. `G/results.txt` is retained unchanged.
- Supplemental instrument repair: 10:43:54–10:44:39Z; command exit **0**, M3/M5 and corrected disposition completed. This did not rerun M0/M2/M4/M6 or change their findings.
- Both originally staged PowerShell instruments parsed before editing. All revised PowerShell instruments and the Bash/Python harnesses also parsed before execution. `controls/group-m-parse.txt` and `G/instrument-correction-parse.txt` record the checks.
- The real driver functions were extracted verbatim from pinned r4 by the existing `controls/harness.sh`. Each arm preserves its extracted functions and hash. No substitute implementation of `bounded`, `register_launched`, `measure_termination`, `capture_run_identities`, or `d2_ledger` was used.
- The real launcher was copied byte-for-byte into each arm's isolated `fp-bin` fixture and hash-checked. These fixture copies are not provisioning of the delivered bundle.
- All subjects were short, self-exiting throwaways. Job closure targeted only jobs to which the probe assigned its own parent. No process-name kill, descendant sweep, field binary, real daemon home, firewall, signing, apply, build, or target-pool mutation occurred.
- L0/L1 were not repeated. Their accepted result and the historical unrecorded-subject gap remain unchanged.

## Arm results

| Arm | Result | Observable evidence |
|---|---|---|
| M0 | **PASS as negative control: assertion exit 1** | Real launcher subject 49652 exited 0. Positive checks established `SUBJECT_STATE=GONE` and `TERMINATION=ATTRIBUTION_INCOMPLETE`; asserting `CONFIRMED_GONE` then failed. `G/M0/stdout.txt`. |
| M2 | **PASS, exit 0** | Subject 30520 returned chosen native exit **37**, preserved through the retained process object and `bounded`. Register and live self/CIM observations agreed on subject executable, pid and birth (within 1 microsecond). Re-query caught pid reuse: recorded `10:38:59.6202148Z`, replacement `10:39:08.8865700Z`. Whole operation stayed incomplete. `G/M2/stdout.txt`. |
| M4 | **FAIL, exit 1 — driver/launcher boundary defect** | Exported `SPT_D2_HOME` reached the self-observing direct pwsh subject verbatim and was unset afterward. The additional exact `capture_run_identities` call then failed with `ParserError`, native exit 1. Its intended pwsh executable was still correctly registered and subsequently measured gone. `G/M4/stdout.txt` and `G/M4/actual-capture/`. |
| M6 | **FAIL, exit 1 — ledger defect** | Real ledger given M0's real partial-coverage register, a gone direct subject and absent-listener fixture emitted `residual_state=CLEAR`. This is a controlled ledger-state exercise, not a field residue observation. `G/M6/run/d2-ledger.txt` and `G/M6/stdout.txt`. |
| M3 initial | **INVALID for kill-on-close efficacy** | Both parent and child membership read true, yet both survived closure. Initial probe logged requested flags but never read them back. Its nested value-type assignment did not actually set the flag; see instrument repair below. Initial failure retained at `G/M3/`. |
| M3 corrected | **PASS** | Kernel limit readback `0x2000`; parent 46996 and child 12136 both anchored, both members, both live immediately before close; successful close; both re-queried **GONE**. `C/M3/check.stdout`. |
| M5 initial | **INVALID for breakaway behavior** | Both child launches failed with error **123**, not a breakaway-policy denial. No child was created. `G/M5/` retained. |
| M5 corrected, denied | **PASS** | Limit readback `0x2000`; parent 55144 measured in job; `CREATE_BREAKAWAY_FROM_JOB` returned **ERROR_ACCESS_DENIED (5)**. Child `NOT_CREATED`, not "gone". Parent gone after close. `C/M5-denied/check.stdout`. |
| M5 corrected, allowed | **PASS; escape demonstrated** | Limit readback `0x2800`; parent 57536 in job; child 43892 created successfully and measured **outside** that job. Parent gone after close; same anchored child **SURVIVING** (`delta_ticks=2`). `C/M5-allowed/check.stdout`. |
| M8 initial | **INVALID — false green** | JSON date coercion generated a roughly seven-hour delta even though printed birth timestamps matched within a microsecond; four live rows were mislabelled reused. Initial exit 0 is NOT accepted as disposition evidence. `G/M8/` retained. |
| M8 corrected | **PASS, exit 0** | All **13 distinct registered subject/parent/child identities**, original and corrected attempts, queried by pid plus birth. Six passes; final readings 10:44:37.453–10:44:39.228Z: **13 GONE, 0 surviving, 0 unreadable**. Escaped child was observed surviving earlier, then gone after its self-exit. `C/all-identities.json`, `C/all-identities-disposition.jsonl`, `C/final-disposition.stdout`. |

M8 covers exactly those 13 registered identities. It does not claim a host-wide process census or coverage of an unrecorded operation.

## The two r4 defects remain unfixed

### M4 — argument boundaries are destroyed before PowerShell parses the actual command

The exact call in `capture_run_identities` supplies `pwsh -NoProfile -Command` followed by one multiline script argument. `bounded` writes arguments one per line; `launch_bounded.ps1` reads one array entry per line and hands that array to `Start-Process -ArgumentList`, which joins it into a command-line string. This protocol cannot distinguish argument boundaries from newlines inside an argument and does not preserve the script's quoting.

Measured launcher record: **`argc=25`**, rather than the call's three intended pwsh arguments. Native stderr:

```
$h = $env:SPT_D2_HOME $pidFile = Join-Path $h daemon.pid if (-not (Te ...
                       ~~~~~~~~
Unexpected token '$pidFile' in expression or statement.
```

This directly refutes r4's comment that no call site contains a newline. Environment inheritance itself passed; it is not the fix target. The empty throwaway home contained no `daemon.pid`; the command failed during parsing, before it could take that no-pid-file branch or enumerate any process table.

**Proposed correction, not applied:** serialize an actual argument vector (e.g. a JSON string array), not lines. Preserve every string including empty arguments, spaces, quotes and newlines through native argument encoding. A managed `ProcessStartInfo.ArgumentList` is appropriate for the existing unsuspended path; a future suspended native launcher must provide equivalent correct Windows command-line encoding. Do not special-case this one script or add separators to mask the transport defect. Keep the exact M4 call as the regression discriminator.

### M6 — the ledger does not consume operation coverage

At `d2_ledger`'s residual decision, the real code checks residue count and listener state. It does not inspect the launched-operation coverage record or `TERMINATION`. Thus `RESIDUE_COUNT=0`, `LISTENER_STATE=ABSENT` wins `CLEAR` even with a real `coverage=PARTIAL` register and `TERMINATION=ATTRIBUTION_INCOMPLETE`.

The output is internally contradictory: it prints `residual_state=CLEAR`, then says the identity capture is direct-subject-only and that incomplete capture cannot support CLEAR.

**Proposed correction, not applied:** known live residue stays LIVE; otherwise missing/incomplete operation coverage forces UNREADABLE, regardless of a gone direct subject or an empty listener reading. CLEAR requires all applicable coverage and disposition predicates, not just the visible-subset counts. Missing records must not become an empty launch set by default. Keep direct-subject state distinct from whole-operation state.

## Instrument corrections and limits of the original results

Original instruments are frozen under `G/instruments-as-run/`. Corrected instruments are frozen under `C/instruments/`. `G/instrument-corrections.diff` records the three post-run repairs.

Before the first run, the parent gained a cooperative assignment gate: it creates no child until the probe successfully assigns the parent and reads its job membership. This removes that race from these cooperative throwaways only; it is **not** a production containment design. Native API return values and child identity were checked; disposition comparisons retained microsecond precision rather than dropping the fractional second.

The first run then exposed three further defects in my instruments:

1. **Nested struct assignment was a write to a copy.** `$info.BasicLimitInformation.LimitFlags = 0x2000` left the actual nested value at zero. The offline discriminator in `C/results.txt` reads `nested_assignment_flags=0`; assigning a modified whole nested value back reads `whole_value_assignment_flags=8192`. Corrected probe additionally calls `QueryInformationJobObject` and refuses unless actual kernel flags equal requested flags. Initial `set_ok=True` alone was never proof of the desired limit.
2. **PowerShell `$null` did not reach the native string parameter as NULL.** The offline discriminator reads `powershell_null_to_native_string_is_null=False`. Initial `CreateProcess` calls returned 123, an invalid-name failure that says nothing about breakaway policy. Corrected C# helper passes the executable explicitly, a mutable `StringBuilder` command buffer and C# null for the optional current directory. The same request then yielded meaningful outcomes: denial 5 without the flag, successful escape with it.
3. **JSON conversion changed the date's runtime type.** `ConvertFrom-Json` materialized the birth as `DateTime`; passing that value to `DateTime.Parse(string)` implicitly formatted it first, losing kind/precision and reinterpreting local time. Corrected reader branches on the value's actual type, preserving UTC. Its live-self roundtrip control measured **9 ticks** difference; deliberately skewing that anchor by 500 ms measured **5,000,009 ticks**, which must not match. This repairs the instrument's own false-green mechanism rather than merely changing the expected result.

The initial M3/M5 failures and M8 false green are retained, explicitly invalidated for the claims above, and never replaced with the later outputs. The corrected read-only pass over the original eight identities also completed before the supplemental job launches.

## Containment proposal for doyle's single ruling — NOT implemented

**What M3/M5 establish on this machine:** job membership propagates for the measured normal child; actual `KILL_ON_JOB_CLOSE` stops those members. A daemon-style explicit breakaway request cannot escape the measured job when breakaway is prohibited, but can escape when `BREAKAWAY_OK` is enabled. This tested the process-creation mechanism, not a real spt daemon or any elevated/service-mediated spawn.

### Required launch and termination contract

1. Create a private job whose handle is not inheritable. Set **KILL_ON_JOB_CLOSE only**; prohibit BREAKAWAY_OK and SILENT_BREAKAWAY_OK. Read limits back and require exact enforcement before proceeding. Interop errors are refusal, not a fallback to an ordinary launch.
2. Create the direct subject **suspended**, retain its process and primary-thread handles, and obtain its identity from those handles. Assign it to the job and read membership **before ResumeThread**. Production cannot rely on the cooperative file gate used by the throwaway parent. On setup failure, never resume; dispose only of the created suspended process through its retained handle and report confirmation or failure separately.
3. Preserve native argument and environment boundaries, stdout/stderr separation, direct-subject identity and native exit. Start deadline accounting before setup; include creation, assignment, waits and confirmation. An exit code such as 124 must not by itself distinguish a subject's ordinary exit from an expired bound: persist a separate completion reason.
4. For normal controlled shutdown/expiry, retain the job handle, request `TerminateJobObject`, then query bounded job accounting until **ActiveProcesses=0**. Report the direct subject's own exit/disposition separately. Release child process handles once their observations are recorded, since outstanding process references can affect accounting. Close the job handle after confirmation; kill-on-close is also the crash/abnormal-unwind safety net. A successful termination call or CloseHandle is a request result, not a substitute for confirmation.
5. No PID-tree kill and no descendant enumeration as an authority source. The only termination authority is the retained job over its members, or the retained direct-process handle during failed suspended setup. Failure to read job state remains UNREADABLE/nonzero, never CLEAR.
6. **Coverage is scoped, not universal.** COMPLETE may describe the kernel-enforced process tree only for an admitted operation known to remain in that creation chain. Jobs are not a security sandbox or a proof over service/WMI/RPC/elevation delegation. Arbitrary commands and operations that may enlist an external service remain PARTIAL or are refused for the complete-coverage mode. No descendant scan may promote coverage. Do not enable breakaway to make a daemon launch succeed and still claim containment.
7. Integrate the coverage predicate into the actual ledger (M6), not only a launcher record or prose. An escaped, unreadable or unconfirmed operation cannot produce CLEAR. A gone direct subject cannot repair incomplete whole-operation coverage.

**Requested next authorized controls, after the two r4 fixes and native boundary are reviewed:** suspended-before-assignment with an immediate-spawn subject; assignment failure that never resumes; ordinary child and grandchild; explicit breakaway denial; native exit versus bound expiry; a child outliving the direct subject; launcher abnormal exit/last-handle closure; job-accounting confirmation failure; exact multiline M4; and M6's partial-coverage refusal. All remain throwaway-owned controls. No field, signing, provisioning or apply permission is requested here.

### Why this is deliberately narrower than “job object means COMPLETE”

Microsoft documents that ordinary CreateProcess children inherit membership but `Win32_Process.Create` children do not, and describes breakaway and nested-job effects. M5 measured one escape mechanism, not every possible mechanism. The repo already records the daemon's intentional breakaway/fallback behavior in `docs/KNOWN-HAZARDS.md` §7.10. A future field operation's custody must be established at that actual boundary; it cannot be inferred from these pwsh throwaways.

Primary API references:
- [Job Objects: child membership, breakaway, kill-on-close and delegation caveat](https://learn.microsoft.com/en-us/windows/win32/procthread/job-objects)
- [Process Creation Flags: CREATE_SUSPENDED and CREATE_BREAKAWAY_FROM_JOB](https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags)
- [Job accounting: ActiveProcesses and outstanding process references](https://learn.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-jobobject_basic_accounting_information)

## Pins, preservation and verification

| Instrument | SHA-256 |
|---|---|
| r4 driver, unchanged | `5fb75766eba3a89d91578b27742498f80d1b20d26206791b5d55f141a36dc78a` |
| real launcher, unchanged | `c0050c3e245fa9c17096935496af3515f12359ca50c0a76647e717999be4ea76` |
| corrected job probe | `70d15951f89970758e26d8d7b87d28fc61e8781a96cf84d28a3f040fc9efbfac` |
| corrected job parent | `a100c06ba92229b9bc7c173b97761f6338f22dd7e89964f5baed9818cd71f2a6` |
| corrected disposition reader | `0daf7b16cab961cc7b2a7e7b955327011a9c6ca52338fc1782346beb015283c6` |
| observation checker | `fd6d849b8e21885d0b651ee51acf2ef9f6fe253b7b90a9cad1861f0fd0208b9f` |
| supplemental driver | `7b240c0a3ba8138ecb19a59bfd0567f4fba9455fe211e40c38288192a6bd13ce` |

`G/INSTRUMENTS.sha256` records the initial pins; `C/INSTRUMENTS.sha256` records the corrections. The complete evidence tree is covered by `G/ARCHIVE.sha256`; this document has its separate sibling SHA-256 receipt.

Repository verification: installed `traceable-reqs 0.4.1`; `traceable-reqs check --json` completed successfully: **903 requirements, 903 complete, 0 incomplete, 0 findings**. This is repository evidence verification, not acceptance of the experimental driver. No production source, requirement registry, shipped documentation or permanent test was changed; these diagnostic artifacts remain in the preserved scratch tree. No old artifact or failed attempt was deleted.

**Return:** accept M0/M2 and the corrected M3/M5/M8 measurements; retain M4/M6 as blockers. Rule the containment/termination controls from the concrete proposal above. r4 and the launcher remain unchanged; no termination authority over pre-existing processes, field run, signing, provisioning or apply is exercised or implied.
