From 2f8c0b98be667e4ce6c5de193139634d1f6743fc Mon Sep 17 00:00:00 2001 From: Paolo Insogna Date: Fri, 13 Nov 2026 12:19:52 +0100 Subject: [PATCH] fix: Added regression coverage for relaxed header values. Assisted-By: OpenAI:GPT-5.6-Sol Signed-off-by: Paolo Insogna --- test/fixtures/header-value-relaxed.c | 114 +++++++++++++++++++++++++++ test/header-value-relaxed-test.ts | 54 +++++++++++++ test/md-test.ts | 2 + 3 files changed, 170 insertions(+) create mode 100644 test/fixtures/header-value-relaxed.c create mode 100644 test/header-value-relaxed-test.ts diff --git a/test/fixtures/header-value-relaxed.c b/test/fixtures/header-value-relaxed.c new file mode 100644 index 00000000..90432d00 --- /dev/null +++ b/test/fixtures/header-value-relaxed.c @@ -0,0 +1,114 @@ +#include +#include +#include + +#include "llhttp.h" + +static void check(llhttp_type_t type, int mode, const char* input, size_t len, + size_t split, size_t value_offset, unsigned char byte) { + llhttp_t parser; + llhttp_settings_t settings; + llhttp_errno_t err; + int invalid; + + llhttp_settings_init(&settings); + llhttp_init(&parser, type, &settings); + if (mode & 1) + llhttp_set_lenient_header_value_relaxed(&parser, 1); + if (mode & 2) + llhttp_set_lenient_headers(&parser, 1); + + invalid = !(mode & 2) && + (byte == 0 || (!(mode & 1) && + ((byte < 0x20 && byte != '\t') || byte == 0x7f))); + + err = llhttp_execute(&parser, input, split); + if (err == HPE_OK) + err = llhttp_execute(&parser, input + split, len - split); + + if (invalid) { + assert(err == HPE_INVALID_HEADER_TOKEN); + assert(llhttp_get_error_pos(&parser) == input + value_offset); + } else { + assert(err == HPE_OK); + assert(llhttp_finish(&parser) == HPE_OK); + } +} + +int main(void) { + static const struct { + llhttp_type_t type; + const char* prefix; + const char* suffix; + } messages[] = { + { HTTP_REQUEST, "GET / HTTP/1.1\r\nX: ", "\r\n\r\n" }, + { HTTP_REQUEST, "GET / HTTP/1.1\r\nHost: ", "\r\n\r\n" }, + { HTTP_RESPONSE, "HTTP/1.1 200 OK\r\nContent-Length: 0\r\nX: ", + "\r\n\r\n" }, + { HTTP_REQUEST, + "POST / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nX: ", + "\r\n\r\n" }, + { HTTP_RESPONSE, + "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n0\r\nX: ", + "\r\n\r\n" }, + }; + static const size_t lengths[] = { 0, 1, 15, 16, 17, 200 }; + char input[512]; + size_t message; + size_t length; + size_t prefix_len; + size_t value_offset; + size_t len; + size_t split; + unsigned int byte; + int mode; + int trailing; + + for (message = 0; message < sizeof(messages) / sizeof(messages[0]); message++) { + for (length = 0; length < sizeof(lengths) / sizeof(lengths[0]); length++) { + for (trailing = 0; trailing <= 1; trailing++) { + prefix_len = strlen(messages[message].prefix); + memcpy(input, messages[message].prefix, prefix_len); + memset(input + prefix_len, 'a', lengths[length]); + value_offset = prefix_len + lengths[length]; + len = value_offset + 1; + if (trailing) { + /* Leave enough bytes after the test byte for vectorized scanning. */ + memset(input + len, 'b', 32); + len += 32; + } + memcpy(input + len, messages[message].suffix, + strlen(messages[message].suffix)); + len += strlen(messages[message].suffix); + assert(len <= sizeof(input)); + + for (mode = 0; mode < 4; mode++) { + for (byte = 0; byte <= 0xff; byte++) { + /* CR and LF delimit headers; their handling has separate tests. */ + if (byte == '\r' || byte == '\n') + continue; + input[value_offset] = (char) byte; + + check(messages[message].type, mode, input, len, len, + value_offset, (unsigned char) byte); + check(messages[message].type, mode, input, len, value_offset, + value_offset, (unsigned char) byte); + check(messages[message].type, mode, input, len, value_offset + 1, + value_offset, (unsigned char) byte); + + /* Exercise every two-buffer split for the NUL regression. */ + if (byte == 0) { + for (split = 0; split <= len; split++) { + check(messages[message].type, mode, input, len, split, + value_offset, (unsigned char) byte); + } + } + } + } + } + } + } + + puts("header value byte and buffer-boundary checks passed"); + return 0; +} diff --git a/test/header-value-relaxed-test.ts b/test/header-value-relaxed-test.ts new file mode 100644 index 00000000..8d7893f4 --- /dev/null +++ b/test/header-value-relaxed-test.ts @@ -0,0 +1,54 @@ +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; +import { mkdirSync } from 'node:fs'; +import path from 'node:path'; +import { test } from 'node:test'; + +// The markdown fixtures pass inputs via argv, which cannot contain NUL bytes. +// Use a native test with explicit buffer lengths and an external watchdog. +test('header value validation rejects NUL without looping', () => { + const root = path.join(__dirname, '..'); + const build = path.join(root, 'build'); + const tmp = path.join(__dirname, 'tmp'); + mkdirSync(tmp, { recursive: true }); + + const generated = spawnSync(process.execPath, + [ '--import', 'tsx', 'bin/generate.ts' ], { + cwd: root, + encoding: 'utf8', + timeout: 60000, + }); + assert.ifError(generated.error); + assert.strictEqual(generated.status, 0, generated.stderr); + + const variants = [ { name: 'scalar', flags: [] as string[] } ]; + if (process.arch === 'x64' || process.arch === 'ia32') { + variants.push({ name: 'sse42', flags: [ '-msse4.2' ] }); + } + + for (const { name, flags } of variants) { + const executable = path.join(tmp, + `header-value-relaxed-${name}${process.platform === 'win32' ? '.exe' : ''}`); + const compiled = spawnSync(process.env.CLANG || 'clang', [ + '-O2', '-Wall', '-Wextra', '-Wno-unused-parameter', + ...flags, + '-I', build, + path.join(build, 'c', 'llhttp.c'), + path.join(root, 'src', 'native', 'api.c'), + path.join(root, 'src', 'native', 'http.c'), + path.join(__dirname, 'fixtures', 'header-value-relaxed.c'), + '-o', executable, + ], { encoding: 'utf8', timeout: 60000 }); + assert.ifError(compiled.error); + assert.strictEqual(compiled.status, 0, compiled.stderr); + + const result = spawnSync(executable, [], { + encoding: 'utf8', + timeout: 10000, + killSignal: 'SIGKILL', + }); + assert.ifError(result.error); + assert.strictEqual(result.status, 0, `${name}: ${result.stderr}`); + assert.match(result.stdout, /header value byte and buffer-boundary checks passed/); + } +}); diff --git a/test/md-test.ts b/test/md-test.ts index 5bdf2dce..9336bb19 100644 --- a/test/md-test.ts +++ b/test/md-test.ts @@ -6,6 +6,8 @@ import { Group, MDGator, Metadata, Test } from 'mdgator'; import path from 'path'; import vm from 'vm'; +import './header-value-relaxed-test'; + import llhttp from '../src/llhttp'; import { allowedTypes, build, FixtureResult, Node, TestType } from './fixtures';