]> dgit.raspbian.org Git - dovecot.git/commitdiff
[PATCH 10/14] lib-mail: Clamp hdr->value cumulatively against header_block_max_size
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Sun, 19 Apr 2026 21:16:14 +0000 (00:16 +0300)
committerNoah Meyerhans <noahm@debian.org>
Wed, 16 Sep 2026 19:06:35 +0000 (15:06 -0400)
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) <noreply@anthropic.com>
Gbp-Pq: Name 0010-lib-mail-Clamp-hdr-value-cumulatively-against-header.patch

src/lib-mail/message-header-parser.c
src/lib-mail/test-message-header-parser.c

index b240a478843a391cc4726f255011d905994e0160..3026a35f293fa23a035cb77a8af101859b20699a 100644 (file)
@@ -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 */
index 5395c54ed624c05d50d0244049fc8e3732e5ee4f..f2c9184e66607f75a2de2d711650bffed0ab95b3 100644 (file)
@@ -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);