What is wrong
src/dyn75/@@75recv.c caps each X'75' RECV at 4096 bytes:
/* we have to limit the recv() length to no more than
* 4096 bytes due to a limitation of the dyn75 interface
* and the Hercules emulator.
* If we don't do this then the buf is overwritten from
* the start after 4096 bytes are received.
*/
chunk = len - read;
if (chunk > 4096) chunk = 4096;
The symptom in that comment is real. The cap does not prevent it, and the stated
cause is not the cause.
The safe bound is 256. Anything above it can corrupt, including 4096.
Why 4096 does not work
x75.c copies host buffer to guest buffer in 256-byte segments, and the
instruction is restartable: a page fault on the guest buffer is a nullifying
exception, so MVS resolves the page and the instruction runs again from the top.
R1 (bytes remaining) and the base register are advanced across that restart, so
the guest side resumes correctly. The host-side pointer is not: it is recomputed
from map32 [R2] on every entry and R2 is a slot index that never advances. The
remaining bytes are therefore copied from the start of the host buffer to
the already advanced guest address.
So the corruption needs one completed segment before the fault -- i.e. a
transfer above 256 bytes -- and its likelihood scales with how many guest pages
the copy touches. That is the whole of the observed size correlation, and it is
why every cap so far has looked like a fix and then failed:
- this file, 4096, since
cd43a70 (December 2024)
- mvslovers/mvsmf then hit the same corruption at >2048 with that cap in
place, capped at 2048 (d2783f5), and that failed five days later
(4bc1014)
- mvsmf's
receive_raw_data() has read one byte per recv() ever since,
which is immune for the same reason 256 is: a single segment that faults has
copied nothing, and restarts correctly
vstorec resolves both page addresses through MADDRL before either memcpy,
so a segment is atomic against a translation exception. That is what makes
<= 256 immune by construction rather than merely less likely.
Full analysis, the emulator-side fix and the corroborating independent sighting
(twinslow/mvs_nfsd, socktest/, same X'75' layer, different application):
mvslovers/hyperion, TODO.md, "a restarted copy replays the host buffer from
its start".
The change
if (chunk > 256) chunk = 256;
plus a comment stating the actual cause, because the present one sends the next
reader after a Hercules buffer-size limit that does not exist.
Is this independent of the emulator fix?
Yes, in both directions, and it should not wait for it.
- On an unpatched emulator it is correct on its own -- no host change is
involved in the argument above.
- On a patched emulator it is redundant but harmless.
- Neither change requires the other.
It is also permanent. A guest cannot detect the emulator fix -- there is no
return value, status bit or function code that distinguishes a patched emulator,
and adding one would change the interface for every existing guest. So this cap
can never be raised again on the strength of a fixed host, because the same
build has to keep working on an unfixed one.
Cost
More X'75' pairs per transfer: 16x against the current 4096. Against what
consumers actually do today it is a large net win -- for a 1.4 MB body, roughly
5700 pairs at 256, against roughly 1.47 million single-byte recv() calls in
mvsmf's present workaround. Once this lands, mvsmf can return
receive_raw_data() to bulk reads.
Not in scope here
@@75send.c has the same exposure and no cap at all (pl.r1 = len
directly), so a large send can be corrupted the same way -- the mirror image,
the host buffer losing its leading segments. It is left out deliberately:
send() returns a byte count and callers loop on partial writes, so capping it
changes what every caller sees per call rather than just how many instructions
are issued. That needs its own decision and its own issue.
The asymmetry in what has actually been observed is consistent with the
mechanism: a send buffer was just written by the application and is hot, while a
receive buffer can have been idle across an I/O wait, which is exactly when its
pages get stolen.
What is wrong
src/dyn75/@@75recv.ccaps each X'75' RECV at 4096 bytes:The symptom in that comment is real. The cap does not prevent it, and the stated
cause is not the cause.
The safe bound is 256. Anything above it can corrupt, including 4096.
Why 4096 does not work
x75.ccopies host buffer to guest buffer in 256-byte segments, and theinstruction is restartable: a page fault on the guest buffer is a nullifying
exception, so MVS resolves the page and the instruction runs again from the top.
R1 (bytes remaining) and the base register are advanced across that restart, so
the guest side resumes correctly. The host-side pointer is not: it is recomputed
from
map32 [R2]on every entry and R2 is a slot index that never advances. Theremaining bytes are therefore copied from the start of the host buffer to
the already advanced guest address.
So the corruption needs one completed segment before the fault -- i.e. a
transfer above 256 bytes -- and its likelihood scales with how many guest pages
the copy touches. That is the whole of the observed size correlation, and it is
why every cap so far has looked like a fix and then failed:
cd43a70(December 2024)place, capped at 2048 (
d2783f5), and that failed five days later(
4bc1014)receive_raw_data()has read one byte perrecv()ever since,which is immune for the same reason 256 is: a single segment that faults has
copied nothing, and restarts correctly
vstorecresolves both page addresses throughMADDRLbefore eithermemcpy,so a segment is atomic against a translation exception. That is what makes
<= 256immune by construction rather than merely less likely.Full analysis, the emulator-side fix and the corroborating independent sighting
(
twinslow/mvs_nfsd,socktest/, same X'75' layer, different application):mvslovers/hyperion,TODO.md, "a restarted copy replays the host buffer fromits start".
The change
plus a comment stating the actual cause, because the present one sends the next
reader after a Hercules buffer-size limit that does not exist.
Is this independent of the emulator fix?
Yes, in both directions, and it should not wait for it.
involved in the argument above.
It is also permanent. A guest cannot detect the emulator fix -- there is no
return value, status bit or function code that distinguishes a patched emulator,
and adding one would change the interface for every existing guest. So this cap
can never be raised again on the strength of a fixed host, because the same
build has to keep working on an unfixed one.
Cost
More X'75' pairs per transfer: 16x against the current 4096. Against what
consumers actually do today it is a large net win -- for a 1.4 MB body, roughly
5700 pairs at 256, against roughly 1.47 million single-byte
recv()calls inmvsmf's present workaround. Once this lands, mvsmf can return
receive_raw_data()to bulk reads.Not in scope here
@@75send.chas the same exposure and no cap at all (pl.r1 = lendirectly), so a large send can be corrupted the same way -- the mirror image,
the host buffer losing its leading segments. It is left out deliberately:
send()returns a byte count and callers loop on partial writes, so capping itchanges what every caller sees per call rather than just how many instructions
are issued. That needs its own decision and its own issue.
The asymmetry in what has actually been observed is consistent with the
mechanism: a send buffer was just written by the application and is hot, while a
receive buffer can have been idle across an I/O wait, which is exactly when its
pages get stolen.