From 9e4f85917d5d4b9556dd7be73d2b5df2943e33d1 Mon Sep 17 00:00:00 2001 From: Evgen Belozerov Date: Thu, 23 Jul 2026 13:09:10 +0200 Subject: [PATCH] fix: harden Q frame handling --- q.c | 14 ++++++++------ q_server.c | 8 ++++++-- test/run.sh | 42 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/q.c b/q.c index 4554ed3..d6bdbed 100644 --- a/q.c +++ b/q.c @@ -100,6 +100,7 @@ static inline size_t ray_scalar_elem_size(int8_t type) { #define Q_ERR (-128) #define Q_MSG_SYNC 1 +#define Q_MAX_BODY ((int64_t)256 << 20) typedef struct { uint8_t endianness; @@ -989,10 +990,9 @@ static int q_decompress(const uint8_t *src, int64_t src_len, uint8_t **out_buf, } n = src[d++]; /* A back-reference expands to 2 literal + n copied bytes at result[s]. - * A malformed or hostile frame can declare a tiny out_size yet expand - * past it, so reject that here instead of writing past the allocation. - * (r < s <= out_size, so the source reads below stay in bounds.) */ - if (s + 2 + n > out_size) { + * Reject frames that point before a complete prior byte-pair exists or + * that would expand past the declared uncompressed size. */ + if (r + 1 >= s || s + 2 + n > out_size) { free(result); return -1; } @@ -1144,8 +1144,10 @@ int q_exchange(int fd, const uint8_t *req, int64_t req_len, uint8_t **resp, return -1; } int64_t body_len = (int64_t)header.size - (int64_t)sizeof header; - if (body_len <= 0) { - q_set_err(err, errlen, "q: empty response body"); + if (body_len <= 0 || body_len > Q_MAX_BODY) { + q_set_err(err, errlen, + body_len <= 0 ? "q: empty response body" + : "q: response body too large"); return -1; } uint8_t *body = (uint8_t *)malloc((size_t)body_len); diff --git a/q_server.c b/q_server.c index 63df8e3..36bcc94 100644 --- a/q_server.c +++ b/q_server.c @@ -207,12 +207,16 @@ static ray_t *q_read_body(ray_poll_t *poll, ray_selector_t *sel) { * buffer is free to reuse the moment it returns. */ q_header_t hdr = cd->hdr; int64_t id = sel->id; - ray_t *req = q_decode(sel->rx.buf->data, body, hdr.compressed, NULL, 0); + char err[128] = {0}; + ray_t *req = + q_decode(sel->rx.buf->data, body, hdr.compressed, err, sizeof err); sel->rx.read_fn = q_read_header; ray_poll_rx_request(poll, sel, (int64_t)sizeof(q_header_t)); - ray_t *result = eval_request(req); + ray_t *result = req ? eval_request(req) + : ray_error("q server: malformed request", "%s", + err[0] ? err : "q server: decode failed"); if (req) ray_release(req); diff --git a/test/run.sh b/test/run.sh index e56d067..2ad15ec 100755 --- a/test/run.sh +++ b/test/run.sh @@ -58,6 +58,22 @@ find_q() { [[ -n "$QBIN" && -x "$QBIN" ]] || QBIN="" } +q_home_for() { + local qbin="$1" qdir parent + if command -v python3 >/dev/null 2>&1; then + qbin="$(python3 -c 'import os,sys; print(os.path.realpath(sys.argv[1]))' "$qbin")" + fi + qdir="$(cd "$(dirname "$qbin")" && pwd)" + parent="$(cd "$qdir/.." && pwd)" + if [[ -f "$qdir/q.k" ]]; then + printf '%s\n' "$qdir" + elif [[ -f "$parent/q.k" ]]; then + printf '%s\n' "$parent" + else + printf '%s\n' "$parent" + fi +} + PIDS=() FDS=() WORK="$(mktemp -d)" @@ -77,6 +93,28 @@ wait_for_port "$HOST" "$SERVERPORT" || { echo "driver --serve did not come up on echo "server up on $HOST:$SERVERPORT; running server round-trip tests..." "$DRIVER" --host "$HOST" --port "$SERVERPORT" "$RFL_DIR"/server/*.rfl +echo "checking malformed Q frame handling..." +python3 - "$HOST" "$SERVERPORT" <<'PY' +import socket +import struct +import sys + +host, port = sys.argv[1], int(sys.argv[2]) +with socket.create_connection((host, port), 1.0) as s: + s.sendall(bytes([3, 0])) + if s.recv(1) != bytes([3]): + raise SystemExit("bad Q handshake response") + s.sendall(struct.pack("