ReviewOS

zig-utils/zig-js

Threads: maintain the shared-realm JavaScript memory model

#12
Closed chris opened this 9 days ago ·
9 days ago

Parent: #1

Goal

Keep the public contract for JavaScript-defined races, engine-state races, synchronization, and GC publication precise as the runtime evolves.

Current baseline

docs/threads/memory-model.md now defines:

  • Lock/Atomics.Mutex release-to-acquire publication.
  • Condition wait release/reacquire semantics, predicate-loop requirements, non-buffered notifications, and the fact that notify alone is not a publication substitute.
  • Sequentially consistent typed-array Atomics and per-(object, key) property-Atomics linearization/publication.
  • Thread join/asyncJoin completion, result/exception identity, and prior-side-effect publication.
  • Per-queue microtask FIFO and synchronized cross-thread queue handoff without a promised global order across independent producers.
  • The boundary between ordinary racing values, explicit ConcurrentAccessError restriction, and engine-state corruption.
  • GC reachability publication as an internal safety mechanism that does not create JavaScript happens-before.

ThreadSanitizer currently runs without a suppression file. Intentional JavaScript shared-buffer byte races are either synchronized in the engine paths or must be handled by a future narrow, load-bearing suppression witness; engine metadata, synchronization, GC, root, lifetime, and handle state remain unsuppressed. Recent maintenance also hardened engine-owned lifetime/handle state: SharedBufferStorage retain counts and C-API protected-handle counts use checked increments, and C value/object boxes carry owner-context tags so context-taking APIs fail closed on wrong-realm handles.

Completed milestone

  • Specify ordering guarantees for Lock, Condition, property Atomics, typed-array Atomics, Thread join/asyncJoin, and task/microtask delivery.
  • Document which ordinary object races have unspecified values versus explicit ConcurrentAccessError behavior.
  • Align GC write-barrier/publication rules with the landed nursery and concurrent full collector.
  • Add litmus tests for the documented happens-before guarantees.
  • Audit the memory-model docs and suppression boundary for the nursery/shared-state changes.
  • Remove stale TSan suppression wiring and keep required gates running suppression-free.
  • Harden shared-buffer and C-API handle lifetime accounting against refcount/protection-count wraparound and wrong-context handle use.

Ongoing maintenance

  • Re-run the focused memory model: normal and TSan tests whenever synchronization or queue handoff changes.
  • Add a minimized litmus test before strengthening any public ordering claim.
  • Keep GC publication explicitly separate from program synchronization.
  • Keep test-only controls excluded from the stable embedder API.
  • Re-audit the no-suppression ThreadSanitizer policy whenever a new raw program-byte access helper is introduced. If a future suppression becomes necessary, land it with a deterministic witness that fails without the suppression and passes with it.
  • Keep owner-context and lifetime/refcount invariants regression-gated whenever public C handles or shared backing storage change.

Acceptance criteria

  • Public guarantees have executable witnesses.
  • TSan runs remain suppression-free, or any future suppression is narrow and load-bearing.
  • Test-only controls remain clearly excluded from the stable embedder API.
9 days ago

Landed 6b215b3 test(threads): specify memory-model publication edges on main.

Public contract added

  • Releasing a Lock / Atomics.Mutex publishes prior ordinary writes to the next successful acquire of that lock.
  • Condition.wait releases and reacquires the associated lock; the reacquire publishes the predicate state. Notifications are not buffered, may be spurious, and are not a substitute for locking the predicate.
  • Typed-array Atomics are sequentially consistent.
  • Property Atomics linearize per addressed (object, key) and publish earlier sequenced writes when the reader observes the atomic publication.
  • join() and asyncJoin() publish completion side effects and preserve returned/thrown value identity, including already-completed joins.
  • Spawned threads drain their own FIFO microtasks before completion publication; cross-thread queue handoff is synchronized, but independent producers have no promised global total order.
  • Ordinary shared-object/collection races remain program races with unspecified winning/intermediate values.
  • ConcurrentAccessError applies only after explicit successful Thread.restrict ownership.
  • GC barriers, remembered owners, root handshakes, and born-cell publication preserve reachability but do not create JavaScript happens-before.

Executable witnesses

