/src/haproxy/fuzz_h1_htx.c
Line | Count | Source |
1 | | /* |
2 | | * Copyright 2026 Google LLC |
3 | | * |
4 | | * Licensed under the Apache License, Version 2.0 (the "License"); |
5 | | * you may not use this file except in compliance with the License. |
6 | | * You may obtain a copy of the License at |
7 | | * |
8 | | * http://www.apache.org/licenses/LICENSE-2.0 |
9 | | * |
10 | | * Unless required by applicable law or agreed to in writing, software |
11 | | * distributed under the License is distributed on an "AS IS" BASIS, |
12 | | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
13 | | * See the License for the specific language governing permissions and |
14 | | * limitations under the License. |
15 | | */ |
16 | | |
17 | | /* |
18 | | * Fuzzer for HAProxy's HTTP/1-to-HTX conversion pipeline. |
19 | | * |
20 | | * This targets h1_htx.c which converts raw HTTP/1 wire data into HAProxy's |
21 | | * internal HTX (HTTP Transaction) representation. Every HTTP/1 request from |
22 | | * an untrusted client passes through this code path: |
23 | | * |
24 | | * h1_parse_msg_hdrs() - parses request/response line + headers into HTX |
25 | | * h1_parse_msg_data() - parses body (content-length / chunked / EOF) |
26 | | * h1_parse_msg_tlrs() - parses chunked-encoding trailers |
27 | | * |
28 | | * This pipeline exercises: |
29 | | * - h1_htx.c (header post-processing, HTX block construction) |
30 | | * - htx.c (HTX buffer management, block allocation) |
31 | | * - h1.c (low-level H1 state machine via h1_headers_to_hdr_list) |
32 | | * |
33 | | * The first byte of fuzz input selects request vs response mode. |
34 | | * The remaining bytes are treated as a raw HTTP/1 message (headers + body). |
35 | | */ |
36 | | |
37 | | #include <haproxy/h1.h> |
38 | | #include <haproxy/h1_htx.h> |
39 | | #include <haproxy/htx.h> |
40 | | #include <haproxy/http-hdr.h> |
41 | | #include <haproxy/global.h> |
42 | | #include <haproxy/buf.h> |
43 | | #include <haproxy/chunk.h> |
44 | | |
45 | | #include <stdint.h> |
46 | | #include <string.h> |
47 | | #include <stdlib.h> |
48 | | |
49 | 1 | #define MAX_HDR_NUM 101 |
50 | 17.8k | #define HTX_BUF_SIZE 65536 |
51 | 2 | #define TRASH_BUF_SIZE 65536 |
52 | | |
53 | | static int fuzz_initialized = 0; |
54 | | static char *htx_area = NULL; |
55 | | |
56 | 5.42k | int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) { |
57 | 5.42k | struct h1m h1m; |
58 | 5.42k | union h1_sl h1sl; |
59 | 5.42k | struct buffer srcbuf; |
60 | 5.42k | struct buffer htxbuf; |
61 | 5.42k | struct htx *htx; |
62 | 5.42k | char *input; |
63 | 5.42k | int ret; |
64 | 5.42k | size_t ofs; |
65 | | |
66 | 5.42k | if (size < 2) |
67 | 1 | return 0; |
68 | | |
69 | | /* One-time init: replicate the trash buffer setup that haproxy normally |
70 | | * performs at startup (alloc_early_trash + alloc_trash_buffers_per_thread). |
71 | | * init_trash_buffers() is the public entry point that creates the pool and |
72 | | * allocates the two alternating trash buffers (trash_buf1/trash_buf2) plus |
73 | | * the scratch buffer (trash) used throughout haproxy. The first call with |
74 | | * first=1 mirrors alloc_early_trash(); the second with first=0 also creates |
75 | | * the large and small trash pools and their per-thread buffers. */ |
76 | 5.42k | if (!fuzz_initialized) { |
77 | 1 | htx_area = malloc(HTX_BUF_SIZE); |
78 | 1 | if (!htx_area) |
79 | 0 | return 0; |
80 | 1 | global.tune.bufsize = TRASH_BUF_SIZE; |
81 | 1 | global.tune.bufsize_large = TRASH_BUF_SIZE * 2; |
82 | 1 | global.tune.bufsize_small = 1024; |
83 | 1 | global.tune.max_http_hdr = MAX_HDR_NUM; |
84 | 1 | if (!init_trash_buffers(1)) |
85 | 0 | return 0; |
86 | 1 | if (!init_trash_buffers(0)) |
87 | 0 | return 0; |
88 | 1 | fuzz_initialized = 1; |
89 | 1 | } |
90 | | |
91 | | /* First byte: bit 0 selects request vs response */ |
92 | 5.42k | int parse_response = data[0] & 1; |
93 | 5.42k | data++; |
94 | 5.42k | size--; |
95 | | |
96 | | /* We need a mutable copy for the source buffer. |
97 | | * IMPORTANT: HAProxy uses ring buffers where b_tail wraps around when |
98 | | * data == capacity. We must allocate capacity > data to prevent |
99 | | * b_tail from wrapping to the start (which would give zero-length input |
100 | | * to the parser). */ |
101 | 5.42k | input = (char *)malloc(size + 1); |
102 | 5.42k | if (!input) |
103 | 0 | return 0; |
104 | 5.42k | memcpy(input, data, size); |
105 | | |
106 | | /* Set up source buffer: capacity = size+1 so b_tail doesn't wrap */ |
107 | 5.42k | srcbuf = b_make(input, size + 1, 0, size); |
108 | | |
109 | | /* Set up destination HTX buffer */ |
110 | 5.42k | memset(htx_area, 0, HTX_BUF_SIZE); |
111 | 5.42k | htxbuf = b_make(htx_area, HTX_BUF_SIZE, 0, 0); |
112 | 5.42k | htx = htx_from_buf(&htxbuf); |
113 | | |
114 | | /* Initialize the H1 message parser */ |
115 | 5.42k | if (parse_response) |
116 | 2.95k | h1m_init_res(&h1m); |
117 | 2.47k | else |
118 | 2.47k | h1m_init_req(&h1m); |
119 | | |
120 | | /* Phase 1: Parse headers into HTX */ |
121 | 5.42k | ret = h1_parse_msg_hdrs(&h1m, &h1sl, htx, &srcbuf, 0, HTX_BUF_SIZE); |
122 | | |
123 | 5.42k | if (ret > 0) { |
124 | 2.55k | ofs = (size_t)ret; |
125 | | |
126 | | /* Phase 2: Parse body data */ |
127 | 2.55k | if (ofs < size && h1m.state < H1_MSG_DONE) { |
128 | 1.43k | h1_parse_msg_data(&h1m, &htx, &srcbuf, ofs, HTX_BUF_SIZE, &htxbuf); |
129 | 1.43k | } |
130 | | |
131 | | /* Phase 3: Parse trailers (for chunked encoding) */ |
132 | 2.55k | if (h1m.state == H1_MSG_TRAILERS) { |
133 | 95 | h1_parse_msg_tlrs(&h1m, htx, &srcbuf, ofs, HTX_BUF_SIZE); |
134 | 95 | } |
135 | 2.55k | } |
136 | | |
137 | 5.42k | free(input); |
138 | 5.42k | return 0; |
139 | 5.42k | } |