Add UTF-8 validation option levels to upb JSON decoder

This defaults to a log-err today, but will switch to enforce in the 2027-Q1 breaking change.

This actually just runs the UTF-8 validate over the entire input buffer at the start. Technically it only needs to run over the contents inside quoted strings, since the other characters have to be a small subset of ascii characters anyway (brackets, quotes, digits, etc). But as JSON is string-heavy (every key is a string) utf8_range simd behavior is happy to be given large ascii buffers. Benchmark results show that the extra cost of this being enabled is within benchmark noise level.

PiperOrigin-RevId: 970651879
This commit is contained in:
Protobuf Team Bot 2026-08-25 10:41:31 -07:00 committed by Copybara-Service
parent 6836552e25
commit 06d6ae6228
5 changed files with 98 additions and 4 deletions

View file

@ -407,7 +407,7 @@ static absl::string_view UpbJsonEncode(upb_benchmark_FileDescriptorProto* proto,
return absl::string_view(buf, size);
}
static void BM_JsonParse_Upb(benchmark::State& state) {
static void BM_JsonParse_Upb(benchmark::State& state, int options) {
upb_Arena* arena = upb_Arena_New();
upb_benchmark_FileDescriptorProto* set =
upb_benchmark_FileDescriptorProto_parse(descriptor.data, descriptor.size,
@ -429,14 +429,28 @@ static void BM_JsonParse_Upb(benchmark::State& state) {
upb_Status status;
upb_Status_Clear(&status);
bool ok = upb_JsonDecode(json.data(), json.size(), UPB_UPCAST(proto), md,
defpool.ptr(), 0, arena, &status);
defpool.ptr(), options, arena, &status);
ABSL_CHECK(ok) << "Failed to parse: " << status.msg;
benchmark::DoNotOptimize(proto);
upb_Arena_Free(arena);
}
state.SetBytesProcessed(state.iterations() * json.size());
}
BENCHMARK(BM_JsonParse_Upb);
static void BM_JsonParse_Upb_Utf8Off(benchmark::State& state) {
BM_JsonParse_Upb(state, upb_JsonDecode_ValidateUtf8_Off);
}
BENCHMARK(BM_JsonParse_Upb_Utf8Off);
static void BM_JsonParse_Upb_Utf8Warn(benchmark::State& state) {
BM_JsonParse_Upb(state, upb_JsonDecode_ValidateUtf8_Warn);
}
BENCHMARK(BM_JsonParse_Upb_Utf8Warn);
static void BM_JsonParse_Upb_Utf8Enforce(benchmark::State& state) {
BM_JsonParse_Upb(state, upb_JsonDecode_ValidateUtf8_Enforce);
}
BENCHMARK(BM_JsonParse_Upb_Utf8Enforce);
static void BM_JsonParse_Proto2(benchmark::State& state) {
protobuf::FileDescriptorProto proto;

View file

@ -26,6 +26,7 @@ cc_library(
features = UPB_DEFAULT_FEATURES,
visibility = ["//visibility:public"],
deps = [
"//third_party/utf8_range",
"//upb/base",
"//upb/lex",
"//upb/mem",

View file

@ -16,6 +16,7 @@
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -33,6 +34,7 @@
#include "upb/reflection/def.h"
#include "upb/reflection/message.h"
#include "upb/wire/encode.h"
#include "utf8_range.h"
// Must be last.
#include "upb/port/def.inc"
@ -1593,6 +1595,21 @@ int upb_JsonDecodeDetectingNonconformance(const char* buf, size_t size,
if (size == 0) return true;
int utf8_level = options & upb_JsonDecode_ValidateUtf8_Mask;
if (utf8_level != upb_JsonDecode_ValidateUtf8_Off) {
if (!utf8_range_IsValid(buf, size)) {
if (utf8_level == upb_JsonDecode_ValidateUtf8_Warn) {
fprintf(
stderr,
"Invalid UTF-8 in JSON input. This will be rejected starting in "
"the 2027-Q1 breaking change\n");
} else if (utf8_level == upb_JsonDecode_ValidateUtf8_Enforce) {
upb_Status_SetErrorMessage(status, "Invalid UTF-8 in JSON input");
return kUpb_JsonDecodeResult_Error;
}
}
}
d.ptr = buf;
d.end = buf + size;
d.arena = arena;

View file

@ -22,7 +22,14 @@
extern "C" {
#endif
enum { upb_JsonDecode_IgnoreUnknown = 1 };
enum {
upb_JsonDecode_IgnoreUnknown = 1 << 0,
upb_JsonDecode_ValidateUtf8_Off = 0 << 1,
upb_JsonDecode_ValidateUtf8_Warn = 1 << 1,
upb_JsonDecode_ValidateUtf8_Enforce = 2 << 1,
upb_JsonDecode_ValidateUtf8_Mask = 3 << 1,
};
enum {
kUpb_JsonDecodeResult_Ok = 0,

View file

@ -382,3 +382,58 @@ TEST(JsonTest, AllocationFailureAny) {
TestJsonAllocationFailure(
R"({"any_val": {"@type": "type.googleapis.com/google.protobuf.Value", "value": "foo"}})");
}
TEST(JsonTest, Utf8ValidationLevels) {
upb::Arena arena;
upb::DefPool defpool;
const upb_MessageDef* m = upb_test_Box_getmsgdef(defpool.ptr());
ASSERT_TRUE(m != nullptr);
// Invalid UTF-8 sequence inside JSON string
std::string invalid_json = "{\"name\": \"\xff\xff\"}";
// 1. OFF: Should not fail on UTF-8 (decoding succeeds)
{
upb_test_Box* box = upb_test_Box_new(arena.ptr());
upb::Status status;
bool ok = upb_JsonDecode(invalid_json.data(), invalid_json.size(),
UPB_UPCAST(box), m, defpool.ptr(),
upb_JsonDecode_ValidateUtf8_Off, arena.ptr(),
status.ptr());
EXPECT_TRUE(ok) << status.error_message();
}
// 2. WARN: Should print warning to stderr but still succeed
{
upb_test_Box* box = upb_test_Box_new(arena.ptr());
upb::Status status;
bool ok = upb_JsonDecode(invalid_json.data(), invalid_json.size(),
UPB_UPCAST(box), m, defpool.ptr(),
upb_JsonDecode_ValidateUtf8_Warn, arena.ptr(),
status.ptr());
EXPECT_TRUE(ok) << status.error_message();
}
// 3. ENFORCE: Should fail to parse
{
upb_test_Box* box = upb_test_Box_new(arena.ptr());
upb::Status status;
bool ok = upb_JsonDecode(invalid_json.data(), invalid_json.size(),
UPB_UPCAST(box), m, defpool.ptr(),
upb_JsonDecode_ValidateUtf8_Enforce, arena.ptr(),
status.ptr());
EXPECT_FALSE(ok);
EXPECT_STREQ(status.error_message(), "Invalid UTF-8 in JSON input");
}
// Valid UTF-8 should succeed with ENFORCE
{
std::string valid_json = "{\"name\": \"hello world\"}";
upb_test_Box* box = upb_test_Box_new(arena.ptr());
upb::Status status;
bool ok = upb_JsonDecode(
valid_json.data(), valid_json.size(), UPB_UPCAST(box), m, defpool.ptr(),
upb_JsonDecode_ValidateUtf8_Enforce, arena.ptr(), status.ptr());
EXPECT_TRUE(ok) << status.error_message();
}
}