fix(marmot): handle being removed mid-commit without OOM (test 14)
When wn admin-removes A in interop test 14, A processed the proposal
locally — `tree.removeLeaf(A.leafIndex)` blanked her leaf and shrank
`tree.leafCount` past `myLeafIndex` — and then immediately walked into
BinaryTree.directPath(myLeafIndex, tree.leafCount)
at MlsGroup.processCommitInner. With `myLeafIndex >= leafCount`, the
left-balanced parent walk had no valid stopping point: each recursion
step doubled the candidate parent index, integer-overflowed past 2^31,
and either returned garbage or kept appending to the result list until
the JVM OOM'd. The OOM bubbled up as a `runBlocking` failure that
rolled back the whole commit — so A also stayed locally convinced she
was still a member, and the test timed out waiting for `not_member`.
Three layered fixes so the failure mode can't recur:
* `BinaryTree.parent`/`directPath`/`copath` now `require` an in-range
input. The root and any node ≥ nodeCount used to silently loop or
return garbage; now they throw `IllegalArgumentException` immediately.
This is defense-in-depth — a future caller passing an invalid index
gets a stack trace at the boundary instead of an OOM five frames
deep.
* `MlsGroup.processCommitInner` short-circuits the path-decrypt + epoch
advancement when the proposals in the commit just removed *us*. We
preserve the proposal-side tree mutations so the caller (and the
outer state machine) can observe that we're out, clear pending
proposals + sent keys, and return. Without this short-circuit the
function would derive a bogus all-zero `commit_secret`, fail
`confirmation_tag` verification, throw, and roll the snapshot back —
leaving us still convinced we were a live member.
* `MlsGroupManager.isMember` and a new `MlsGroup.isLocalMember()`
helper now return false once our leaf is null or past `leafCount`.
The post-Remove group entry still lives in `groups` so callers can
inspect the final tree, but every cli command (`group show`,
`message send`, etc.) sees `not_member` and returns the right error.
Also add a comprehensive `BinaryTreeTest` covering non-power-of-2
trees (3, 5, …, 32 leaves) and the boundary cases (root has no
parent, leafIndex ≥ leafCount must throw). The pre-existing tests
only exercised the 4-leaf example from RFC 9420 Appendix C; nothing
hit the `parentInRange` branch, which is exactly where the OOM lived.
Test 14's bash polling needed a small companion fix: it captured
amy's stderr through `2>&1` and fed the result to `jq`, but the
captured stream is interleaved with quartz `Log.d(…)` debug lines,
so the JSON parse always failed. Switch to a `grep` for the
`"error":"not_member"` literal — that signature only appears in
the JSON payload and survives the debug noise. Also tee the
captured output into `$LOG_FILE` so post-mortem logs include each
polling iteration's `[cli] ingest …` traces.
Marmot interop score: 11/16 → 14/16 (tests 9, 15 are unrelated MLS
issues — see follow-up).
https://claude.ai/code/session_013VYkpz8P1mPh9Ejxy9anhJ
This commit is contained in:
@@ -240,12 +240,24 @@ test_14_wn_removes_a() {
|
||||
|
||||
# Amy should observe that she's no longer a member. `group show` returns
|
||||
# a not_member error as soon as the Remove commit is applied locally.
|
||||
#
|
||||
# The amy CLI contract puts error JSON on stderr (README: "stdout is
|
||||
# JSON … stderr is for humans"), but stderr is interleaved with debug
|
||||
# logs from `Log.d(…)` calls in quartz, so feeding the captured stream
|
||||
# straight into `jq` fails to parse. Capture stderr alongside stdout
|
||||
# via `2>&1` and grep for the `"error":"not_member"` literal — that
|
||||
# signature is unambiguous (the debug log lines never produce JSON).
|
||||
# Also tee the per-iteration capture into $LOG_FILE so post-mortem
|
||||
# logs include each polling sync's `[cli] ingest …` trace, mirroring
|
||||
# what amy_json normally writes when stderr isn't being captured.
|
||||
local deadline=$(( $(date +%s) + 120 )) removed=0
|
||||
while [[ $(date +%s) -lt $deadline ]]; do
|
||||
local show rc
|
||||
show=$(amy_json marmot group show "$a_gid" 2>&1)
|
||||
show=$("$AMY_BIN" --data-dir "$A_DIR" --secret-backend plaintext \
|
||||
marmot group show "$a_gid" 2>&1)
|
||||
rc=$?
|
||||
if [[ $rc -ne 0 ]] && printf '%s' "$show" | jq -e '.error == "not_member"' >/dev/null 2>&1; then
|
||||
printf '%s\n' "$show" >>"$LOG_FILE"
|
||||
if [[ $rc -ne 0 ]] && printf '%s' "$show" | grep -qE '"error":[[:space:]]*"not_member"'; then
|
||||
removed=1; break
|
||||
fi
|
||||
sleep 3
|
||||
|
||||
Reference in New Issue
Block a user