Six focused no-GIL unit tests now cover:

  • Lock release + Condition reacquire publication of nested ordinary object writes.
  • Property Atomics flag publication of an ordinary object payload.
  • Typed-array Atomics flag publication of an ordinary object payload.
  • join side effects, return identity, thrown identity, and asyncJoin settlement.
  • Per-thread FIFO microtask drain before completion.

Verification

  • zig build test -Dtest-filter='memory model:'
  • zig build test -Dtsan=true -Dtest-filter='memory model:'
  • zig build test
  • bun run docs:build
  • git diff --check

This slice changes docs/tests only. The full all-tests TSan sweep and clean 227/227 corpus had already passed immediately before it on the same runtime commit; they were not rerun after this docs/test-only commit.

#12 remains open as a maintenance tracker: any future strengthening of the public contract still requires a focused executable witness, and new raw byte-access helpers still require suppression-boundary review.

9 days ago

Memory-model maintenance check after the per-queue microtask lock change in caad91e:

  • Local focused gate passed: zig build test -Dtest-filter="memory model:".
  • This specifically rechecks the documented lock/condition publication, property/typed-array Atomics publication, join/asyncJoin publication, and per-thread microtask FIFO-before-completion witnesses after moving no-GIL microtask synchronization from a realm-wide lock to queue-local locks.
  • The public memory-model text already describes per-queue FIFO plus synchronized cross-thread handoff without a global order across independent producers, so no contract text needed to change.

Fresh CI for the pushed chunk is still running, with early green checks including docs, test262-parallel, threads-nogil-witness, tsan-parallel-js, and threadfuzz-verify: https://github.com/zig-utils/zig-js/actions/runs/29114710319.

9 days ago

Memory-model/lifecycle guard fix landed in zig-js a0375cb (fix(threads): pair backing unlocks with acquisitions).

Root cause from tsan-threadfuzz-lifecycle: Object.lockBacking() was gated by the global element_locks_enabled flag, and unlockBacking() checked the flag again at defer time. In parallel lifecycle setup, that flag can flip between the two checks, so a context could attempt to unlock an unheld std.atomic.Mutex while installing the Thread API.

Fix: lockBacking() now returns whether it actually acquired the lock, and all unlocks use that captured boolean. Added a focused regression: Object backing lock pairs unlock with actual acquisition.

Focused validation also passed the exact local lifecycle TSan command: zig build threadfuzz -Dtsan=true -Dfuzz-lifecycle=true -Dfuzz-iters=2 → 112 programs from seed 1, 0 failures.

CI rerun: https://github.com/zig-utils/zig-js/actions/runs/29116066646

9 days ago

Pushed another small CI hardening chunk: 87c4c8b (fix(threads): enable binding locks before thread builtins).\n\nWhat changed:\n- Replaced the old set/unset parallel synchronization switch with an enable-only process-wide helper.\n- For threaded parallel/concurrent-GC contexts, binding/object/IC synchronization is enabled before Thread/Atomics builtins are installed. That closes the lock/unlock mismatch seen in CI where Environment.get() could take the fast path before another lifecycle thread flipped binding locks on, then unlock a mutex it never acquired.\n- The flags are left enabled process-wide once needed, avoiding false→true→false races across concurrent context startup.\n\nFocused local checks run before push:\n- zig build test -Dtest-filter="Object backing lock pairs unlock with actual acquisition"\n- zig build test -Dtest-filter="enable_threads: Object backing allocator stays synchronized under parallel_js"\n\nI did not rerun the broad/slow fuzz gates locally. The previous main CI run also exposed separate midgc termination-reaction failures; I’m tracking those under the mid-script/lifecycle fuzzing issues rather than mixing them into this lifecycle-lock chunk.

9 days ago

Correction to the previous binding-lock note: 87c4c8b enabled synchronization before threaded builtins, but CI showed a remaining create-time race earlier in setup (). Pushed 641c5e9 to make true from process start, eliminating the false→true lock/unlock mismatch for binding tables during concurrent context creation.\n\nThis is intentionally conservative and compatible with the "perfect tool first" direction: we can later replace the process flag with a per-call acquisition token or owner guard if profiling shows the default binding lock cost matters.\n\nFocused checks: two lock-pair/backing tests passed, and non-TSan passed locally.

9 days ago

