droid
The Startup Handshake
Wire Protocol · Stage 2 of 11

The Startup Handshake

Speak PostgreSQL's protocol from the first byte: frame messages by their length, turn down encryption politely, and greet the client the way every driver expects

With no --wire, speak the PostgreSQL protocol version 3.0: cut the bytes of each connection into length-prefixed messages however they arrive, answer an SSLRequest or a GSSENCRequest with the byte N, greet a StartupMessage with AuthenticationOk, six ParameterStatus messages, BackendKeyData carrying the session number and ReadyForQuery, refuse other protocol versions and broken first messages without disturbing anyone else, and end the session on Terminate.

Run the tests on your machine

This part needs a network, which the platform's test sandbox does not have, so its tests run on your computer. Download the test kit (it needs only Python 3) and your project, build your program, and run:

python3 run.py --stage pg-startup --bin ./c-db
Download the test kit

Sign in to download the code you wrote here.

Concepts in this stage
  • Length-prefixed framing: a type byte, a 4-byte length that counts itself, then the body
  • The first message is special: no type byte, and a code in place of a protocol version can turn it into a request for encryption
  • Refusing TLS is part of the protocol: one byte, N, and the client carries on in plain text
  • The greeting is a fixed sequence a driver waits for: authentication, parameters, a cancel key, then ready
  • ReadyForQuery's status byte is how the client knows whose turn it is
  • A broken client is that client's problem: close its connection, keep serving everyone else
Where this stage sits in the engine
Talking to the user
REPLSessionsPrinter
Server
Wire protocol
Understanding SQL
LexerParserValidator
Answering a query
PlannerExecutor
Keeping changes safe
TransactionsWrite-ahead logUndo logLock managerRead views
Storing rows
Row formatSlotted pageB+Tree
Reaching the disk
PagerDatabase file
this stagebuilt alreadystill to comeA statement enters at the top; rows come back up.

Why this stage exists

Your server answers nc now. Nothing else can talk to it, because every database client speaks some database's own protocol. Here is psql, PostgreSQL's terminal client, pointed at your server from the last stage:

shell
$psql -h 127.0.0.1 -p 5433 -U droid droid
psql: error: connection to server at "127.0.0.1", port 5433 failed: received invalid response to SSL negotiation: W

psql opened with eight bytes asking for TLS. Your server read them as the start of a line and sent back the welcome line, Welcome to droid!, whose first byte is W. From this stage on, your server speaks PostgreSQL's protocol, version 3.0, the one every PostgreSQL driver in every language speaks: libpq, JDBC, psycopg, node-postgres, pgx. Speaking it is a bigger payoff than it looks. Those drivers, and the tools built on them, can then talk to your database. This stage does the first exchange. The next one sends queries.

The line protocol stays behind --wire text. Every test of the last stage passes that flag and keeps passing.

Messages have lengths, not newlines

Text protocols end a message with a newline, which works until a value contains one. PostgreSQL's protocol instead puts the size of each message in front of it. Every message, both ways, is a type byte, then a 4-byte big-endian length, then a body. The length counts itself and the body, not the type byte:

0
52
00
00
00
08
00
00
00
00
  • typebytes 0–0 (1 bytes) · 'R': an authentication message
  • lengthbytes 1–4 (4 bytes) · 8: these 4 bytes and the 4 of the body
  • codebytes 5–8 (4 bytes) · 0: authentication succeeded
AuthenticationOk, the first message of every greeting. A client reads 5 bytes, learns that 4 more follow, reads them, and knows exactly where the next message starts.

Everything you learned about lines still holds. One read may hold half a message or several. The input buffer and the rule stay the same: handle a message once all of it has arrived, and keep the rest. Only "where does it end" changes. Before, the answer was "at the next \n". Now it is "length bytes after the type".

There is one more thing to check: the length comes from the client. A length of 3 cannot even hold itself, and a length of 10 MB for a startup message is either a bug or an attack. Check it before waiting for the body, then close that connection and nothing else.

The first message is different

The very first message from a client has no type byte, because the client cannot know yet what the server speaks. It is a length, then a 4-byte number, then a body:

0
00
00
00
08
04
d2
16
2f
  • lengthbytes 0–3 (4 bytes) · 8: the whole request
  • codebytes 4–7 (4 bytes) · 80877103: 1234 in the high 16 bits, 5679 in the low
The SSLRequest psql sent first. The code is chosen so that it can never be a real protocol version.

If the number is one of the two special codes, the client asks for encryption: TLS (80877103) or GSS (80877104). This track does not encrypt, so the answer is a single byte, N, with no type and no length. The client then sends a new first message on the same connection, which is the StartupMessage. Otherwise the number is a protocol version: 3 in the high 16 bits and 0 in the low, so 196608. The body is a list of parameter names and values, ended by an empty name:

