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

20 KiB
Raw Blame History

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:

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[]):

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:

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.