Correction to the previous binding-lock note, with shell-safe wording: 87c4c8b enabled synchronization before threaded builtins, but CI showed a remaining create-time race earlier in setup at Context.createWithTestingOptions while reading the $262 global binding. Pushed 641c5e9 to make Environment binding locks true from process start, eliminating the false-to-true lock/unlock mismatch for binding tables during concurrent context creation.

This is intentionally conservative and aligned with the perfect-tool-first direction: we can later replace the process flag with a per-call acquisition token or owner guard if profiling shows the default binding lock cost matters.

Focused checks: two lock-pair/backing tests passed, and non-TSan threadfuzz lifecycle 2 1 passed locally.

9 days ago

Shared-memory-model maintenance update pushed in zig-js@0b29b20 (fix(dataview): serialize resize races):

  • DataView.prototype.get*/set* now validate the live view bounds and read/write through the same backing-byte snapshot while holding ArrayBufferData.lockBuffer when a non-shared resizable buffer can be resized by a peer.
  • Added a shared-realm witness where one Thread repeatedly performs DataView reads/writes while the owner resizes the same resizable ArrayBuffer.
  • Local validation:
    • zig build test -Dtest-filter="DataView access serializes with parallel ArrayBuffer resize" --summary none
    • zig build test -Dtsan=true -Dtest-filter="DataView access serializes with parallel ArrayBuffer resize" --summary none

This is an engine-state race fix; JavaScript-level racy values remain intentionally distinct from engine storage safety.

9 days ago

Additional shared-memory-model safety update pushed in zig-js@227b69a (fix(arraybuffer): lock immutable slice copy):

  • ArrayBuffer.prototype.sliceToImmutable now re-checks the resolved source range and copies from the source through ArrayBufferData.lockBuffer when peer resize/free is possible. This mirrors the existing ArrayBuffer.prototype.slice source-side UAF guard.
  • Added a shared-realm witness where one thread repeatedly calls sliceToImmutable(0, 64) while the owner resizes the same resizable ArrayBuffer.
  • Local validation:
    • zig build test -Dtest-filter="ArrayBuffer sliceToImmutable serializes with parallel resize" --summary none
    • zig build test -Dtsan=true -Dtest-filter="ArrayBuffer sliceToImmutable serializes with parallel resize" --summary none

This is another engine-state race/security hardening fix; it does not strengthen JavaScript program-race ordering claims.

9 days ago

Docs follow-up pushed in zig-js@334ebf0 (docs(threads): document buffer resize guards):

  • Updated the value.ArrayBufferData synchronization audit to include DataView read/write helpers and ArrayBuffer bulk-copy guards (slice / sliceToImmutable) alongside typed-array element helpers.
  • Updated the testing guide to mention the deterministic DataView access and sliceToImmutable resize-race witnesses, not just constructor races.
  • Validation for this docs-only chunk: git diff --check plus stale wording scan for the replaced phrases.
9 days ago

Security-quality/bindings-audit update pushed in zig-js@e962457 (fix(math): seed random from entropy):

  • Math.random() no longer starts every process/thread from one fixed deterministic DefaultPrng seed.
  • The PRNG remains threadlocal, preserving the no-GIL race-free ruling, but is now lazily seeded from the engine std.Io.randomSecure entropy source. If secure entropy is unavailable, it falls back to a thread-id-mixed deterministic seed rather than failing JS execution.
  • Added a focused Math.random unit witness for [0, 1) values and advancing stream behavior.
  • Local validation:
    • zig build test -Dtest-filter="Math.random" --summary none
    • zig build test -Dtest-filter="Math" --summary none
    • zig build test -Dtsan=true -Dtest-filter="Math.random" --summary none

This closes the stale audit note that called out the fixed seed as a future quality pass.

9 days ago

Follow-up pushed in 97fd03d5 (fix(math): harden random entropy fallback).

This keeps the secure entropy path as the normal Math.random seed source, but removes the fixed fallback constant if randomSecure is unavailable: the fallback now mixes the awake timestamp and current thread id before seeding the per-thread PRNG. This is still a fallback, but it avoids silently returning to deterministic streams in unusual environments.

Focused validation:

  • zig build test -Dtest-filter="Math.random" --summary none
  • zig build test -Dtsan=true -Dtest-filter="Math.random" --summary none
  • git diff --check
9 days ago