0
00
00
00
23
00
03
00
00
75
73
65
72
12
00
64
72
6f
69
64
00
64
61
74
61
62
24
61
73
65
00
64
72
6f
69
64
00
00
  • lengthbytes 0–3 (4 bytes) · 35: the whole message
  • protocolbytes 4–7 (4 bytes) · 196608 = 3.0
  • "user"bytes 8–12 (5 bytes) · a name, with its zero byte
  • "droid"bytes 13–18 (6 bytes) · its value
  • "database"bytes 19–27 (9 bytes) · another name
  • "droid"bytes 28–33 (6 bytes) · its value
  • endbytes 34–34 (1 bytes) · the empty name that ends the list
A StartupMessage for user droid and database droid. Real clients send more parameters, such as application_name and client_encoding; droid reads them and changes nothing.

The greeting

A real PostgreSQL would now ask for a password. Droid trusts every client, so it goes straight to the greeting. These are four kinds of message, in an order every driver waits for:

Drawing the diagram…
  • ParameterStatus messages tell the driver how to talk to you. client_encoding says strings are UTF-8. standard_conforming_strings says a backslash inside quotes is an ordinary character. server_version lets a driver decide which features to use.
  • BackendKeyData gives the client a ticket for later. To cancel a long statement, a client opens a second connection and sends this process id and secret key. That is a later stage, but the key is handed out now. In PostgreSQL the process id is the backend process's real id. In droid it is the session number, which plays the same part.
  • ReadyForQuery ends every exchange in this protocol, not just the greeting. Its last byte says the session's state: I for idle, T inside a transaction, E inside a failed one. The client sends nothing more until it sees one.

When the first message is wrong

A client that asks for protocol 2.0 or 4.0 gets an ErrorResponse and is disconnected. An ErrorResponse is a list of fields, each a one-letter code and a string, ended by a zero byte. The severity FATAL means the connection is ending. The SQLSTATE 0A000 is PostgreSQL's code for "feature not supported", and the tests read it. A startup message whose parameters do not end with the final zero byte, the empty name after the last pair, is 08P01, a protocol violation. Bad lengths get no reply at all, because nothing about the message can be trusted. In every case it is only that connection that ends. A server that crashes, or stops serving others, because of one client's bad bytes is broken whatever the client did.

The algorithm in words

1

Cutting the stream into messages

The same idea as lines in the last stage, with a length instead of a newline. It runs on each connection's input buffer after every read.

  1. 1.Before the startup is done, a message has no type byte: when the buffer holds at least 4 bytes, read the length from them. Afterwards a message is a type byte and then the length: wait for 5 bytes.
  2. 2.Check the length before waiting for the body: below 8 or above 10000 for a first message (below 4 for any other) means the client is broken. Close the connection.
  3. 3.When the buffer holds the whole message, take it out and handle it. Leave the rest in the buffer: it is the start of the next message.
  4. 4.Invariant: no message is handled before its last byte has arrived, and no byte is handled twice.
2

The first message

One 8-byte prefix decides what the client wants.

  1. 1.Read the 32-bit number after the length. 80877103 is an SSLRequest and 80877104 a GSSENCRequest: send the single byte N, with no type and no length, and wait for another first message.
  2. 2.Otherwise it is a protocol version: the major number is the high 16 bits. If it is not 3, send an ErrorResponse with severity FATAL and SQLSTATE 0A000, then close the connection once it is sent.
  3. 3.Read the parameters: name and value strings, one after the other, until an empty name, the single zero byte that ends the list. If the message ends before that byte, after a name with no value or after a complete pair, or if bytes are left after it, send FATAL 08P01 and close.
  4. 4.The parameters change nothing in droid: there is one database, and every user is trusted.
3

The greeting

Four kinds of message, in this order, all appended to the connection's output buffer at once.

  1. 1.AuthenticationOk: R, length 8, the integer 0.
  2. 2.One ParameterStatus (S, a name, a value) for each of server_version, server_encoding, client_encoding, DateStyle, integer_datetimes and standard_conforming_strings, with the values of the contract.
  3. 3.BackendKeyData: K, length 12, the session number as the process id, then a secret key you choose and remember for this connection.
  4. 4.ReadyForQuery: Z, length 5, the byte I: the session is idle and waits for the client.
  5. 5.From now on, the messages from this client have a type byte. In this stage only X, Terminate, is defined: close the connection and end the session as when a client leaves.

Output contract for this stage

