# Java TCP write mode ownership review

The optional pure-Java `stream-toggle` write path could deadlock an accepted
connection when called outside its read-owner thread. The correction confines
blocking-mode changes to that owner and routes foreign callers through the
existing bounded nonblocking writer. The ordinary nonblocking configuration is
unchanged; no reader lock or per-read allocation was added.

## Reproduction

The existing eight-case `PureJavaLogonRecoveryWriteFailureTest` passed here
against the unchanged implementation. That does not disprove the hang another
session reported: its occurrence depends on the reader entering a read during
the brief blocking-mode window.

A controlled regression wraps only the accepted socket's output stream. After
a foreign writer sends its frame, the wrapper waits for the real reader to
enter `SocketChannel.read` while the channel is temporarily blocking and the
peer deliberately sends no more data. Both the dedicated-worker and
multiplexed acceptor cases then fail their three-second completion assertion.
The baseline's other two blocking-toggle cases pass; this run does not claim
to have dynamically reproduced that related path's race.

The captured stacks verify the cycle: the writer is parked in
`SocketChannelImpl.implConfigureBlocking`, called from the stream writer's
restoration step, while the reader is blocked in `SocketDispatcher.read0`.
Closing the fixture's peer releases the reader and allows cleanup. The test
does not abandon its TCP workers on assertion failure.

## Correction and coverage

Accepted clients already record their read-owner thread before registration.
Only that thread may now use the stream or blocking-toggle diagnostic path.
Foreign sends retain their existing write serialization but use the normal
nonblocking channel writer, including its existing no-progress deadline.
The profiled and ordinary application-driven initiator writers also require
their recorded owner before changing mode. Selector configurations remain on
their existing nonblocking path.

This preserves the selected mechanism for owner callbacks, rather than
disabling the diagnostic configuration globally. Default nonblocking branches
short-circuit before the added owner checks. There is no new configuration,
queue, synchronization object or JNI entry point, and no measured latency
claim for the change.

The regression was expanded to 12 live-loopback cases: two write modes, three
owner models (dedicated, multiplexed and single-owner), and busy/selector
runtime choices. Every case verifies that a foreign send completes and leaves
the channel nonblocking. A subsequent owner-side TestRequest reply verifies
that busy stream-toggle callbacks still use their configured output stream;
selector replies remain nonblocking.

Eleven selected suites passed 361 tests in the development edition and the
same 361 tests in the open edition, with zero failures, errors or skips. They
include all 182 `PureJavaSessionTest` cases, the eight-case recovery fixture,
the new ownership cases, ordinary writer deadlines, heap staging, multiplexer
readiness, single-owner admission, batching, command admission, settings and
required-native loading. Both JNI libraries were freshly built and loaded
from separate paths. Export scans of the three changed source/test files
reported zero findings.

## Evidence and limits

`~/archive/libhft/java-tcp-write-owner-review-20261003/` retains the baseline
source head `49bed9fbd014e85c4bb89e34c2ed8c4f8c06c1a3`, unchanged sources,
the original two failing cases and stacks, the exact production patch and
expanded regression source, source hashes, scripts, logs and all 22 selected
XML reports with JSON totals. The ordinary passing baseline fixture is also
retained in `~/archive/libhft/java-logon-write-hang-20261003/`.

Development JNI SHA-256:
`7a8603509c045596e46b936098eb39d97a5f8df0d10c8cd875936985d08c1901`.
Open JNI SHA-256:
`699be95512e78c50d1d8d536a8a125e9008611abf7bb75afafcb9567f6ea94d0`.
The development library includes TCPDirect and SocketXtreme; the open one does
not. These tests exercise kernel loopback and ownership semantics, not a NIC
benchmark or the entire latest Java suite. Owner-side blocking diagnostics
remain optional controls, not a guarantee that a slow peer cannot block them.

## Post-merge verification

An upstream JNI change added the native FIXP liveness switch while this fix was
being published. Verification was repeated at
`5b3ce90b1fa81a550e78f7184a666c0235f8a7a5`, with both native libraries rebuilt
from that revision. The eleven ownership and session suites above plus five
native FIXP runtime suites passed **391 tests in each edition**, with zero
failures, errors or skips. This includes the new liveness binding checks;
it is a selected-suite result, not a full-suite result.

The `postmerge/` evidence subdirectory retains the source pin and hashes, all
32 XML reports, JSON totals, both native build logs, and the verification script
and output. Development JNI SHA-256:
`3a376dd5294b789ce3a068ab693efff80db6aefb4292ae6e486bc8de5cf42078`.
Open JNI SHA-256:
`b38b6aa6636645bef87d7149ef7eecbc36e1f1e27963270fd835f4473efcdeab`.
The earlier 361-test results and library hashes remain tied to their original
source snapshot rather than being relabelled as post-merge results.

The later managed NIC preflight also passed the ownership and recovery suites
within 1,074 selected Java checks against freshly built JNI at `60b9ea253`.
During that work, an older Java 21 conformance fork reproduced the same deadlock
in the original recovery fixture. A thread dump with ownable synchronizers
matched the writer's awaited read lock to the blocked reader's held lock.
See [the preflight record](RECORD_2026-10-03_hp-managed-nic-preflight.md) for the
separate runtime evidence, verification pin and staging status.
