From: Vsevolod Stakhov Date: Sun, 8 Feb 2026 14:20:45 +0000 (+0000) Subject: [Fix] protocol: Handle shared memory and whole-body compression for v3 proxy path X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=0fae86945aef469da14c642d5c30a528209e5de9;p=thirdparty%2Frspamd.git [Fix] protocol: Handle shared memory and whole-body compression for v3 proxy path When the proxy forwards /checkv3 requests to a local upstream, it uses shared memory (GET + Shm headers) instead of sending the body inline. The v3 request handler only read from chunk/len parameters which are empty in this case. Add Shm/Shm-Offset/Shm-Length header handling to read the body from the shared memory segment. Additionally, the proxy may compress the entire response body with zstd before forwarding to the client. The v3 client finish handler parsed multipart directly from the compressed body_buf. Add whole-body decompression (matching the v2 handler) before multipart parsing. --- diff --git a/src/client/rspamdclient.c b/src/client/rspamdclient.c index 10647a55e6..3573b5c122 100644 --- a/src/client/rspamdclient.c +++ b/src/client/rspamdclient.c @@ -533,6 +533,60 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, return 0; } + /* Decompress whole-body compression (proxy may compress the entire response) */ + unsigned char *whole_body_decompressed = NULL; + const char *resp_body = msg->body_buf.begin; + gsize resp_body_len = msg->body_buf.len; + + const rspamd_ftok_t *comp_tok = rspamd_http_message_find_header(msg, COMPRESSION_HEADER); + if (comp_tok) { + rspamd_ftok_t zstd_tok; + zstd_tok.begin = "zstd"; + zstd_tok.len = 4; + + if (rspamd_ftok_casecmp(comp_tok, &zstd_tok) == 0) { + ZSTD_DStream *zstream = ZSTD_createDStream(); + ZSTD_initDStream(zstream); + ZSTD_inBuffer zin; + zin.src = msg->body_buf.begin; + zin.size = msg->body_buf.len; + zin.pos = 0; + gsize outlen = ZSTD_getDecompressedSize(zin.src, zin.size); + if (outlen == 0) { + outlen = ZSTD_DStreamOutSize(); + } + whole_body_decompressed = g_malloc(outlen); + ZSTD_outBuffer zout; + zout.dst = whole_body_decompressed; + zout.size = outlen; + zout.pos = 0; + + while (zin.pos < zin.size) { + gsize r = ZSTD_decompressStream(zstream, &zout, &zin); + if (ZSTD_isError(r)) { + err = g_error_new(RCLIENT_ERROR, 500, + "Whole-body decompression error: %s", + ZSTD_getErrorName(r)); + req->cb(c, msg, c->server_name->str, NULL, + req->input, req->ud, c->start_time, + c->send_time, NULL, 0, err); + g_error_free(err); + g_free(whole_body_decompressed); + ZSTD_freeDStream(zstream); + return 0; + } + if (zout.pos == zout.size) { + zout.size *= 2; + whole_body_decompressed = g_realloc(zout.dst, zout.size); + zout.dst = whole_body_decompressed; + } + } + ZSTD_freeDStream(zstream); + resp_body = (const char *) whole_body_decompressed; + resp_body_len = zout.pos; + } + } + /* Check if response is multipart/mixed */ const rspamd_ftok_t *ct = rspamd_http_message_find_header(msg, "Content-Type"); @@ -546,7 +600,7 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, if (parsed_ct && parsed_ct->boundary.len > 0) { struct rspamd_multipart_form_c *form = rspamd_multipart_form_parse( - msg->body_buf.begin, msg->body_buf.len, + resp_body, resp_body_len, parsed_ct->boundary.begin, parsed_ct->boundary.len); if (form) { @@ -585,6 +639,7 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, req->input, req->ud, c->start_time, c->send_time, NULL, 0, err); g_error_free(err); + g_free(whole_body_decompressed); return 0; } if (zout.pos == zout.size) { @@ -671,6 +726,7 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, c->send_time, body, bodylen, err); g_error_free(err); g_free(body_decompressed); + g_free(whole_body_decompressed); return 0; } @@ -712,8 +768,8 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, } else { /* Fallback: non-multipart response, handle like v2 */ - start = msg->body_buf.begin; - len = msg->body_buf.len; + start = resp_body; + len = resp_body_len; parser = ucl_parser_new(UCL_PARSER_SAFE_FLAGS); if (!ucl_parser_add_chunk(parser, (const unsigned char *) start, len)) { @@ -724,6 +780,7 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, req->input, req->ud, c->start_time, c->send_time, NULL, 0, err); g_error_free(err); + g_free(whole_body_decompressed); return 0; } @@ -734,6 +791,7 @@ rspamd_client_v3_finish_handler(struct rspamd_http_connection *conn, ucl_parser_free(parser); } + g_free(whole_body_decompressed); return 0; } diff --git a/src/libserver/protocol.c b/src/libserver/protocol.c index fda9b9a243..a50f0aedae 100644 --- a/src/libserver/protocol.c +++ b/src/libserver/protocol.c @@ -2385,6 +2385,21 @@ rspamd_protocol_handle_metadata(struct rspamd_task *task, return TRUE; } +/* Shared memory mapping cleanup for v3 request body */ +struct rspamd_v3_shm_map { + gpointer begin; + gulong len; + int fd; +}; + +static void +rspamd_v3_shm_unmapper(gpointer ud) +{ + struct rspamd_v3_shm_map *m = ud; + munmap(m->begin, m->len); + close(m->fd); +} + /* * Handle v3 multipart/form-data request. */ @@ -2395,6 +2410,108 @@ rspamd_protocol_handle_v3_request(struct rspamd_task *task, { const char *boundary = NULL; gsize boundary_len = 0; + const char *body_data = chunk; + gsize body_len = len; + + /* + * When the proxy forwards to a local upstream, it uses shared memory + * (GET + Shm/Shm-Offset/Shm-Length headers) instead of sending the + * body inline. In that case chunk/len are empty, so we must read + * the body from the shared memory segment referenced by the headers. + */ + if (body_len == 0 || body_data == NULL) { + const rspamd_ftok_t *shm_tok = rspamd_http_message_find_header(msg, "Shm"); + + if (shm_tok) { + char filepath[PATH_MAX], *fp; + int fd; + struct stat st; + gulong offset = 0, shmem_size = 0; + + rspamd_strlcpy(filepath, shm_tok->begin, + MIN(sizeof(filepath), shm_tok->len + 1)); + rspamd_url_decode(filepath, filepath, strlen(filepath) + 1); + + int flen = strlen(filepath); + if (filepath[0] == '"' && flen > 2) { + fp = &filepath[1]; + fp[flen - 2] = '\0'; + } + else { + fp = &filepath[0]; + } + +#ifdef HAVE_SANE_SHMEM + fd = shm_open(fp, O_RDONLY, 00600); +#else + fd = open(fp, O_RDONLY, 00600); +#endif + if (fd == -1) { + g_set_error(&task->err, rspamd_protocol_quark(), 500, + "cannot open shm segment (%s): %s", fp, strerror(errno)); + return FALSE; + } + + if (fstat(fd, &st) == -1) { + g_set_error(&task->err, rspamd_protocol_quark(), 500, + "cannot stat shm segment (%s): %s", fp, strerror(errno)); + close(fd); + return FALSE; + } + + gpointer map = mmap(NULL, st.st_size, PROT_READ, MAP_SHARED, fd, 0); + if (map == MAP_FAILED) { + g_set_error(&task->err, rspamd_protocol_quark(), 500, + "cannot mmap shm segment (%s): %s", fp, strerror(errno)); + close(fd); + return FALSE; + } + + const rspamd_ftok_t *off_tok = rspamd_http_message_find_header(msg, "Shm-Offset"); + if (off_tok) { + rspamd_strtoul(off_tok->begin, off_tok->len, &offset); + if (offset > (gulong) st.st_size) { + munmap(map, st.st_size); + close(fd); + g_set_error(&task->err, rspamd_protocol_quark(), 500, + "invalid shm offset"); + return FALSE; + } + } + + shmem_size = st.st_size; + const rspamd_ftok_t *len_tok = rspamd_http_message_find_header(msg, "Shm-Length"); + if (len_tok) { + rspamd_strtoul(len_tok->begin, len_tok->len, &shmem_size); + if (shmem_size > (gulong) st.st_size) { + munmap(map, st.st_size); + close(fd); + g_set_error(&task->err, rspamd_protocol_quark(), 500, + "invalid shm length"); + return FALSE; + } + } + + body_data = ((const char *) map) + offset; + body_len = shmem_size; + + /* Register cleanup for the mapping */ + struct rspamd_v3_shm_map *m = rspamd_mempool_alloc(task->task_pool, sizeof(*m)); + m->begin = map; + m->len = st.st_size; + m->fd = fd; + rspamd_mempool_add_destructor(task->task_pool, + rspamd_v3_shm_unmapper, m); + + msg_info_task("v3 request: loaded body from shm %s (%ul size, %ul offset)", + fp, (unsigned long) shmem_size, (unsigned long) offset); + } + else if (msg->body_buf.len > 0) { + /* Fallback: use the HTTP message body buffer directly */ + body_data = msg->body_buf.begin; + body_len = msg->body_buf.len; + } + } /* Extract boundary from HTTP Content-Type header */ const rspamd_ftok_t *ct_hdr = rspamd_http_message_find_header(msg, "Content-Type"); @@ -2419,7 +2536,7 @@ rspamd_protocol_handle_v3_request(struct rspamd_task *task, /* Parse multipart body */ struct rspamd_multipart_form_c *form = rspamd_multipart_form_parse( - chunk, len, boundary, boundary_len); + body_data, body_len, boundary, boundary_len); if (!form) { g_set_error(&task->err, rspamd_protocol_quark(), 400,