From: Vsevolod Stakhov Date: Thu, 23 Jul 2026 11:39:06 +0000 (+0100) Subject: [Fix] map: bound remote HTTP map sizes, compressed and decompressed X-Git-Tag: 4.1.3~35 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=95d51df4a3d61c2e02b0fc8174f0d10abb94348c;p=thirdparty%2Frspamd.git [Fix] map: bound remote HTTP map sizes, compressed and decompressed Remote HTTP maps had no size ceiling: the map client connections never called rspamd_http_connection_set_max_size, so a map server could feed an arbitrarily large body that is fully retained in shared memory, and the zstd path allocated the output buffer straight from the frame-advertised decompressed size, then doubled it without limit, so a small zstd bomb could balloon memory indefinitely. Add a max_map_size option (256Mb by default, 0 disables) applied as the HTTP body limit on both map client connections, reject frames advertising a larger decompressed size upfront and enforce the same ceiling in the streaming growth loop. The limit can be overridden per map with max_size in the maps { } block, like the existing timeout and keepalive knobs. --- diff --git a/src/libserver/cfg_file.h b/src/libserver/cfg_file.h index a14624ce8a..3c7f357f81 100644 --- a/src/libserver/cfg_file.h +++ b/src/libserver/cfg_file.h @@ -446,6 +446,7 @@ struct rspamd_config { double map_timeout; /**< maps watch timeout */ double map_file_watch_multiplier; /**< multiplier for watch timeout when maps are files */ char *maps_cache_dir; /**< where to save HTTP cached data */ + gsize max_map_size; /**< maximum size of a map, compressed or expanded */ double monitored_interval; /**< interval between monitored checks */ gboolean disable_monitored; /**< disable monitoring completely */ diff --git a/src/libserver/cfg_rcl.cxx b/src/libserver/cfg_rcl.cxx index e3b08c5abd..5583524c6a 100644 --- a/src/libserver/cfg_rcl.cxx +++ b/src/libserver/cfg_rcl.cxx @@ -2074,6 +2074,12 @@ rspamd_rcl_config_init(struct rspamd_config *cfg, GHashTable *skip_sections) G_STRUCT_OFFSET(struct rspamd_config, maps_cache_dir), 0, "Directory to save maps cached data (default: $DBDIR)"); + rspamd_rcl_add_default_handler(sub, + "max_map_size", + rspamd_rcl_parse_struct_integer, + G_STRUCT_OFFSET(struct rspamd_config, max_map_size), + RSPAMD_CL_FLAG_INT_SIZE, + "Maximum size of a map, compressed or expanded (256Mb by default, 0 to disable the limit)"); rspamd_rcl_add_default_handler(sub, "monitoring_watch_interval", rspamd_rcl_parse_struct_time, diff --git a/src/libserver/cfg_utils.cxx b/src/libserver/cfg_utils.cxx index 420cc70ab3..1c5ebd03c3 100644 --- a/src/libserver/cfg_utils.cxx +++ b/src/libserver/cfg_utils.cxx @@ -87,6 +87,7 @@ #define DEFAULT_RLIMIT_MAXCORE 0 #define DEFAULT_MAP_TIMEOUT 60.0 * 5 #define DEFAULT_MAP_FILE_WATCH_MULTIPLIER 1 +#define DEFAULT_MAX_MAP_SIZE (256 * 1024 * 1024) #define DEFAULT_MIN_WORD 0 #define DEFAULT_MAX_WORD 40 #define DEFAULT_WORDS_DECAY 600 @@ -291,6 +292,7 @@ rspamd_config_new(enum rspamd_config_init_flags flags) cfg->map_timeout = DEFAULT_MAP_TIMEOUT; cfg->map_file_watch_multiplier = DEFAULT_MAP_FILE_WATCH_MULTIPLIER; + cfg->max_map_size = DEFAULT_MAX_MAP_SIZE; cfg->log_level = G_LOG_LEVEL_WARNING; cfg->log_flags = RSPAMD_LOG_FLAG_DEFAULT; diff --git a/src/libserver/maps/map.c b/src/libserver/maps/map.c index bd5bb248e4..24d1dca996 100644 --- a/src/libserver/maps/map.c +++ b/src/libserver/maps/map.c @@ -586,6 +586,16 @@ rspamd_map_payload_is_zstd(const unsigned char *p, gsize len) return FALSE; } +static inline gsize +rspamd_map_effective_max_size(struct http_callback_data *cbd) +{ + if (cbd->data->max_size > 0) { + return cbd->data->max_size; + } + + return cbd->map->cfg ? cbd->map->cfg->max_map_size : 0; +} + static void rspamd_map_try_load_secretbox_key(struct rspamd_config *cfg, struct rspamd_map_backend *bk) @@ -793,9 +803,7 @@ http_map_finish(struct rspamd_http_connection *conn, ZSTD_inBuffer zin; ZSTD_outBuffer zout; gsize outlen, r; - - zstream = ZSTD_createDStream(); - ZSTD_initDStream(zstream); + gsize max_size = rspamd_map_effective_max_size(cbd); zin.pos = 0; zin.src = payload; @@ -804,6 +812,21 @@ http_map_finish(struct rspamd_http_connection *conn, if ((outlen = ZSTD_getDecompressedSize(zin.src, zin.size)) == 0) { outlen = ZSTD_DStreamOutSize(); } + else if (max_size > 0 && outlen > max_size) { + msg_err_map("%s(%s): declared decompressed size %z exceeds max_map_size %z", + cbd->bk->uri, + rspamd_inet_address_to_string_pretty(cbd->addr), + outlen, max_size); + if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) { + rspamd_explicit_memzero(payload, payload_len); + g_free(payload); + } + MAP_RELEASE(cbd->shmem_data, "shmem_data"); + goto err; + } + + zstream = ZSTD_createDStream(); + ZSTD_initDStream(zstream); final_out = g_malloc(outlen); @@ -831,7 +854,27 @@ http_map_finish(struct rspamd_http_connection *conn, if (zout.pos == zout.size) { /* We need to extend output buffer */ + if (max_size > 0 && zout.size >= max_size) { + msg_err_map("%s(%s): decompressed data exceeds max_map_size %z", + cbd->bk->uri, + rspamd_inet_address_to_string_pretty(cbd->addr), + max_size); + ZSTD_freeDStream(zstream); + g_free(final_out); + if (cbd->bk->is_encrypted && payload && payload != (unsigned char *) in) { + rspamd_explicit_memzero(payload, payload_len); + g_free(payload); + } + MAP_RELEASE(cbd->shmem_data, "shmem_data"); + goto err; + } + zout.size = zout.size * 2 + 1.0; + + if (max_size > 0 && zout.size > max_size) { + zout.size = max_size; + } + final_out = g_realloc(zout.dst, zout.size); zout.dst = final_out; } @@ -1751,6 +1794,11 @@ rspamd_map_dns_callback(struct rdns_reply *reply, void *arg) cbd->addr); if (cbd->conn != NULL) { + gsize max_size = rspamd_map_effective_max_size(cbd); + + if (max_size > 0) { + rspamd_http_connection_set_max_size(cbd->conn, max_size); + } /* Apply optional staged timeouts and keepalive tuning */ if (cbd->data->connect_timeout > 0 || cbd->data->ssl_timeout > 0 || cbd->data->write_timeout > 0 || cbd->data->read_timeout > 0) { @@ -2481,6 +2529,11 @@ check: addr); if (cbd->conn != NULL) { + gsize max_size = rspamd_map_effective_max_size(cbd); + + if (max_size > 0) { + rspamd_http_connection_set_max_size(cbd->conn, max_size); + } /* Apply optional staged timeouts and keepalive tuning */ if (cbd->data->connect_timeout > 0 || cbd->data->ssl_timeout > 0 || cbd->data->write_timeout > 0 || cbd->data->read_timeout > 0) { @@ -3437,6 +3490,9 @@ rspamd_map_parse_backend(struct rspamd_config *cfg, const char *map_line) opt = ucl_object_lookup_any(src, "max_reuse", "max-reuse", "keepalive_max_reuse", NULL); if (opt) hdata->max_reuse = (unsigned int) ucl_object_toint(opt); + opt = ucl_object_lookup_any(src, + "max_size", "max-size", NULL); + if (opt) hdata->max_size = (gsize) ucl_object_toint(opt); } } diff --git a/src/libserver/maps/map_private.h b/src/libserver/maps/map_private.h index 72a42870a1..7aa6e665e3 100644 --- a/src/libserver/maps/map_private.h +++ b/src/libserver/maps/map_private.h @@ -127,6 +127,8 @@ struct http_map_data { double connection_ttl; double idle_timeout; unsigned int max_reuse; + /* Optional per-map size limit; 0 means use cfg->max_map_size */ + gsize max_size; }; struct static_map_data { @@ -201,11 +203,11 @@ struct rspamd_map { double poll_timeout; time_t next_check; bool active_http; - bool non_trivial; /* E.g. has http backends in active mode */ - bool file_only; /* No HTTP backends found */ - bool static_only; /* No need to check */ - bool no_file_read; /* Do not read files, pass filename to consumer */ - bool seen; /* This map has already been watched or pre-loaded */ + bool non_trivial; /* E.g. has http backends in active mode */ + bool file_only; /* No HTTP backends found */ + bool static_only; /* No need to check */ + bool no_file_read; /* Do not read files, pass filename to consumer */ + bool seen; /* This map has already been watched or pre-loaded */ gsize no_file_read_offset; /* Payload offset when consumer mmaps the file (0 for file, 4096 for HTTP cache) */ /* Shared lock for temporary disabling of map reading (e.g. when this map is written by UI) */ struct rspamd_map_shared_data *shared;