Memory-model maintenance check after 3eb36f6 (perf(shape): avoid locks on cached transitions).

That change added an append-only release/acquire publication path for cached shape transition hits while leaving transition insertion/miss convergence under the per-shape transition lock. The shape-specific normal and TSan checks already passed before the push; I also reran the focused public memory-model witnesses now that the change is on main:

  • zig build test -Dtest-filter='memory model:' --summary none
  • zig build test -Dtsan=true -Dtest-filter='memory model:' --summary none

Both passed. No public JavaScript happens-before claim changed: this is engine-state publication for immutable shape metadata, not program-state synchronization.

9 days ago

Shared-buffer lifetime hardening landed in 209e2cf (fix(shared-buffer): guard storage retain overflow).

What changed:

  • SharedBufferStorage.retain() now uses a checked atomic increment instead of an unchecked fetch-add.
  • New tryRetain() refuses to wrap the process-wide SAB/ArrayBuffer storage refcount.
  • Internal retain() panics on impossible overflow rather than silently wrapping storage lifetime accounting.
  • Added a focused overflow witness.

This does not strengthen JavaScript program-ordering guarantees; it keeps engine-owned shared backing storage lifetime from entering a wraparound state.

Validation:

  • zig fmt src/shared_buffer.zig
  • zig build test -Dtest-filter='SharedBufferStorage' --summary none
  • zig build test -Dtest-filter='RetainList' --summary none
  • zig build test -Dtest-filter='memory model:' --summary none
  • git diff --check
9 days ago

Maintenance update from c414f19:

The C-API protected-handle table now has a stricter public contract. JSValueProtect / JSValueUnprotect return bool, and JSValueProtect uses a checked counter increment so protection-count overflow is rejected rather than wrapping. The table is still mutated under realm_lock when parallel JS is enabled; docs/threads/bindings.md now calls out the overflow guard alongside the existing deduplication and fixed-capacity-growth behavior.

Validation:

  • zig build test -Dtest-filter='C-API: JSValueProtect' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Maintenance update from a144701:

C-API value/object boxes now carry an owner-context tag while keeping the Value field first for GC root tracing. Context-taking C APIs reject wrong-context JSValueRef / JSObjectRef handles, closing a class of accidental cross-realm arena/object-graph mixing. docs/threads/bindings.md now records both sides of the invariant: protected handle tracing still sees the first-field Value, and public C APIs use the owner tag to fail closed for wrong-realm handles.

Validation:

  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Follow-up from a0efcbc:

The C handle-inspection APIs now use a dedicated helper that preserves debug owner-thread affinity without requiring VM GIL ownership for simple no-exception reads. That keeps the owner-context validation from a144701 compatible with serialized threaded C contexts while still avoiding cross-thread handle use in Debug builds.

Validation:

  • zig build test -Dtest-filter='C-API:' --summary none
  • bun run docs:build
  • git diff --check
9 days ago

Pushed a CI/security hardening chunk in 762617d (ci(tsan): remove stale suppressions).

Root cause from the latest #22 acceptance run: tsan-suppression-witness failed because neither old timing-dependent program-byte case reproduced a raw TSan race, which means the suppression file was no longer demonstrably load-bearing.

What changed:

  • Removed tsan-suppressions.txt and the tools/tsan-suppression-witness.sh job instead of preserving an inert suppression path.
  • The sharded no-GIL TSan corpus now runs with TSAN_OPTIONS=halt_on_error=1 and no suppressions.
  • Memory-model/testing/production docs now state the stricter policy: no TSan suppressions by default; any future JS program-byte false positive needs a deterministic load-bearing witness and must not cover engine state.

Focused validation:

  • Workflow YAML parse
  • bun run docs:build
  • git diff --check
  • Unsuppressed TSan focused cases:
    • cve/mc-prim-arraybuffer-transfer-vs-atomics.js
    • cve/mc-df-ta-detach-resize.js

Keeping #12 open as the memory-model maintenance bucket; the new full CI run from this push is pending.

9 days ago

Small shared-state audit docs hygiene landed in f33df0e7 (docs(threads): remove local path from binding audit). The docs/threads/bindings.md path-dependency section now names the sibling ../zig-regex dependency without embedding a machine-local absolute path. No runtime contract change; this keeps the binding/thread-safety audit docs portable.

