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.
20 KiB
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/ printable4.50.JC10, build 146 from 2023-03-07; SHA256 inJDBC_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 (seeJDBC_NOTES.md) - 🔵 PCAP: observed in
docs/CAPTURES/<file>.pcapat 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 offsetKeepAlive(socKeepAlive)— opt-in viaIFX_SOC_KEEPALIVEproperty- Optional SSL wrap via
SSLSocketFactory. Phase 6+; skip for MVP. - Buffered streams:
BufferedInputStream(in, 4096)/BufferedOutputStream(out, 4096)over the raw socket streams. TheIfxDataInputStream/IfxDataOutputStreamwrap 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=130AFTER 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_COMMANDpayload — 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 betweenCap_3and 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.