informix-db/docs/PROTOCOL_NOTES.md
Ryan Malloy 1a149074d4 Phase 0: populate PROTOCOL_NOTES and JDBC_NOTES from clean-room JDBC reading
Decompiled ifxjdbc.jar (4.50.JC10, build 146, 2023-03-07) with CFR 0.152
into build/jdbc-src/. The decompiled tree is gitignored — it's a
clean-room understanding reference, not shipped code.

Findings landed in two artifacts:

JDBC_NOTES.md — the reverse-lookup index:
- JAR identity (SHA256, manifest, line counts)
- Package layout (com.informix.{asf,jdbc,lang} are the load-bearing
  packages; org.bson and the JDBC API surface get ignored)
- Class index mapping each wire-protocol concern to the responsible
  Java class. Highlights:
  - com.informix.asf.Connection (the wire transport / login PDU)
  - com.informix.asf.IfxData{Input,Output}Stream (framing primitives)
  - com.informix.jdbc.IfxMessageTypes (140+ message-tag constants)
  - com.informix.lang.JavaToIfxType / IfxToJavaType (codecs)
  - com.informix.jdbc.IfxSqli / IfxSqliConnect (the SQLI state machine)
- Auth landscape: plain-password is inline in the binary login PDU;
  PAM is a server-initiated post-login challenge/response; CSM is
  removed from this driver (literally throws an error if you try)

PROTOCOL_NOTES.md — the byte-level wire-format reference:
- Endianness: big-endian, network byte order (confirmed from
  JavaToIfxInt source)
- Width table: SmallInt 2B, Int 4B, BigInt 8B, plus the legacy 10-byte
  LongInt that we skip for MVP
- 16-bit alignment requirement for variable-length payloads — every
  string/decimal/datetime is 0-padded if odd-length, missing this
  desynchronizes the parser
- Login PDU structure decoded byte-by-byte from encodeAscBinary():
  SLheader (6 bytes) + PFheader with markers 100/101/104/106/107/
  108/116/127, capability bitfield, env vars, process info, app name
- Disconnection: bare [short SQ_EXIT=56] both directions, no header
- Post-login messages have NO header — protocol is stream-oriented:
  [short tag][payload][short tag][payload]...
- Message-type tag table categorized by purpose
- Open questions list and cross-check matrix tracking what's
  JDBC-derived vs PCAP-confirmed

DECISION_LOG.md additions:
- ifxjdbc.jar 4.50.JC10 selected as JDBC reference; CFR 0.152 as decompiler
- CSM is officially dead — never plan for it
- Plain-password auth is single-round-trip (no challenge/response)
- Wire-framing primitives locked in for _protocol.py
- Container credentials: user=informix, password=in4mix, on port 9088,
  TLS off