Validation:

  • bun run docs:build
  • git diff --check
9 days ago

Landed b01e3a5 (docs(threads): refresh zig-regex binding audit).

The path-dependency audit now reflects the current upstream zig-regex state: zig-regex@5937fa7 made the C-API allocator binding const and removed the stale zig_regex_get_last_error() doc reference. The row remains classified as locked/thread-safe because std.heap.c_allocator is internally synchronized, and zig-js still uses the Zig API rather than linking zig-regex's C API.

Validation:

  • bun run docs:build
  • git diff --check

I did not modify the local zig-regex checkout because it has unrelated dirty package/lockfile edits and is behind origin by that already-landed cleanup commit.

9 days ago

Maintenance audit after the recent CI/suppression cleanup: the current tree has no TSan suppression file or TSAN_OPTIONS suppression wiring. .github/workflows/ci.yml, docs/threads/memory-model.md, docs/threads/testing.md, and docs/threads/production-readiness.md all describe the intended no-suppression policy.

I also searched for stale suppression-boundary text after 762617d and did not find a remaining repo doc/config mismatch. The #12 ongoing rule still stands: if a future raw program-byte false positive needs a suppression, it must land with a deterministic load-bearing witness and must not cover engine metadata, GC, root, lifetime, handle, or synchronization state.

9 days ago

TSan maintenance note: the full gate on 34134b77 caught a real no-suppression race pattern, not a flaky assertion. The fix in e179650 stops allocation-failure recovery from conservatively reading parked peer native stacks while native wait code can still mutate wait-frame / park-record slots. Focused TSan repros are green locally.

9 days ago

Follow-up from CI run 29165826514: tsan-unit reported a race in the serialized/GIL keepalive path for the creator-owned ArrayBuffer survivor test.

Root cause:

  • markThreadExited() writes ThreadRecord.exited under join_mutex.
  • The no-GIL keepalive branch already reads it under that mutex, but the serialized/GIL branch was polling rec.exited directly while waiting on the GIL condition.

Pushed d1f6346 (fix(threads): guard exited checks in gil keepalive) to route those checks through the same locked helper used by thread-quiescence checks.

Focused validation:

  • zig build test -Dtsan=true -Dtest-filter="creator-owned ArrayBuffer storage survives creator exit" --summary none
  • git diff --check
9 days ago

Pushed d0092f5e (docs(threads): sync memory model hardening) as a #12 documentation-maintenance chunk.

What changed:

  • docs/threads/memory-model.md now explicitly classifies resizable ArrayBuffer resize-vs-view/copy races as engine-state memory-safety concerns: typed-array/DataView helpers borrow through the buffer lock and bulk-copy helpers such as slice/sliceToImmutable copy from one locked source snapshot.
  • The same section now says these guards do not make unsynchronized JavaScript resize races deterministic.
  • The C-API rules now record the already-landed lifetime hardening: JSString retain-count overflow rejection, protected-handle count overflow rejection, and owner-context tags on JSValueRef/JSObjectRef boxes so wrong-realm handles fail closed.

Validation:

  • bun run docs:build
  • stale/text consistency grep across memory-model.md, bindings.md, and api.md
  • git diff --check

No runtime ordering claim changed here; this syncs the public memory-model page with the implementation and binding-audit invariants already covered by focused tests.

CI: https://github.com/zig-utils/zig-js/actions/runs/29201642565 is pending for d0092f5e.

9 days ago

Green gate note for d0092f5e: https://github.com/zig-utils/zig-js/actions/runs/29201642565

The memory-model/hardening sync passed the full combined gate, including suppression-free TSan legs. This keeps the documented no-suppression boundary current for the shared-buffer retain-count, protected C-handle count, owner-context handle, and race-contract documentation changes.

9 days ago

Green gate note for c6856a28: https://github.com/zig-utils/zig-js/actions/runs/29203285907

Landed root-publication hardening so RootHandshake.open() never publishes cycle id 0, which remains the idle/no-request sentinel, even at counter wrap. Focused local check: zig test src/root_handshake.zig passed all 3 tests, including the new wrap guard. The full CI gate then passed.

9 days ago

Green gate note for c92bbc01: https://github.com/zig-utils/zig-js/actions/runs/29204523233

