From 65f8482335ebc31862cf4d1a4857ae99bd63abc8 Mon Sep 17 00:00:00 2001 From: Arve Knudsen Date: Wed, 24 Dec 2025 11:52:37 +0100 Subject: [PATCH] fix(promql): prevent panic in trimStringByBytes on invalid UTF-8 Add bounds check to prevent index out of range panic when trimStringByBytes receives a string containing only UTF-8 continuation bytes (0x80-0xBF). Previously, the loop would decrement size below 0 when no valid rune start byte was found, causing a panic. A malicious query string with only continuation bytes could crash the Prometheus server via the ActiveQueryTracker before the query was parsed or validated. Signed-off-by: Arve Knudsen --- promql/query_logger.go | 2 +- promql/query_logger_test.go | 41 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/promql/query_logger.go b/promql/query_logger.go index 954f8b1a5b..0c4b218828 100644 --- a/promql/query_logger.go +++ b/promql/query_logger.go @@ -164,7 +164,7 @@ func trimStringByBytes(str string, size int) string { trimIndex := len(bytesStr) if size < len(bytesStr) { - for !utf8.RuneStart(bytesStr[size]) { + for size > 0 && !utf8.RuneStart(bytesStr[size]) { size-- } trimIndex = size diff --git a/promql/query_logger_test.go b/promql/query_logger_test.go index 8c88757bd7..edd3baad12 100644 --- a/promql/query_logger_test.go +++ b/promql/query_logger_test.go @@ -127,6 +127,47 @@ func TestMMapFile(t *testing.T) { require.Equal(t, []byte(data), bytes[:2], "Mmap failed") } +func TestTrimStringByBytes(t *testing.T) { + for _, tc := range []struct { + name string + input string + size int + expected string + }{ + { + name: "normal ASCII string", + input: "hello", + size: 3, + expected: "hel", + }, + { + name: "no trimming needed", + input: "hi", + size: 10, + expected: "hi", + }, + { + name: "UTF-8 multibyte character boundary", + input: "日本", // 6 bytes (3 bytes per character) + size: 4, + expected: "日", // trims back to complete character boundary + }, + { + name: "invalid UTF-8 continuation-only bytes", + input: string([]byte{0x80, 0x81, 0x82, 0x83, 0x84}), // only continuation bytes + size: 4, + expected: "", + }, + } { + t.Run(tc.name, func(t *testing.T) { + require.NotPanics(t, func() { + result := trimStringByBytes(tc.input, tc.size) + require.Equal(t, tc.expected, result) + }) + }) + } +} + func TestParseBrokenJSON(t *testing.T) { for _, tc := range []struct { b []byte