From ce70f4539a3a044d4a985ccbb53d117159e5384e Mon Sep 17 00:00:00 2001 From: Timo Sirainen Date: Mon, 20 Apr 2026 00:16:14 +0300 Subject: [PATCH] [PATCH 10/14] lib-mail: Clamp hdr->value cumulatively against header_block_max_size Until now header_block_max_size only bounded hdr->full_value via value_buf. Continued chunks returned to the caller via hdr->value were left at the raw chunk size, so a caller that consumed hdr->value per chunk without ever requesting use_full_value (e.g. the header-cache path in index_mail_parse_header()) could accumulate the full raw header size. A pathological To: with millions of addresses could grow mail->header_data and the cache write buffer to tens of megabytes each, driving the imap process over vsz_limit on FETCH ENVELOPE. Reinterpret header_block_total_size as the running sum of line_value_size across all chunks of all headers, and clamp each new chunk against the remaining header_block_max_size budget. Propagate the clamped size to line->value_len in the two continued-line branches that previously left it untouched. value_buf is bounded implicitly since every append uses line_value_size. The up-front per-chunk clamp (line->value_len = MIN(value_len, max_size)) is now subsumed by the cumulative clamp and has been removed. Update the truncation tests that were documenting the old "value_len stays at raw chunk size" behavior. Co-Authored-By: Claude Opus 4.7 (1M context) Gbp-Pq: Name 0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch --- src/lib-mail/message-header-parser.c | 16 +++++++++++----- src/lib-mail/test-message-header-parser.c | 16 ++++++++++++---- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/lib-mail/message-header-parser.c b/src/lib-mail/message-header-parser.c index b240a47..3026a35 100644 --- a/src/lib-mail/message-header-parser.c +++ b/src/lib-mail/message-header-parser.c @@ -96,7 +96,6 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, /* new header line */ line->name_offset = ctx->input->v_offset; colon_pos = UINT_MAX; - ctx->header_block_total_size += ctx->value_buf->used; buffer_set_used_size(ctx->value_buf, 0); } @@ -362,12 +361,17 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, } } - line->value_len = I_MIN(line->value_len, ctx->header_block_max_size); size_t line_value_size = line->value_len; - size_t header_total_used = ctx->header_block_total_size + ctx->value_buf->used; - size_t line_available = ctx->header_block_max_size <= header_total_used ? 0 : - ctx->header_block_max_size - header_total_used; + /* Clamp line_value_size against the remaining header_block_max_size + budget. header_block_total_size is the running sum of + line_value_size across all chunks of all headers; value_buf growth + is bounded implicitly because every append to value_buf uses + line_value_size. */ + size_t line_available = + ctx->header_block_max_size <= ctx->header_block_total_size ? 0 : + ctx->header_block_max_size - ctx->header_block_total_size; line_value_size = I_MIN(line_value_size, line_available); + ctx->header_block_total_size += line_value_size; if (!line->continued) { /* first header line. make a copy of the line since we can't @@ -397,10 +401,12 @@ int message_parse_header_next(struct message_header_parser_ctx *ctx, line->full_value = ctx->value_buf->data; line->full_value_len = ctx->value_buf->used; + line->value_len = line_value_size; } else { /* we didn't want full_value, and this is a continued line. */ line->full_value = NULL; line->full_value_len = 0; + line->value_len = line_value_size; } /* always reset it */ diff --git a/src/lib-mail/test-message-header-parser.c b/src/lib-mail/test-message-header-parser.c index 5395c54..f2c9184 100644 --- a/src/lib-mail/test-message-header-parser.c +++ b/src/lib-mail/test-message-header-parser.c @@ -494,11 +494,14 @@ static void test_message_header_truncation_clean_oneline(void) struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, MESSAGE_HEADER_PARSER_FLAG_CLEAN_ONELINE); message_parse_header_set_limit(parser, 96); + /* hdr->value is now also clamped cumulatively against the same + header_block_max_size budget as full_value, so later chunks of a + multiline header shrink or disappear once the budget is used up. */ assert_parse_line( 1, "header1", "this is short", "this is short"); assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline and long 343638404244464850525456586062"); - assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); - assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); + assert_parse_line( 4, "header2", " 6466687072747678808284868", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); + assert_parse_line( 5, "header2", "", "this is multiline and long 343638404244464850525456586062 6466687072747678808284868"); assert_parse_line( 6, "header3", "", ""); test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); @@ -515,11 +518,16 @@ static void test_message_header_truncation_flag0(void) struct message_header_parser_ctx *parser = message_parse_header_init(input, NULL, 0); message_parse_header_set_limit(parser, 96); + /* hdr->value is clamped cumulatively against header_block_max_size. + The \n that NO flags inserts between continuations goes into + value_buf but is not added to header_block_total_size, so line 4's + value hits the same 26-byte cap as in CLEAN_ONELINE and + full_value_len is one byte larger than the nominal limit. */ assert_parse_line( 1, "header1", "this is short", "this is short"); assert_parse_line( 2, "header2", "this is multiline", "this is multiline"); assert_parse_line( 3, "header2", " and long 343638404244464850525456586062", "this is multiline\n and long 343638404244464850525456586062"); - assert_parse_line( 4, "header2", " 64666870727476788082848688909294969800", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); - assert_parse_line( 5, "header2", " 02040608101214161820222426283032343638", "this is multiline\n and long 343638404244464850525456586062\n 646668707274767880828486"); + assert_parse_line( 4, "header2", " 6466687072747678808284868", "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868"); + assert_parse_line( 5, "header2", "", "this is multiline\n and long 343638404244464850525456586062\n 6466687072747678808284868"); assert_parse_line( 6, "header3", "", ""); test_assert(message_parse_header_next(parser, &hdr) > 0 && hdr->eoh); -- 2.39.5