Landed $262.agent broadcast-generation hardening: broadcast generations now skip the idle 0 sentinel, and rendezvous accounting checks whether each live agent acked the exact current generation instead of relying on monotonic < ordering. Focused checks: zig test src/agent.zig and zig build test -Dtest-filter=agent; full CI then passed.

9 days ago

Progress update: pushed 5a9217d44e85b5e247e947d1f50f74d8b52e41b2 (fix(agent): keep waitAsync ticket ids live-unique).

This tightens the shared-realm Atomics.waitAsync bookkeeping by minting async waiter ticket IDs while holding the waiter mutex, skipping zero, and skipping any ID that is still live. That keeps promise-root settlement routing unambiguous even after the u64 counter wraps.

Validation:

9 days ago

Maintenance hardening landed in e9df0649 (gc: harden parallel publication generations).

This is internal engine-state publication, not a new JavaScript happens-before claim: mid-script parallel-GC root-publication generations now reserve 0 exclusively as the idle sentinel, use a separate non-zero generation source, and compare exact generation equality. That closes the stale-high-generation / wrap-to-zero class for the no-GIL root-publication protocol.

Focused coverage added: parallel_js (M3): publication generations skip idle zero at wrap.

Validation included normal + TSan focused tests, a focused mid-GC fuzzer seed (45 programs, 0 failures), docs build, and diff hygiene. New combined-head CI is pending at https://github.com/zig-utils/zig-js/actions/runs/29216356732

9 days ago

Pushed bbf0125e (runtime: harden shared identity serials) as a memory-model/security maintenance slice.

Root cause:

  • Context.private_name_serial was a plain u64; parallel shared-realm class evaluation could race its += 1 and mint the same internal storage key for unrelated private names.
  • symbol_counter used atomic fetchAdd, but exhaustion wrapped into previously issued Symbol property keys.

Fix:

  • Per-context private-name serials are now atomic.
  • Symbol and private-name identities use shared checked CAS minting; standalone interpreter serials use checked local addition.
  • Exhaustion fails closed as OOM instead of reusing an existing identity.
  • Added a real eight-thread class-evaluation witness that cross-checks every private brand against every foreign instance, plus deterministic wrap and four-thread serial uniqueness tests.
  • Updated the binding audit and public memory-model engine-state contract.

Batched validation:

  • identity serial tests → 3 passed
  • class-private unit set → 4 passed
  • semantics/private-fields-shared.js
  • semantics/symbol-registry-cross-thread.js
  • focused parallel-class witness under normal and suppression-free TSan
  • bun run docs:build → 21 pages
  • git diff --check

Combined CI gate: https://github.com/zig-utils/zig-js/actions/runs/29219431086

9 days ago

Security maintenance follow-up pushed in 285b534c: structured-clone Worker bytes no longer embed raw SharedBufferStorage pointers. SAB transfer now uses a locked registry of opaque random single-use tokens; forged/replayed/trailing payloads fail closed, concurrent consumption is single-winner, and the focused token race passes without TSan suppressions. Tracking acceptance and CI on #38: /zig-utils/zig-js/issues/38.

9 days ago

Memory-model maintenance: #41 / 6ef776c fixed a real cross-thread async-function activation handoff race found by the sharded TSan corpus. Generator.running is now an atomic release/acquire resume-ownership word for async promise jobs, while ordinary generator reentrancy remains guarded by resume_mutex. The deterministic handoff witness and full suppression-free replacement gate are green: https://github.com/zig-utils/zig-js/actions/runs/29226671310

9 days ago

Closing this tracker as completed.

The public memory-model contract is documented and backed by focused executable witnesses for Lock/Condition, typed-array and property Atomics, join/asyncJoin, task/microtask handoff, ordinary race behavior, and GC publication boundaries. The TSan matrix remains suppression-free, test-only controls remain outside the stable embedder API, and the later async-resume handoff hardening in #41 preserved the same acquire/release publication model.

Combined acceptance gate: https://github.com/zig-utils/zig-js/actions/runs/29226671310 — 31 success, 1 expected nightly-only skip, 0 failures.

The items left in the body are maintenance rules, not unfinished implementation. Future changes that strengthen ordering, add a raw program-byte access path, or alter public handle/lifetime state should open a narrow issue with its own minimized litmus witness.

Sign in to comment on this issue.