These are the exact messages the tests check. The full definition is on the output contract page, section 94.

Client sendsServer sends
SSLRequest (00 00 00 08 04 d2 16 2f)the byte N
GSSENCRequest (00 00 00 08 04 d2 16 30)the byte N
StartupMessage, protocol 3.0R 0; S server_version 16.0 (droid), server_encoding UTF8, client_encoding UTF8, DateStyle ISO, MDY, integer_datetimes on, standard_conforming_strings on (any order); K session number, secret key; Z I
StartupMessage, protocol 4.0 (any major but 3)ErrorResponse S and V FATAL, C 0A000; the connection closes
a StartupMessage whose parameters lack their final zero byte (user\0, user\0droid\0)ErrorResponse FATAL, 08P01; the connection closes
a first message with a length below 8 or above 10000nothing; the connection closes
Terminate (58 00 00 00 04)nothing; the connection closes, the session ends

What you should see when done

psql gets a step further than before. It connects, reads the greeting and shows its prompt. Answering what you type there is the next stage's work:

shell
$psql -h 127.0.0.1 -p 5433 -U droid droid
psql (16.4, server 16.0)
Type "help" for help.
 
droid=>

And the kit:

shell
$python3 run.py --stage pg-startup --bin ./c-db
PASS server/server-listen:listens-and-greets
...
PASS server/server-listen:shutdown
PASS server/pg-startup:greeting-sequence
PASS server/pg-startup:golden-bytes
PASS server/pg-startup:ssl-request-refused
PASS server/pg-startup:gss-request-refused
PASS server/pg-startup:byte-at-a-time
PASS server/pg-startup:unknown-protocol-rejected
PASS server/pg-startup:bad-startup-packets
PASS server/pg-startup:terminate-closes
PASS server/pg-startup:wire-pg-flag
================================================================================
summary: passed=23 failed=0 total=23

Common bugs in this stage

  • Little-endian lengths. Every integer in this protocol is big-endian: the most significant byte first. On x86 and ARM a plain memcpy of an int gives the wrong order.
  • Counting the type byte in the length. The length counts itself and the body. AuthenticationOk is 9 bytes on the wire with a length of 8.
  • Sending N with a type and a length. The answer to an SSLRequest is one bare byte.
  • Waiting for a 10 MB body, or for 8 bytes that never come. Check the length as soon as its 4 bytes arrive: a length of 3 must close the connection before anything more is sent.
  • Checking only the last byte. user\0droid\0 ends with a zero and is still broken: the list needs one more zero, the empty name, after its last pair.
  • Reading the first message as a typed message. Only the messages after the startup start with a type byte. A connection needs a flag saying whether its startup is done.
  • A fixed process id. Every connection's BackendKeyData carries its own session number. Two clients with the same id could not cancel each other's statements later.

Language notes

The page above is the same for every language. These are the only language-specific hints.

  • C: read a big-endian length with ntohl on four bytes copied out of the buffer (never by casting a pointer into the buffer: it may not be aligned). Write one with htonl. A small put_u32 / put_cstr pair that appends to an output buffer keeps the message builders short.
  • C++: the same ntohl and htonl, or build the value from the bytes with shifts. A std::string output buffer and a helper that writes the type, leaves 4 bytes, appends the body, then fills in the length, makes every message builder three lines long.
  • Rust: u32::from_be_bytes(buf[0..4].try_into()?) and n.to_be_bytes(). A Vec<u8> output buffer, and extend_from_slice.
  • Zig: std.mem.readInt(u32, buf[0..4], .big) and std.mem.writeInt(u32, &bytes, n, .big).

Checklist

One line per thing the tests check. The tags name the test cases.

  • A StartupMessage gets AuthenticationOk, the six parameters, one BackendKeyData whose process id is the session number, and ReadyForQuery I
    greeting-sequence
  • AuthenticationOk and idle ReadyForQuery are exactly 52 00 00 00 08 00 00 00 00 and 5A 00 00 00 05 49
    golden-bytes
  • An SSLRequest gets the single byte N, and startup then works on the same connection
    ssl-request-refused
  • A GSSENCRequest gets N too
    gss-request-refused
  • A StartupMessage sent one byte per write gets the same greeting
    byte-at-a-time
  • A protocol other than 3.0 gets FATAL 0A000, and the connection closes
    unknown-protocol-rejected
  • An impossible length closes that connection without a reply, a StartupMessage whose parameters lack their final zero byte gets FATAL 08P01, and the next client is served as usual
    bad-startup-packets
  • Terminate closes the connection and ends the session
    terminate-closes
  • --wire pg is the same protocol, said explicitly
    wire-pg-flag