Phase 0 exit gate: criteria #1 (login layout), #2 (message-type tags),
#3 (SELECT 1 hypothesis) are derived from JDBC. PCAP capture (task #7)
and cross-reference (task #2) remaining to corroborate.
2026-05-02 16:00:30 -06:00

526 lines
20 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# SQLI Wire Protocol Notes
> **Phase 0 spike artifact.** Byte-level reference for the Informix SQLI wire protocol, derived from clean-room study of the decompiled IBM Informix JDBC driver (`ifxjdbc.jar`, `Implementation-Version: 4.50.10-SNAPSHOT` / printable `4.50.JC10`, build 146 from 2023-03-07; SHA256 in `JDBC_NOTES.md`) cross-checked against packet captures of the reference exchange (pending).
---
## Source attribution conventions
Each documented byte sequence cites both sources of evidence:
- 🟡 **JDBC**: cross-referenced against `<class>#<method>()` in the decompiled tree (see `JDBC_NOTES.md`)
- 🔵 **PCAP**: observed in `docs/CAPTURES/<file>.pcap` at offset `<n>`
-**CONFIRMED**: corroborated by both 🟡 and 🔵
- 🟠 **UNVERIFIED**: only one of the two sources
**Current state** (2026-05-02): all 🟡 findings are present from JDBC reading. PCAP capture is pending (Phase 0 task #7) and required for ✅. Treat everything below as 🟠 pending PCAP cross-check.
---
## 1. Wire framing primitives 🟡
### Endianness
**Big-endian (network byte order) for all multi-byte integers.**
Source: `com.informix.lang.JavaToIfxType.JavaToIfxInt(int)` line 51-54:
```java
public static byte[] JavaToIfxInt(int i) {
byte[] b = new byte[]{(byte)(i >> 24), (byte)(i >> 16), (byte)(i >> 8), (byte)i};
return b;
}
```
Same pattern for `JavaToIfxSmallInt` (2 bytes) and `JavaToIfxLongBigInt` (8 bytes). Java's `DataOutputStream` defaults to big-endian; Informix matches that.
### Width table
| Wire type | Width (bytes) | Java method | Notes |
|-----------|---------------|-------------|-------|
| SmallInt | 2 | `writeSmallInt(short)` / `writeShort` | SMALLINT, message-type tags, length prefixes |
| Int | 4 | `writeInt(int)` | INTEGER, capability flags, sizes |
| LongBigInt| 8 | `writeLongBigint(long)` | BIGINT (use this for 64-bit ints) |
| LongInt | 10 | `writeLongInt(long)` | **Legacy variable-numeric LONG INT — skip MVP**, predates 64-bit ints |
| Real | 4 | `writeReal(float)` | IEEE 754 single |
| Double | 8 | `writeDouble(double)` | IEEE 754 double |
| Date | 4 | `writeDate(Date)` | day-count from Informix epoch (1899-12-31, to confirm) |
### Variable-length encoding (string, decimal, datetime, interval, BYTE, TEXT)
```
[short length][bytes payload][optional 0x00 pad if length is odd]
```
**The 16-bit alignment is a hard requirement.** Source: `IfxDataOutputStream.writePadded(byte[])`:
```java
public void writePadded(byte[] b) throws IOException {
this.write(b, 0, b.length);
if ((b.length & 1) >= 1) {
this.write(0);
}
}
```
Mirror in `IfxDataInputStream.readPadded`. Every variable-payload message needs this padding or the next short read will misalign and the entire parser desynchronizes.
### String encoding
`com.informix.lang.JavaToIfxType.JavaToIfxChar(String)` returns:
```
[short length+1][bytes][0x00 nul terminator]
```
The `+1` is the trailing nul. So an N-character string takes `2 + N + 1` bytes, then padded to even.
Note: there is also a `JavaToIfx4BytesChar` variant for wide-char strings. Probably for GLS multibyte locales. Phase 6+.
---
## 2. Connection establishment 🟡
### TCP setup
- Default port: **9088** (native SQLI). Port 9089 is SSL.
- `Socket.connect(InetSocketAddress(host, port), loginTimeout)`
- `setTcpNoDelay(true)` — Nagle off
- `setKeepAlive(socKeepAlive)` — opt-in via `IFX_SOC_KEEPALIVE` property
- Optional SSL wrap via `SSLSocketFactory`. **Phase 6+; skip for MVP.**
- Buffered streams: `BufferedInputStream(in, 4096)` / `BufferedOutputStream(out, 4096)` over the raw socket streams. The `IfxDataInputStream`/`IfxDataOutputStream` wrap the buffered streams.
### Capability bits (driving login path selection)
- `capabilities == 0` → legacy text-mode login (`EncodeAscString`)
- `capabilities > 0` → modern binary login (`encodeAscBinary`) **— this is what we implement**
The capabilities bitfield itself is opaque from the client side; the JDBC driver computes it based on opt-props. For MVP we will hardcode a sensible value matching what JDBC sends in a vanilla connection (will derive from PCAP).
### Other constants seen in `Connection.java`
| Constant | Value | Notes |
|----------|-------|-------|
| `MAX_BUFF_SIZE` | 32768 | upper bound for individual PDUs |
| `MIN_BUFF_SIZE` | 140 | lower bound |
| `STREAM_BUF_SIZE` | 4096 | socket buffer size |
| `PFCONREQ_BUF_SIZE` | 2048 | login request buffer |
| `SL_HEADER_SIZE` | 6 | the SLheader is 6 bytes |
| `applType` | `"sqlexec"` | 12-char nul-padded application type |
| `applID` | `"sqli"` | application ID string |
| `PROT_SQLIOL` | `"ol"` | 8-char nul-padded protocol identifier |
| `NET_TLITCP` | `"tlitcp"` | 8-char nul-padded network type |
| `floatType` | `"IEEEM"` | float encoding type identifier (5 chars) |
---
## 3. Login sequence (binary path) 🟡
### 3a. Client→server: SLheader (6 bytes)
```
[short totalPduSize+6] # 2 bytes — PDU payload length + 6 (header overhead)
[byte slType] # 1 byte — 1=SLTYPE_CONREQ (connection request)
[byte slAttribute] # 1 byte — 60=PF_PROT_SQLI_0600 (protocol version)
[short slOptions] # 2 bytes — 0 in vanilla connect
```
Source: `Connection.EncodeSLheader(o, pfPDUsize, 1, 60, 0)`.
### 3b. Client→server: PFheader (the binary login PDU)
This is the body that the SLheader announces. Total size depends on credentials and env vars. The byte layout, in emission order from `Connection.encodeAscBinary()`:
```
[short 100] # SQ_ASSOC marker — start of "association" record
[short 101] # SQ_ASCBINARY marker — binary-format indicator
[int 61] # ?? known constant; meaning TBD
[short floatType.len+1] # = 6
[bytes "IEEEM"][byte 0] # float-format identifier, nul-terminated
[short 108] # SQ_ASCBPARMS marker — binary parameters block
[bytes "sqlexec\0\0\0\0\0"] # applType, fixed 12 bytes
[short version.len+1] # = 6
[bytes "9.280"][byte 0] # client version (hardcoded "9.280" — note: NOT JDBC version!)
[short serial.len+1] # = 12
[bytes "RDS#R000000"][byte 0] # client serial (hardcoded constant)
[short applID.len+1] # = 5
[bytes "sqli"][byte 0] # application ID
[int capabilities] # client capability flags (opaque bitfield)
[int 0] # reserved
[int 0] # reserved
[short 1] # ?? section marker (NOT in IfxMessageTypes)
[short username.len+1]
[bytes username][byte 0]
# password (optional)
if password is null:
[short 0] # zero-length password
else:
[short password.len+1]
[bytes password][byte 0]
[bytes "ol\0\0\0\0\0\0"] # PROT_SQLIOL — 8 bytes
[int 61] # ?? same magic as before
[bytes "tlitcp\0\0"] # NET_TLITCP — 8 bytes
[int 1] # UTYPE_INTERNET (= 1)
[short 104] # SQ_ASCINITREQ marker
[short 11] # ASF_XCONNECT (= 11)
[int stmtoptions] # = 3 | (sqlhGroup ? 0x2000000 : 0) | (trustedCtx ? 0x4000000 : 0)
# bit 0+1 = ASF_AMBIG_SEOL (=3)
# bit 25 = ASF_GRPREF (sqlh group)
# bit 26 = ASF_TRUSTCTXT (trusted context)
[short servername.len+1]
[bytes servername][byte 0]
# database (optional)
if dbname is null:
[short 0]
else:
[short dbname.len+1]
[bytes dbname][byte 0]
[short 0][short 0][short 0][short 0] # 4 reserved/empty option slots (8 bytes total)
# Environment vars (encodeEnv)
[short 106] # SQ_ASCENV marker
[short num_envvars]
for each (name, value) in envlist:
[short name.len+1]
[bytes name][byte 0]
[short value.len+1]
[bytes value][byte 0]
# Process info (encodeEnvPInfo)
[short 107] # SQ_ASCPINFO marker
[int 0] # reserved
[int procId] # OS process ID
[int threadId] # thread ID
[short hostname.len+1]
[bytes hostname][byte 0]
[short 0] # reserved
[short cwd.len+1]
[bytes cwd][byte 0]
# AppName section (SQ_ASCMISC_60)
[short 116] # SQ_ASCMISC_60
[short total_section_length] # = 10 + appname.len + 1
[int 0] # reserved
[int 0] # reserved
[short appname.len+1]
[bytes appname][byte 0]
# End-of-PDU
[short 127] # SQ_ASCEOT — end-of-transmission for login
```
### 3c. Server→client: response
`Connection.recvConnectionResponse()` reads:
```
[short totalLength] # length of the response PDU
[bytes (totalLength - 2)] # payload, parsed below
```
Then the payload starts with:
```
[byte SLType] # 2=CONACC (accepted), 3=CONREJ (rejected), 13=REDIRECT
[skipBytes 3] # slAttribute + slOptions — usually unused on response
[short 100] # SQ_ASSOC marker
[short 101] # SQ_ASCBINARY marker
... DecodeAscBinary payload ...
```
`DecodeAscBinary(in)` reads:
```
[skipBytes 4]
[short subLength]
[skipBytes subLength]
[short 108] # SQ_ASCBPARMS marker
[skipBytes 12]
[short verLen][bytes serverVersion] # e.g. "15.00.UC1"
[short serLen][bytes serverSerial]
[short appLen][bytes appID]
[int Cap_1][int Cap_2][int Cap_3] # server capability flags (3 × 4 bytes)
[skipBytes 2]
[short subLen1][skipBytes subLen1] # optional reserved
[short subLen2][skipBytes subLen2] # optional reserved
[skipBytes 24]
[short marker] # final disposition:
# 102 = SQ_ASCINITRESP → error/warning section follows
# 103 = redirect
# 127 = SQ_ASCEOT (clean)
```
Error block (when marker=102):
```
[skipBytes 6]
[short svcError]
[short osError]
[short Warnings]
[short numErrors]
for i in 0..numErrors-1:
[short offset] # if -1, stop
[readChar errMsg] # length-prefixed string
```
### 3d. Login PDU summary
- One client-emitted PDU for the entire login (username + password inline)
- One server-emitted PDU for the response (success → server version + capabilities; failure → error block)
- **No challenge-response round trip for plain auth.** PAM auth uses `SQ_CHALLENGE=129`/`SQ_RESPONSE=130` AFTER the binary login PDU, but only if the server requests it. Plain-auth happy path is two messages total.
---
## 4. Message framing (post-login) 🟡
After successful login, messages are **bare** — no SLheader, no PFheader. Each message is:
```
[short messageType] # 2-byte big-endian tag from IfxMessageTypes
[payload] # variable, depends on message type
```
Evidence: `Connection.disconnectOrderly()` writes only:
```java
byte[] b = JavaToIfxType.JavaToIfxSmallInt((short)56); // SQ_EXIT = 56
this.asfIfxOs.write(b);
this.asfIfxOs.flush();
```
And reads back exactly the same shape. No length prefix, no framing overhead. The receiving side keys off the message type tag to know how many bytes follow.
This is a **stream-oriented** protocol, not a packet-oriented one. The wire is a continuous sequence of `[tag][payload][tag][payload]…` and the parser must know each tag's payload structure to know where the next tag begins.
---
## 5. Message type tags 🟡
The full enumeration is in `com.informix.jdbc.IfxMessageTypes` (163 lines, ~140 constants). Key categories below.
### Connection lifecycle (post-login)
| Tag | Decimal | Hex | Purpose |
|-----|---------|-----|---------|
| `SQ_VERSION` | 53 | 0x35 | client/server version negotiation |
| `SQ_INFO` | 81 | 0x51 | info request (sub-types: INFO_VERSION=2, INFO_TYPE=3, INFO_CAPABILITY=4, INFO_DB=5, INFO_ENV=6) |
| `SQ_PROTOCOLS` | 126 | 0x7E | protocol negotiation |
| `SQ_CONNECT` | 112 | 0x70 | connection setup |
| `SQ_DBOPEN` | 36 | 0x24 | open database |
| `SQ_DBCLOSE` | 37 | 0x25 | close database |
| `SQ_DBLIST` | 26 | 0x1A | list databases |
| `SQ_DISCONNECT` | 114 | 0x72 | logical disconnect |
| `SQ_EXIT` | 56 | 0x38 | terminate session (on the wire for disconnect) |
| `SQ_EOT` | 12 | 0x0C | end of transmission (returned by server on disconnect ack) |
### Auth (server-initiated challenge-response, PAM only)
| Tag | Decimal | Purpose |
|-----|---------|---------|
| `SQ_CHALLENGE` | 129 | server challenges client (PAM) |
| `SQ_RESPONSE` | 130 | client response to challenge |
| `SQ_ACCEPT` | 127 | server accepts |
| `SQ_ACK` | 128 | acknowledgement |
**Plain-password auth does NOT use these.** They are only for PAM and other challenge-response auth schemes. Skip MVP.
### Statement execution
| Tag | Decimal | Purpose |
|-----|---------|---------|
| `SQ_COMMAND` | 1 | execute SQL directly (immediate execution, no preparation) |
| `SQ_PREPARE` | 2 | prepare a statement (returns a handle) |
| `SQ_ID` | 4 | reference a prepared statement by id |
| `SQ_BIND` | 5 | bind a parameter to a prepared statement |
| `SQ_OPEN` | 6 | open cursor on a prepared statement |
| `SQ_EXECUTE` | 7 | execute a prepared statement |
| `SQ_DESCRIBE` | 8 | describe columns of a prepared statement |
| `SQ_NFETCH` | 9 | fetch next row(s) |
| `SQ_CLOSE` | 10 | close cursor |
| `SQ_RELEASE` | 11 | release prepared statement |
| `SQ_CURNAME` | 3 | name a cursor |
### Result responses (server→client)
| Tag | Decimal | Purpose |
|-----|---------|---------|
| `SQ_TUPLE` | 14 | a row of data |
| `SQ_DONE` | 15 | end of result set / completion of DML |
| `SQ_EOT` | 12 | end of transmission |
| `SQ_ERR` | 13 | error |
### Transactions
| Tag | Decimal | Purpose |
|-----|---------|---------|
| `SQ_BEGIN` | 35 | begin transaction |
| `SQ_BEGIN_NOREPL` | 115 | begin (no replication) |
| `SQ_CMMTWORK` | 19 | commit work |
| `SQ_RBWORK` | 20 | rollback work |
| `SQ_SVPOINT` | 21 | declare savepoint |
| `SQ_SQLISETSVPT` | 137 | set savepoint (newer protocol) |
| `SQ_SQLIRELSVPT` | 138 | release savepoint |
| `SQ_SQLIRBACKSVPT` | 139 | rollback to savepoint |
### Distributed transactions (XA) — Phase 6+
`SQ_X*` family: 65-74, 82. `SQ_XBEGIN`, `SQ_XCOMMIT`, `SQ_XROLLBACK`, `SQ_XPREPARE`, etc.
### LOB / Blob — Phase 6+
`SQ_FETCHBLOB` (38), `SQ_BLOB` (39), `SQ_BBIND` (41), `SQ_SBBIND` (52), `SQ_FILE_READ` (106), `SQ_FILE_WRITE` (107).
### RPC layer (sub-protocol, tag range 200-205) — Phase 6+
| Tag | Decimal | Purpose |
|-----|---------|---------|
| `SQ_INVOKE` | 200 | RPC invocation |
| `SQ_REPLY` | 201 | RPC reply |
| `SQ_EXCEPTION` | 202 | RPC exception |
| `SQ_VERSION_REQ` | 203 | version request |
| `SQ_VERSION_REPLY` | 204 | version reply |
| `SQ_AMFPARAM` | 205 | AMF parameter (used by stored procedures?) |
---
## 6. Statement execution: `SELECT 1` 🟠
Hypothesis (to be validated by PCAP):
**Client→server:**
```
[short SQ_COMMAND=1]
[short sql.len+1]
[bytes "SELECT 1"][byte 0]
```
or possibly with extra wrapper bytes (e.g. statement ID, options). Unknown without capture.
**Server→client (success):**
```
[short SQ_DESCRIBE=8 or implicit description]
... column descriptors ...
[short SQ_TUPLE=14]
... row payload ...
[short SQ_DONE=15]
... completion data (rowcount, etc.) ...
[short SQ_EOT=12]
```
This will be validated and made specific by Phase 0 task #2 (cross-reference captures).
---
## 7. Result-set framing 🟠
Pending PCAP capture. Hypothesized structure:
- Each row is `[short SQ_TUPLE=14][row payload]`
- Row payload is the column values in order, encoded per their type's wire format (with the 16-bit alignment rule)
- NULL representation: TBD (probably a per-column flag bit or sentinel value)
- End-of-result: `[short SQ_DONE=15]` followed by completion metadata (estimated rows, actual rows, etc.)
`com.informix.jdbc.IfxColumnInfo` likely holds the column-descriptor structure. Read this once we have a PCAP to align against.
---
## 8. Disconnection ✅ (well-corroborated by JDBC alone)
**Client→server:**
```
[short SQ_EXIT=56]
```
**Server→client:**
```
[short SQ_EXIT=56] # echo
or
[short SQ_EOT=12] # end of transmission
```
Server may interleave one or more `[short SQ_XACTSTAT=99]` records before the final ack (transaction status info during shutdown). Client reads and discards these until it sees 56 or 12.
Source: `Connection.disconnectOrderly()` (Connection.java:227-265).
---
## 9. Error responses 🟠
Two error paths observed:
### 9a. Login-time errors (during login PDU exchange)
Error block embedded in the response, marked by `[short 102]` (SQ_ASCINITRESP):
```
[skipBytes 6]
[short svcError] # Informix service error code
[short osError] # OS-level error code
[short numWarnings]
[short numErrors]
for i in 0..numErrors:
[short offset] # if -1, stop
[readChar errMsg] # length-prefixed string
```
Throws `IfxASFRemoteException(svcError, osError, errMsg)`.
### 9b. Statement-time errors (post-login)
Hypothesized: `[short SQ_ERR=13][error payload]`. Structure of the payload TBD; needs PCAP. Likely contains SQLCODE, ISAM error code, error message text, possibly SQLSTATE. Look at `IfxSqli` error-decode methods once we have a capture.
---
## 10. Type codecs
### 10a. IDS type codes (column descriptors)
`com.informix.lang.IfxTypes` is the constants class. Pending — read this file and tabulate the type codes we care about for MVP (SMALLINT, INT, BIGINT, FLOAT, REAL, CHAR, VARCHAR, NCHAR, NVARCHAR, BOOLEAN, DATE, NULL marker).
### 10b. Wire encodings (Phase-2 MVP types)
| IDS Type | Type Code | Wire format | Reader | Writer |
|----------|-----------|-------------|--------|--------|
| SMALLINT | TBD | 2 bytes BE | `readSmallInt()` | `writeSmallInt(short)` |
| INTEGER | TBD | 4 bytes BE | `readInt()` | `writeInt(int)` |
| BIGINT | TBD | 8 bytes BE | `readLongBigint()` | `writeLongBigint(long)` |
| FLOAT (DOUBLE) | TBD | 8 bytes IEEE 754 | `readDouble(prec)` | `writeDouble(double)` |
| REAL (SMALLFLOAT) | TBD | 4 bytes IEEE 754 | `readReal(prec)` | `writeReal(float)` |
| CHAR(N) | TBD | length-prefixed, padded to even | `readChar()` | `writeChar(byte[])` |
| VARCHAR(N) | TBD | length-prefixed, padded to even | `readChar()` | (similar) |
| BOOLEAN | TBD | 1 byte 't'/'f' (per Informix convention; verify) | TBD | TBD |
| DATE | TBD | 4 bytes — int day count from Informix epoch (1899-12-31, verify) | `readDate()` | `writeDate(Date)` |
### 10c. Phase 6+ types
DATETIME (qualifier-byte precision), INTERVAL (qualifier-encoded), DECIMAL/NUMERIC (Informix BCD), MONEY, LVARCHAR, BYTE/TEXT (length-prefixed BLOBs), CLOB/BLOB (smart blobs with handles), ROW, COLLECTION (LIST/SET/MULTISET).
---
## 11. Open questions
- Exact byte layout of `SQ_COMMAND` payload — does it include statement options after the SQL text?
- NULL representation in row data — null bitmap, sentinel value, or per-column flag?
- Result-set column-descriptor structure (the bytes that come back before the first `SQ_TUPLE`).
- How `Cap_1`/`Cap_2`/`Cap_3` (server capability flags) are bit-allocated.
- The leading `[int 61]` magic in the login PDU — what does 61 mean? (Possibly version-related: PF_PROT_SQLI_0600 = 60, PF_PROT_SQLI_WITH_CSS = 61?)
- Why the embedded `versionNumber = "9.280"` string (not the JDBC version, not the server version).
- The `[short 1]` marker between `Cap_3` and the username — what's it for?
- Whether the server emits any banner at TCP-accept time before the client's login PDU.
All of these resolve from packet capture in Phase 0 task #7.
---
## 12. Cross-check matrix
| Phase 0 milestone | 🟡 JDBC | 🔵 PCAP | Status |
|-------------------|--------|--------|--------|
| Wire framing primitives (endianness, widths, padding) | ✅ | ⬜ | 🟠 unverified |
| Login byte layout (binary path) | ✅ | ⬜ | 🟠 unverified |
| Disconnection | ✅ | ⬜ | 🟠 unverified |
| Message-type tag enumeration | ✅ | n/a | ✅ source-of-truth |
| `SELECT 1` request layout | partial (hypothesis) | ⬜ | 🟠 needs PCAP |
| Result-set framing | partial (hypothesis) | ⬜ | 🟠 needs PCAP |
| Error response (login) | ✅ | ⬜ | 🟠 unverified |
| Error response (statement-time) | ⬜ | ⬜ | 🟠 needs both |
**Phase 0 exit gate** requires the first three rows AND the `SELECT 1` row promoted to ✅. That's the explicit work remaining: capture login + `SELECT 1` + disconnect, validate the JDBC-derived hypotheses against the bytes, fill in the gaps for the result-set framing.