Description
RFC 8949 §3.2.3 requires that the chunks of an indefinite-length string be definite-length strings of the same major type:
Each chunk of an indefinite-length string is a definite-length string of the same major type.
get_cbor_string() and get_cbor_binary() read each chunk by recursing into themselves (binary_reader.hpp:1025-1037 and 1125-1137), and the recursive call re-enters the full switch including the 0x7F/0x5F case. So a chunk may itself be an indefinite-length string, to arbitrary depth, and the library accepts it.
The cross-major-type rule is already enforced correctly — a byte-string chunk inside a text string is rejected, and vice versa. It is only the "definite-length" half of the rule that is missing.
Secondary effect: because the nesting is unbounded, 0x7F repeated ~50 000 times segfaults. That aspect overlaps with #5104 / #5293, but a depth guard alone would only turn the crash into a clean parse_error — the two-level case below would still be accepted rather than rejected. The conformance fix is independent and cheaper: reject 0x7F/0x5F when already inside an indefinite-length string, which also removes this particular recursion path entirely.
Reproduction steps
Parse a two-level indefinite-length text string and byte string.
Expected vs. actual results
Expected: parse_error.113, same as any other invalid chunk head.
Actual:
| input |
meaning |
result |
7F 7F 61 61 FF FF |
indefinite tstr containing an indefinite tstr |
accepted, yields "a" |
5F 5F 41 61 FF FF |
indefinite bstr containing an indefinite bstr |
accepted, yields h'61' |
7F 41 61 FF |
bstr chunk inside a tstr |
rejected (correct) |
5F 61 61 FF |
tstr chunk inside a bstr |
rejected (correct) |
Minimal code example
#include <nlohmann/json.hpp>
#include <iostream>
using json = nlohmann::json;
int main()
{
// 0x7F ( 0x7F ( "a" ) ) -- a chunk that is itself indefinite-length
const std::vector<std::uint8_t> v{0x7F, 0x7F, 0x61, 0x61, 0xFF, 0xFF};
std::cout << json::from_cbor(v) << "\n"; // prints "a"; should throw
// unbounded nesting -> stack exhaustion (cf. #5104)
std::vector<std::uint8_t> deep(50000, 0x7F);
deep.insert(deep.end(), 50000, 0xFF);
(void)json::from_cbor(deep); // segfault
}
Error messages
None — the malformed input is accepted. (The deep case terminates with SIGSEGV.)
Compiler and operating system
g++ 13.3.0, Ubuntu 24.04, -std=c++11
Library version
develop @ 8ec98e2
Would the fix be breaking?
No API/ABI change, and the behavior change is confined to input that is malformed per the spec.
- What changes: byte sequences that RFC 8949 forbids and that other CBOR implementations reject would start throwing
parse_error.113 instead of being accepted. There is no valid encoder that produces them, so no legitimate document is affected.
- Round-trip safety: the library never emits indefinite-length strings, so nothing it writes can trip the new check.
- Risk: the only realistic breakage is a user whose non-conforming producer happens to emit nested chunks and who relies on this library's leniency. That seems remote, and tightening it matches how the reader already handles the cross-major-type half of the same rule.
- Docs/tests: the "Deserialization" section of
docs/mkdocs/docs/features/binary_formats/cbor.md doesn't currently say anything about chunk structure; a sentence could be added. I did not find an existing unit test that covers nested chunks, so nothing in unit-cbor.cpp should need to be relaxed.
Fixing this also shrinks the surface of #5104: with nesting rejected, indefinite-length strings stop being a recursion vector at all, and the depth guard in #5293 is left to handle the genuinely recursive cases (nested arrays/maps and chained tags).
Description
RFC 8949 §3.2.3 requires that the chunks of an indefinite-length string be definite-length strings of the same major type:
get_cbor_string()andget_cbor_binary()read each chunk by recursing into themselves (binary_reader.hpp:1025-1037and1125-1137), and the recursive call re-enters the fullswitchincluding the0x7F/0x5Fcase. So a chunk may itself be an indefinite-length string, to arbitrary depth, and the library accepts it.The cross-major-type rule is already enforced correctly — a byte-string chunk inside a text string is rejected, and vice versa. It is only the "definite-length" half of the rule that is missing.
Secondary effect: because the nesting is unbounded,
0x7Frepeated ~50 000 times segfaults. That aspect overlaps with #5104 / #5293, but a depth guard alone would only turn the crash into a cleanparse_error— the two-level case below would still be accepted rather than rejected. The conformance fix is independent and cheaper: reject0x7F/0x5Fwhen already inside an indefinite-length string, which also removes this particular recursion path entirely.Reproduction steps
Parse a two-level indefinite-length text string and byte string.
Expected vs. actual results
Expected:
parse_error.113, same as any other invalid chunk head.Actual:
7F 7F 61 61 FF FF"a"5F 5F 41 61 FF FFh'61'7F 41 61 FF5F 61 61 FFMinimal code example
Error messages
None — the malformed input is accepted. (The deep case terminates with SIGSEGV.)
Compiler and operating system
g++ 13.3.0, Ubuntu 24.04,
-std=c++11Library version
develop@ 8ec98e2Would the fix be breaking?
No API/ABI change, and the behavior change is confined to input that is malformed per the spec.
parse_error.113instead of being accepted. There is no valid encoder that produces them, so no legitimate document is affected.docs/mkdocs/docs/features/binary_formats/cbor.mddoesn't currently say anything about chunk structure; a sentence could be added. I did not find an existing unit test that covers nested chunks, so nothing inunit-cbor.cppshould need to be relaxed.Fixing this also shrinks the surface of #5104: with nesting rejected, indefinite-length strings stop being a recursion vector at all, and the depth guard in #5293 is left to handle the genuinely recursive cases (nested arrays/maps and chained tags).