Fix clang-doc crash when comment contains invalid UTF-8 bytes (#210886)

When a source file comment contains non-UTF-8 bytes (e.g., byte 0x97),
clang-doc's JSONGenerator would crash with an assertion failure:

  Assertion `false && "Invalid UTF-8 in value used as JSON"' failed.

This happens because json::Value(StringRef) asserts on invalid UTF-8
input in debug builds. While the release-build path calls fixUTF8()
automatically, the assertion crash affects developers and fuzzer runs.

Fix by following the same pattern used elsewhere in LLVM (lldb, clangd):
check isUTF8() before constructing a json::Value, and call fixUTF8() to
sanitize the string if invalid bytes are detected. This avoids an
unnecessary copy for the common case of valid UTF-8.

The fix covers all paths in serializeComment() where user-written text
from source comments enters JSON objects:
  - CK_TextComment (I.Text)
  - CK_InlineCommandComment (I.Args)
  - CK_VerbatimBlockLineComment / CK_VerbatimLineComment (I.Text)
  - CK_Unknown (I.Text)

Also add a regression test with an actual invalid UTF-8 byte in a
comment.

AI assistance was used for code review analysis and local build
verification.

Fixes: https://github.com/llvm/llvm-project/issues/210675

---------

Co-authored-by: Chenguang Ding <dingchenguang@kylinos.cn>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Cyrus Ding 2026-07-25 05:20:11 +08:00 committed by GitHub
parent 8e34b2cbb0
commit 84b931c00f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 58 additions and 4 deletions

View file

@ -106,6 +106,12 @@ static void insertNonEmpty(StringRef Key, StringRef Value, Object &Obj) {
Obj[Key] = Value;
}
static json::Value safeJSONString(StringRef S) {
if (LLVM_LIKELY(json::isUTF8(S)))
return S;
return json::fixUTF8(S);
}
static std::string infoTypeToString(InfoType IT) {
switch (IT) {
case InfoType::IT_default:
@ -233,7 +239,7 @@ static Object serializeComment(const CommentInfo &I, Object &Description) {
switch (I.Kind) {
case CommentKind::CK_TextComment: {
if (!I.Text.empty())
Obj.insert({commentKindToString(I.Kind), I.Text});
Obj.insert({commentKindToString(I.Kind), safeJSONString(I.Text)});
return Obj;
}
@ -258,7 +264,7 @@ static Object serializeComment(const CommentInfo &I, Object &Description) {
auto &ARef = *ArgsArr.getAsArray();
ARef.reserve(I.Args.size());
for (const auto &Arg : I.Args)
ARef.emplace_back(Arg);
ARef.emplace_back(safeJSONString(Arg));
Child.insert({"Command", I.Name});
Child.insert({"Args", ArgsArr});
Child.insert({"Children", ChildArr});
@ -292,7 +298,7 @@ static Object serializeComment(const CommentInfo &I, Object &Description) {
case CommentKind::CK_VerbatimBlockLineComment:
case CommentKind::CK_VerbatimLineComment: {
Child.insert({"Text", I.Text});
Child.insert({"Text", safeJSONString(I.Text)});
Child.insert({"Children", ChildArr});
Obj.insert({commentKindToString(I.Kind), ChildVal});
return Obj;
@ -333,7 +339,7 @@ static Object serializeComment(const CommentInfo &I, Object &Description) {
}
case CommentKind::CK_Unknown: {
Obj.insert({commentKindToString(I.Kind), I.Text});
Obj.insert({commentKindToString(I.Kind), safeJSONString(I.Text)});
return Obj;
}
}

View file

@ -0,0 +1,2 @@
// non-UTF-8: <20>
class A {};

View file

@ -0,0 +1,46 @@
// RUN: rm -rf %t && mkdir -p %t
// RUN: clang-doc --pretty-json --output=%t --format=json --executor=standalone %S/../Inputs/invalid-utf8-comment.cpp 2>&1
// RUN: cat %t/json/GlobalNamespace/_ZTV1A.json | FileCheck %s
// Regression test for https://github.com/llvm/llvm-project/issues/210675.
// clang-doc should not crash when encountering comments with invalid UTF-8 bytes.
// Instead, the invalid bytes should be replaced with valid UTF-8 replacement characters.
// CHECK: {
// CHECK-NEXT: "Contexts": [
// CHECK-NEXT: {
// CHECK-NEXT: "DocumentationFileName": "index",
// CHECK-NEXT: "End": true,
// CHECK-NEXT: "Name": "Global Namespace",
// CHECK-NEXT: "QualName": "GlobalNamespace",
// CHECK-NEXT: "RelativePath": "./",
// CHECK-NEXT: "USR": "0000000000000000000000000000000000000000"
// CHECK-NEXT: }
// CHECK-NEXT: ],
// CHECK-NEXT: "Description": {
// CHECK-NEXT: "HasParagraphComments": true,
// CHECK-NEXT: "ParagraphComments": [
// CHECK-NEXT: [
// CHECK-NEXT: {
// CHECK: "TextComment":
// CHECK-NEXT: }
// CHECK-NEXT: ]
// CHECK-NEXT: ]
// CHECK-NEXT: },
// CHECK-NEXT: "DocumentationFileName": "_ZTV1A",
// CHECK-NEXT: "HasContexts": true,
// CHECK-NEXT: "InfoType": "record",
// CHECK-NEXT: "IsTypedef": false,
// CHECK-NEXT: "Location": {
// CHECK-NEXT: "Filename": "{{.*}}invalid-utf8-comment.cpp",
// CHECK-NEXT: "LineNumber": 2
// CHECK-NEXT: },
// CHECK-NEXT: "MangledName": "_ZTV1A",
// CHECK-NEXT: "Name": "A",
// CHECK-NEXT: "Namespace": [
// CHECK-NEXT: "GlobalNamespace"
// CHECK-NEXT: ],
// CHECK-NEXT: "Path": "GlobalNamespace",
// CHECK-NEXT: "TagType": "class",
// CHECK-NEXT: "USR": "{{[0-9A-F]*}}"
// CHECK-NEXT: }