Skip to content

AOT: sock.write() via Map-retrieved object silently drops ~100-byte packets inside 'data' callback (closure sock.write works) #91

Description

@proggeramlug

Environment

  • perry 0.5.98
  • macOS arm64 (darwin 25.4.0)

Problem

Inside a net.Socket 'data' event callback, calling obj.sock.write(bytes) where obj was retrieved from a Map.get() lookup silently fails to deliver the bytes to the peer, even though:

  • sock.write(bytes) returns immediately (with undefined — no error)
  • The same bytes buffer, written via the closure-captured sock reference, reaches the server and gets a response
  • Small writes (≤5 bytes) via the Map path DO work; the failure appears only for larger writes (the driver hits it at 101 bytes)

This looks like the same class of bug as #87 (Promise resolve via property dispatch) but for Socket.write — the method call returns but the underlying I/O op never happens.

Minimal repro

// repro.ts — requires a TCP service that speaks first (any MySQL server).
import * as net from 'node:net';
import { writeHandshakeResponse41 } from '@perry/mysql/src/protocol/writer';
import { writePacket } from '@perry/mysql/src/protocol/framing';
import { DEFAULT_CLIENT_CAPABILITIES, CLIENT_CONNECT_ATTRS } from '@perry/mysql/src/protocol/capabilities';
import { sha256Scramble } from '@perry/mysql/src/auth/caching-sha2';

const STATES = new Map<number, { sock: net.Socket }>();
const sock = net.createConnection('YOUR.MYSQL.HOST' as never, 3306 as never);
STATES.set(1, { sock: sock });

let count = 0;
sock.on('data', (chunk: Buffer) => {
    count += 1;
    if (count === 1) {
        // Extract 20-byte challenge from HandshakeV10 at known offsets, scramble, build packet...
        // (elided for brevity — full repro in @perry/mysql tests/perry-aot-smoke.ts)
        const bytes = /* 101-byte HandshakeResponse41 */;
        const st = STATES.get(1);
        if (st !== undefined) {
            st.sock.write(bytes);   // ← via Map-retrieved socket
        }
        console.log('[write] done');
    } else {
        console.log('[data #2] server responded!');
        process.exit(0);
    }
});
setTimeout(() => { console.log('timeout'); process.exit(1); }, 6000);

Change the key line to:

sock.write(bytes);   // ← closure-captured socket, same bytes

and the program immediately receives the server's response.

Observed (Map path)

[data #1] len=95        — got HandshakeV10
[write] bytes.len=101
[write] done
timeout                  — server never sends AuthMoreData / OK / ERR

Expected (matches closure path and Node/Bun behaviour)

[data #1] len=95
[write] bytes.len=101
[write] done
[data #2] len=6         — server's AuthMoreData{0x03} FAST_AUTH_SUCCESS

Content verification

I verified the 101 bytes produced via the Map path are byte-identical to the bytes the closure path produces (both computed via writeHandshakeResponse41(...) + writePacket(1, payload)), and the MySQL server accepts the closure-sent version. So the bytes are valid; they're simply not being flushed when dispatched via obj.sock.write(...).

Hit in real code

@perry/mysql stores the net.Socket inside a ConnState struct keyed in CONN_STATES: Map<number, ConnState>. Every sendAuthFrame / sendCommand call does st.sock.write(bytes). Under Bun/Node this works perfectly (all 138 unit + 13 real-server integration tests pass). Under Perry 0.5.98 AOT, after #78#88 were fixed, the driver successfully:

  1. Connects to MySQL 8.0.45 on webserver.skelpo.net
  2. Decodes HandshakeV10, receives 20-byte challenge
  3. Computes correct caching_sha2_password auth response (hex matches Node's output byte-for-byte)
  4. Builds valid 97-byte HandshakeResponse41 + 4-byte header = 101 bytes
  5. Calls st.sock.write(bytes) — call returns normally
  6. Server never responds → connect times out after 10 s

Workarounds on the @perry/mysql side are ugly (either flatten the state map out of the write path or pass the socket directly through every helper). I'd rather wait for a proper codegen fix.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions