From ff92562b3754163c0ddac4be7f184d3c618c1283 Mon Sep 17 00:00:00 2001 From: Aditya <205600203+Rohilalala@users.noreply.github.com> Date: Thu, 24 Sep 2026 22:30:24 +0530 Subject: [PATCH] expfmt: reject invalid UTF-8 and a trailing CR in OpenMetrics 2.0 metadata MetricFamilyToOpenMetrics20 wrote a metric name, HELP or UNIT that is not valid UTF-8, and a HELP ending in a carriage return, which makes the line end in "\r\n". OpenMetrics 2.0 requires UTF-8 and forbids \r in line endings, and the Prometheus OpenMetrics 2.0 parser rejects the whole exposition in both cases. Return an error instead, as for raw newlines in the name and unit. Signed-off-by: Aditya <205600203+Rohilalala@users.noreply.github.com> --- expfmt/openmetrics_2_0_create.go | 23 ++++++++++- expfmt/openmetrics_2_0_create_test.go | 59 +++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/expfmt/openmetrics_2_0_create.go b/expfmt/openmetrics_2_0_create.go index f9b2a3c0..600ac32f 100644 --- a/expfmt/openmetrics_2_0_create.go +++ b/expfmt/openmetrics_2_0_create.go @@ -21,6 +21,7 @@ import ( "math" "strconv" "strings" + "unicode/utf8" dto "github.com/prometheus/client_model/go" "google.golang.org/protobuf/types/known/timestamppb" @@ -51,8 +52,26 @@ func MetricFamilyToOpenMetrics20(out io.Writer, in *dto.MetricFamily, options .. if containsRawNewline(name) { return 0, fmt.Errorf("MetricFamily name %q contains raw newlines", name) } - if in.Unit != nil && containsRawNewline(*in.Unit) { - return 0, fmt.Errorf("MetricFamily unit %q contains raw newlines", *in.Unit) + if !utf8.ValidString(name) { + return 0, fmt.Errorf("MetricFamily name %q is not valid UTF-8", name) + } + if in.Help != nil { + if !utf8.ValidString(*in.Help) { + return 0, fmt.Errorf("MetricFamily help %q is not valid UTF-8", *in.Help) + } + // A carriage return may appear in HELP, but not at the end, where it + // would make the line end in "\r\n". + if strings.HasSuffix(*in.Help, "\r") { + return 0, fmt.Errorf("MetricFamily help %q ends with a carriage return", *in.Help) + } + } + if in.Unit != nil { + if containsRawNewline(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q contains raw newlines", *in.Unit) + } + if !utf8.ValidString(*in.Unit) { + return 0, fmt.Errorf("MetricFamily unit %q is not valid UTF-8", *in.Unit) + } } // Try the interface upgrade. If it doesn't work, we'll use a diff --git a/expfmt/openmetrics_2_0_create_test.go b/expfmt/openmetrics_2_0_create_test.go index ff08eee4..4c42f3b2 100644 --- a/expfmt/openmetrics_2_0_create_test.go +++ b/expfmt/openmetrics_2_0_create_test.go @@ -304,6 +304,18 @@ http_requests_total 1027.0 http_requests_total 1027.0 `, }, + { + name: "CarriageReturnInsideHelp", + in: &dto.MetricFamily{ + Name: proto.String("http_requests_total"), + Help: proto.String("a\rb"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1027)}}, + }, + }, + out: "# HELP http_requests_total a\rb\n# TYPE http_requests_total counter\nhttp_requests_total 1027.0\n", + }, { name: "CounterWithNaNExemplar", in: &dto.MetricFamily{ @@ -2155,6 +2167,53 @@ func TestCreateOpenMetrics20_Errors(t *testing.T) { }, expectedErr: "contains raw newlines", }, + { + name: "InvalidUTF8InMetricName", + in: &dto.MetricFamily{ + Name: proto.String("test_counter\xff"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, + { + name: "InvalidUTF8InHelp", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Help: proto.String("help \xff"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, + { + name: "CarriageReturnAtEndOfHelp", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Help: proto.String("help\r"), + Type: dto.MetricType_COUNTER.Enum(), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "ends with a carriage return", + }, + { + name: "InvalidUTF8InUnit", + in: &dto.MetricFamily{ + Name: proto.String("test_counter_total"), + Type: dto.MetricType_COUNTER.Enum(), + Unit: proto.String("seconds\xff"), + Metric: []*dto.Metric{ + {Counter: &dto.Counter{Value: proto.Float64(1.0)}}, + }, + }, + expectedErr: "is not valid UTF-8", + }, { name: "NilMetric", in: &dto.MetricFamily{