mirror of
https://github.com/felixrieseberg/windows95.git
synced 2026-05-14 02:21:59 +00:00
Fix guest TCP recv() stalling when v86 NE2000 TX ring wraps (#363)
* Fix guest TCP recv() stalling under concurrent traffic v86's fake_network stores TCPConnection routing fields (hsrc/hdest/ psrc/pdest) as zero-copy subarrays of the SYN frame, which is itself a view into the NE2000 TX ring. Win95's driver uses a 12-slot ring; once it wraps (any concurrent SMB/NBNS/ping while waiting for an upstream reply), pump() emits segments with whatever IP now occupies that slot, the guest RSTs them, and recv() blocks forever. - libv86.js: copy the four address arrays at TCPConnection construction (matches felixrieseberg/v86@dd13099c on fake-network-copy-tcp-addrs, now merged into windows95-base) - tools/probe-tcp.sh + net/tcp-trace.ts + tcp-relay.ts test stub + debug-harness WIN95_PROBE_RUN2: end-to-end regression harness (boot → ping -t → telnet → async write after ring wrap → assert ACK). All env-gated, no production-path change. - docs/v86-patches.md: tracker for all fork patches + upstream PR state - update-v86 SKILL.md: cross-link and new fork-branch entry * Drop checked-in upstream PR description Belongs on the GitHub PR, not in the repo.
This commit is contained in:
@@ -183,7 +183,10 @@ export function startProbe(emulator: any) {
|
||||
const scriptCmd = process.env.WIN95_PROBE_SCRIPT;
|
||||
// WIN95_PROBE_RUN='telnet 1.2.3.4 7777' → literal text into Start→Run,
|
||||
// Enter, then optional WIN95_PROBE_RUN_AFTER keystrokes after _RUN_WAIT ms.
|
||||
// WIN95_PROBE_RUN2 fires a second Start→Run sequence after _RUN2_WAIT ms,
|
||||
// for two-process scenarios (e.g., background ping + telnet).
|
||||
const runCmd = process.env.WIN95_PROBE_RUN;
|
||||
const runCmd2 = process.env.WIN95_PROBE_RUN2;
|
||||
const runAfter = process.env.WIN95_PROBE_RUN_AFTER;
|
||||
// WIN95_PROBE_DOSBOX=1 → after desktop, open COMMAND.COM, type `dir`,
|
||||
// optionally Alt+Enter to fullscreen. Regression test for the windowed
|
||||
@@ -272,6 +275,19 @@ export function startProbe(emulator: any) {
|
||||
{ type: "text", text: runCmd },
|
||||
{ type: "wait", ms: 400 },
|
||||
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
|
||||
...(runCmd2 ? [
|
||||
{ type: "wait", ms: Number(process.env.WIN95_PROBE_RUN2_WAIT) || 3000 },
|
||||
{ type: "chord", keys: [
|
||||
{ dn: SC.CTRL_DN, up: SC.CTRL_UP },
|
||||
{ dn: SC.ESC_DN, up: SC.ESC_UP },
|
||||
]},
|
||||
{ type: "wait", ms: 1200 },
|
||||
{ type: "keys", dn: SC.R_DN, up: SC.R_UP },
|
||||
{ type: "wait", ms: 1000 },
|
||||
{ type: "text", text: runCmd2 },
|
||||
{ type: "wait", ms: 400 },
|
||||
{ type: "keys", dn: SC.ENTER_DN, up: SC.ENTER_UP },
|
||||
] : []),
|
||||
...(runAfter ? [
|
||||
{ type: "wait", ms: Number(process.env.WIN95_PROBE_RUN_WAIT) || 6000 },
|
||||
{ type: "text", text: runAfter },
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Win95Window } from "./app";
|
||||
import { resetState } from "./utils/reset-state";
|
||||
import { setupSmbShare } from "./smb";
|
||||
import { setupTcpRelay } from "./net/tcp-relay";
|
||||
import { setupTcpTrace } from "./net/tcp-trace";
|
||||
import { setupDnsShim } from "./net/dns-shim";
|
||||
import { setupClipboardSync } from "./clipboard";
|
||||
import { startProbe } from "./debug-harness";
|
||||
@@ -394,6 +395,7 @@ export class Emulator extends React.Component<{}, EmulatorState> {
|
||||
|
||||
// Raw TCP egress for ports the fetch adapter ignores (everything but 80).
|
||||
setupTcpRelay(window["emulator"]);
|
||||
setupTcpTrace(window["emulator"]);
|
||||
|
||||
// Serve a host folder over SMB on port 139. Read-only, traversal/symlink
|
||||
// guarded. In Win95: Start → Run → \\HOST\HOST. The env var wins so the
|
||||
|
||||
@@ -191,7 +191,7 @@ function Hb(a,b){let c={};var d=(new DataView(a.buffer,a.byteOffset,a.byteLength
|
||||
a.rst=!!(d&4),a.psh=!!(d&8),a.ack=!!(d&16),a.urg=!!(d&32),a.ece=!!(d&64),a.cwr=!!(d&128),c.tcp=a,c.tcp_data=e.subarray(4*a.doff);else if(17===d){a=new DataView(e.buffer,e.byteOffset,e.byteLength);a={sport:a.getUint16(0),dport:a.getUint16(2),len:a.getUint16(4),checksum:a.getUint16(6),data:e.subarray(8),data_s:(new TextDecoder).decode(e.subarray(8))};if(67===a.dport||67===a.sport){e=e.subarray(8);d=new DataView(e.buffer,e.byteOffset,e.byteLength);e.subarray(44,236);d={op:d.getUint8(0),htype:d.getUint8(1),
|
||||
hlen:d.getUint8(2),hops:d.getUint8(3),xid:d.getUint32(4),secs:d.getUint16(8),flags:d.getUint16(10),ciaddr:d.getUint32(12),yiaddr:d.getUint32(16),siaddr:d.getUint32(20),giaddr:d.getUint32(24),chaddr:e.subarray(28,44),magic:d.getUint32(236),options:[]};e=e.subarray(240);for(l=0;l<e.length;++l)g=l,0!==e[l]&&(++l,h=e[l],l+=h,d.options.push(e.subarray(g,g+h+2)));c.dhcp=d;c.dhcp_options=d.options}else 53===a.dport||53===a.sport?Ib(e.subarray(8),c):123===a.dport&&(d=e.subarray(8),d=new DataView(d.buffer,
|
||||
d.byteOffset,d.byteLength),c.ntp={flags:d.getUint8(0),stratum:d.getUint8(1),poll:d.getUint8(2),precision:d.getUint8(3),root_delay:d.getUint32(4),root_disp:d.getUint32(8),ref_id:d.getUint32(12),ref_ts_i:d.getUint32(16),ref_ts_f:d.getUint32(20),ori_ts_i:d.getUint32(24),ori_ts_f:d.getUint32(28),rec_ts_i:d.getUint32(32),rec_ts_f:d.getUint32(36),trans_ts_i:d.getUint32(40),trans_ts_f:d.getUint32(44)});c.udp=a}}else 2054===d?(d=new DataView(a.buffer,a.byteOffset,a.byteLength),a={htype:d.getUint16(0),ptype:d.getUint16(2),
|
||||
oper:d.getUint16(6),sha:a.subarray(8,14),spa:a.subarray(14,18),tha:a.subarray(18,24),tpa:a.subarray(24,28)},c.arp=a):34525!==d&&y(d);if(c.ipv4)if(c.tcp)a:{a=`${c.ipv4.src.join(".")}:${c.tcp.sport}:${c.ipv4.dest.join(".")}:${c.tcp.dport}`;if(c.tcp.syn&&!c.tcp.ack){b.tcp_conn[a]&&delete b.tcp_conn[a];d=new Jb(b);d.state="syn-received";d.tuple=a;d.last=c;d.hsrc=c.eth.dest;d.psrc=c.ipv4.dest;d.sport=c.tcp.dport;d.hdest=c.eth.src;d.dport=c.tcp.sport;d.pdest=c.ipv4.src;b.bus.pair.send("tcp-connection",
|
||||
oper:d.getUint16(6),sha:a.subarray(8,14),spa:a.subarray(14,18),tha:a.subarray(18,24),tpa:a.subarray(24,28)},c.arp=a):34525!==d&&y(d);if(c.ipv4)if(c.tcp)a:{a=`${c.ipv4.src.join(".")}:${c.tcp.sport}:${c.ipv4.dest.join(".")}:${c.tcp.dport}`;if(c.tcp.syn&&!c.tcp.ack){b.tcp_conn[a]&&delete b.tcp_conn[a];d=new Jb(b);d.state="syn-received";d.tuple=a;d.last=c;d.hsrc=new Uint8Array(c.eth.dest);d.psrc=new Uint8Array(c.ipv4.dest);d.sport=c.tcp.dport;d.hdest=new Uint8Array(c.eth.src);d.dport=c.tcp.sport;d.pdest=new Uint8Array(c.ipv4.src);b.bus.pair.send("tcp-connection",
|
||||
d);if(b.on_tcp_connection)b.on_tcp_connection(d,c);if(b.tcp_conn[a])break a}if(b.tcp_conn[a])b.tcp_conn[a].process(c);else{a=c.tcp.ackn;if(c.tcp.fin||c.tcp.syn)a+=1;d={};d.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src};d.ipv4={proto:6,src:c.ipv4.dest,dest:c.ipv4.src};d.tcp={sport:c.tcp.dport,dport:c.tcp.sport,seq:a,ackn:c.tcp.seq+(c.tcp.syn?1:0),winsize:c.tcp.winsize,rst:!0,ack:c.tcp.syn};b.receive(Eb(b.eth_encoder_buf,d))}}else if(c.udp)if(c.dns)if("static"===b.dns_method){a={};a.eth={ethertype:2048,
|
||||
src:b.router_mac,dest:c.eth.src};a.ipv4={proto:17,src:b.router_ip,dest:c.ipv4.src};a.udp={sport:53,dport:c.udp.sport};d=[];for(e=0;e<c.dns.questions.length;++e)switch(l=c.dns.questions[e],l.type){case 1:d.push({name:l.name,type:l.type,class:l.class,ttl:600,data:[192,168,87,1]})}a.dns={id:c.dns.id,flags:33152,questions:c.dns.questions,answers:d};b.receive(Eb(b.eth_encoder_buf,a))}else Fb(c,b);else c.dhcp?Gb(c,b):c.ntp?(a=Date.now()+rb,d=a%1E3/1E3*sb,e={},e.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},
|
||||
e.ipv4={proto:17,src:c.ipv4.dest,dest:c.ipv4.src},e.udp={sport:123,dport:c.udp.sport},e.ntp=Object.assign({},c.ntp),e.ntp.flags=36,e.ntp.poll=10,e.ntp.ori_ts_i=c.ntp.trans_ts_i,e.ntp.ori_ts_f=c.ntp.trans_ts_f,e.ntp.rec_ts_i=a/1E3,e.ntp.rec_ts_f=d,e.ntp.trans_ts_i=a/1E3,e.ntp.trans_ts_f=d,e.ntp.stratum=2,b.receive(Eb(b.eth_encoder_buf,e))):8===c.udp.dport&&(a={},a.eth={ethertype:2048,src:b.router_mac,dest:c.eth.src},a.ipv4={proto:17,src:c.ipv4.dest,dest:c.ipv4.src},a.udp={sport:c.udp.dport,dport:c.udp.sport,
|
||||
|
||||
@@ -45,6 +45,13 @@ const log = (...a: unknown[]) => {
|
||||
// Ports already claimed by other in-process handlers.
|
||||
const RESERVED_PORTS = new Set([80, 139]);
|
||||
|
||||
// WIN95_TCP_TEST_PORT=<n> short-circuits that port to an in-process fake
|
||||
// upstream that writes a banner asynchronously (i.e., from outside a CPU
|
||||
// tick) and then echoes everything back. Lets the probe harness exercise
|
||||
// the recv() path deterministically without a real network endpoint.
|
||||
const TEST_PORT = Number(process.env.WIN95_TCP_TEST_PORT) || 0;
|
||||
const TEST_BANNER_BYTES = Number(process.env.WIN95_TCP_TEST_BYTES) || 3000;
|
||||
|
||||
// Destinations we never relay to. The emulated LAN (192.168.86/87) has nothing
|
||||
// real behind it; loopback and link-local would let guest software poke at the
|
||||
// host's own services or cloud-metadata endpoints. The rest of RFC1918 is left
|
||||
@@ -71,6 +78,34 @@ export function setupTcpRelay(emulator: V86) {
|
||||
conn.accept();
|
||||
log(`→ ${ip}:${port} (${conn.tuple})`);
|
||||
|
||||
if (TEST_PORT && port === TEST_PORT) {
|
||||
const mode = process.env.WIN95_TCP_TEST_MODE || "banner";
|
||||
const banner = Buffer.alloc(TEST_BANNER_BYTES, 0x41);
|
||||
banner.write(`HELLO from tcp-relay test, ${TEST_BANNER_BYTES}B\r\n`);
|
||||
let n = 0;
|
||||
conn.on("data", (d) => {
|
||||
if (d.length === 0) return;
|
||||
n += d.length;
|
||||
log(`test ← guest ${d.length}B (total ${n})`);
|
||||
if (mode === "after-data" && n >= 1) {
|
||||
setTimeout(() => {
|
||||
log(`test → guest ${banner.length}B banner (async, after-data)`);
|
||||
conn.write(banner);
|
||||
}, 10);
|
||||
}
|
||||
});
|
||||
if (mode === "banner")
|
||||
setTimeout(
|
||||
() => {
|
||||
log(`test → guest ${banner.length}B banner (async)`);
|
||||
conn.write(banner);
|
||||
},
|
||||
Number(process.env.WIN95_TCP_TEST_DELAY) || 50,
|
||||
);
|
||||
conn.on_shutdown = conn.on_close = () => log("test guest closed");
|
||||
return;
|
||||
}
|
||||
|
||||
let connected = false;
|
||||
let pending: Buffer[] | null = [];
|
||||
let upstreamGone = false;
|
||||
|
||||
118
src/renderer/net/tcp-trace.ts
Normal file
118
src/renderer/net/tcp-trace.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
// Deep packet trace for the v86 userspace TCP ↔ NE2000 path. Activated by
|
||||
// WIN95_TCP_TRACE=<port> — every guest-TX and host-RX TCP frame on that
|
||||
// dest port is decoded and appended to /tmp/win95-tcp-trace.log, and the
|
||||
// NE2000 receive() is wrapped so we can see whether frames injected from
|
||||
// outside a CPU tick ever land in the RX ring (vs. being filtered out).
|
||||
|
||||
import * as fs from "fs";
|
||||
|
||||
const TRACE_FILE =
|
||||
process.env.WIN95_TCP_TRACE_FILE || "/tmp/win95-tcp-trace.log";
|
||||
|
||||
interface V86 {
|
||||
bus: { register(name: string, fn: (arg: unknown) => void): void };
|
||||
v86?: { cpu?: { devices?: { net?: unknown } } };
|
||||
}
|
||||
|
||||
function flags(b: number): string {
|
||||
let s = "";
|
||||
if (b & 1) s += "F";
|
||||
if (b & 2) s += "S";
|
||||
if (b & 4) s += "R";
|
||||
if (b & 8) s += "P";
|
||||
if (b & 16) s += "A";
|
||||
return s || ".";
|
||||
}
|
||||
|
||||
function decodeTcp(f: Uint8Array, port: number) {
|
||||
if (f.length < 54) return null;
|
||||
if (((f[12] << 8) | f[13]) !== 0x0800) return null;
|
||||
const ihl = (f[14] & 0x0f) * 4;
|
||||
if (f[14 + 9] !== 6) return null;
|
||||
const ipLen = (f[16] << 8) | f[17];
|
||||
const t = 14 + ihl;
|
||||
const sport = (f[t] << 8) | f[t + 1];
|
||||
const dport = (f[t + 2] << 8) | f[t + 3];
|
||||
if (sport !== port && dport !== port) return null;
|
||||
const seq =
|
||||
(f[t + 4] * 0x1000000 + (f[t + 5] << 16) + (f[t + 6] << 8) + f[t + 7]) >>>
|
||||
0;
|
||||
const ack =
|
||||
(f[t + 8] * 0x1000000 + (f[t + 9] << 16) + (f[t + 10] << 8) + f[t + 11]) >>>
|
||||
0;
|
||||
const doff = (f[t + 12] >> 4) * 4;
|
||||
const fl = f[t + 13];
|
||||
const win = (f[t + 14] << 8) | f[t + 15];
|
||||
const dlen = ipLen - ihl - doff;
|
||||
return { sport, dport, seq, ack, fl: flags(fl), win, dlen, frameLen: f.length };
|
||||
}
|
||||
|
||||
export function setupTcpTrace(emulator: V86) {
|
||||
const port = Number(process.env.WIN95_TCP_TRACE);
|
||||
if (!port) return;
|
||||
|
||||
const t0 = Date.now();
|
||||
fs.writeFileSync(TRACE_FILE, `--- trace port ${port} ---\n`);
|
||||
const w = (s: string) => {
|
||||
const line = `[${((Date.now() - t0) / 1000).toFixed(3)}s] ${s}\n`;
|
||||
try {
|
||||
fs.appendFileSync(TRACE_FILE, line);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
emulator.bus.register("net0-send", (raw: unknown) => {
|
||||
const f = raw as Uint8Array;
|
||||
const r = decodeTcp(f, port);
|
||||
if (r)
|
||||
w(
|
||||
`guest→ ${r.sport}>${r.dport} seq=${r.seq} ack=${r.ack} ${r.fl} win=${r.win} len=${r.dlen}`,
|
||||
);
|
||||
else if (f.length >= 14) {
|
||||
const et = (f[12] << 8) | f[13];
|
||||
const dst =
|
||||
et === 0x0800 ? f.subarray(30, 34).join(".") : et === 0x0806 ? "arp" : "?";
|
||||
w(
|
||||
`guest→ [other] len=${f.length}@${f.byteOffset} et=0x${et.toString(16)} dst=${dst}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Wrap NE2000 receive() to see if/why a frame is dropped at the NIC layer.
|
||||
const arm = () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const ne2k = (emulator as any).v86?.cpu?.devices?.net;
|
||||
if (!ne2k) return false;
|
||||
const orig = ne2k.receive.bind(ne2k);
|
||||
ne2k.receive = function (a: Uint8Array) {
|
||||
const r = decodeTcp(a, port);
|
||||
if (r) {
|
||||
const pages = 1 + ((Math.max(60, a.length) + 4) >> 8);
|
||||
const avail =
|
||||
this.boundary > this.curpg
|
||||
? this.boundary - this.curpg
|
||||
: this.pstop - this.curpg + this.boundary - this.pstart;
|
||||
const macOk =
|
||||
a[0] === this.mac[0] && a[1] === this.mac[1] && a[2] === this.mac[2];
|
||||
const drop =
|
||||
this.cr & 1
|
||||
? "STP"
|
||||
: !macOk && a[0] !== 0xff && !(this.rxcr & 16)
|
||||
? "MAC"
|
||||
: avail < pages && this.boundary !== 0
|
||||
? "FULL"
|
||||
: "";
|
||||
w(
|
||||
` ne2k.recv frame=${a.length} sip=${a.slice(26, 30).join(".")} cr=0x${this.cr.toString(16)}` +
|
||||
` cur=${this.curpg} bnd=${this.boundary}${drop ? " DROP:" + drop : ""}`,
|
||||
);
|
||||
}
|
||||
return orig(a);
|
||||
};
|
||||
w("ne2k.receive wrapped");
|
||||
return true;
|
||||
};
|
||||
if (!arm()) {
|
||||
const p = setInterval(() => arm() && clearInterval(p), 100);
|
||||
setTimeout(() => clearInterval(p), 10000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user