Skip to content

Add FuzzTest-based fuzzing for schema serde and fix the codec bugs it… - #3973

Draft
sbaluja wants to merge 1 commit into
mainfrom
fuzz-testing
Draft

sbaluja wants to merge 1 commit into
mainfrom
fuzz-testing

Conversation

@sbaluja

@sbaluja sbaluja commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Issue #, if available:

Description of changes:
Framework (tests/fuzz, see its README): google/fuzztest targets for the JSON, CBOR and XML codecs, awsQuery/ec2Query and JSON documents, built with -DENABLE_SCHEMA_FUZZ_TESTS=ON (Linux, Clang, static) and run in a container by tools/scripts/run_fuzz.sh (unit, fuzz, campaign, repro, replay). A fixed schema registry covers every serde trait; a choice-tape generator builds values for any schema. Oracles: no crash or hang on arbitrary bytes; whatever is read is written and read back unchanged, and the second write is a byte-for-byte fixed point; generated values round-trip; query requests carry exactly the fields the awsQuery and ec2Query naming rules imply, with their exact text (numbers parse back to the same value, timestamps to the same instant, also under a ',' decimal locale). Known codec losses are named CodecTolerance flags, each pinned by a core test.

Codec fixes:

  • floats near FLT_MAX round to FLT_MAX; XML and query floats are written with round-trip precision, non-finite XML attributes as NaN/Infinity
  • epoch-seconds timestamps decode exactly as DateTime(double) does, for parity with legacy clients
  • JSON: unpaired surrogates, bad \u hex and unknown escapes are errors; a leading byte-order mark is skipped; non-object roots are errors and a null root is an empty struct
  • XML: numeric character references decode, CDATA is literal, comments and processing instructions are not text, line ends normalize as tinyxml2 does, CR is written as ; invalid names, duplicate or unquoted attributes are errors; attribute members are delivered only when present; number, boolean and timestamp text is trimmed and booleans are read in any case, as legacy clients read them
  • CBOR: negative integers convert to double with one rounding; text and byte strings, including keys, may be indefinite-length
  • query: floating-point values are URL-encoded; awsQuery sends an empty list as a bare key (EmptyQueryLists), ec2Query omits it

Locale: StringUtils::ParseDouble and the FormatRoundTripDouble/Float reparse read '.' whatever the C numeric locale, and FormatRoundTrip* and StringUtils::to_string format in the classic locale.

Check all that applies:

  • Did a review by yourself.
  • Added proper tests to cover this PR. (If tests are not applicable, explain.)
  • Checked if this PR is a breaking (APIs have been changed) change.
  • Checked if this PR will not introduce cross-platform inconsistent behavior.
  • Checked if this PR would require a ReadMe/Wiki update.

Check which platforms you have built SDK on to verify the correctness of this PR.

  • Linux
  • Windows
  • Android
  • MacOS
  • IOS
  • Other Platforms

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

… found

Framework (tests/fuzz, see its README): google/fuzztest targets for the
JSON, CBOR and XML codecs, awsQuery/ec2Query and JSON documents, built
with -DENABLE_SCHEMA_FUZZ_TESTS=ON (Linux, Clang, static) and run in a
container by tools/scripts/run_fuzz.sh (unit, fuzz, campaign, repro,
replay). A fixed schema registry covers every serde trait; a choice-tape
generator builds values for any schema. Oracles: no crash or hang on
arbitrary bytes; whatever is read is written and read back unchanged,
and the second write is a byte-for-byte fixed point; generated values
round-trip; query requests carry exactly the fields the awsQuery and
ec2Query naming rules imply, with their exact text (numbers parse back
to the same value, timestamps to the same instant, also under a ','
decimal locale). Known codec losses
are named CodecTolerance flags, each pinned by a core test.

Reference parity: ReferenceParity.cpp walks the cJSON (via the CRT,
cJSON_ParseWithOpts so trailing data is rejected) or tinyxml2 11.0.0
tree along a schema by the protocol rules and records what the codec
must read, whether it must fail, and what the reference cannot judge.
The codecs must accept exactly what the reference accepts, read the
same tree, and have their own output read back by the reference;
awsQuery responses are judged the same way. Scalar text is read as
legacy clients read it (XML numbers, booleans and timestamps trimmed),
with the oracles' own parsers (ScalarText.cpp) rather than the codecs'.
Reference leniency is modeled, not copied: only input holding markup
tinyxml2 tolerates is excused from the accept/reject check. There is no
CBOR reference.

Robustness: unit mode runs under ASan and UBSan; deep seeds sit at
MAX_NESTING_DEPTH, so fuzz runs check that depth fits the stack
limit; SCHEMA_FUZZ_LOCALE runs every target under a ',' decimal
locale; campaigns size their job pool from memory and cap RSS per
target.
Failing inputs are kept as corpus files (.b64 for binary) or findings
tapes and replayed on every unit run.

Read contract: deserializers report failures through
Optional<DeserializerError> and share MAX_NESTING_DEPTH with the
serializers. A read fails on structural malformation anywhere in the
payload, including skipped values and trailing data after the root; on
a value the target type cannot represent (int/long overflow, a
non-integral long, float/double overflow, a non-finite document number,
epoch seconds beyond the system clock); and on a union with more than
one non-null modeled member. A well-formed value of the wrong type
leaves the member absent. StringUtils::ParseInt64 rejects out-of-range
input instead of saturating.

Codec fixes:
- floats near FLT_MAX round to FLT_MAX; XML and query floats are written
  with round-trip precision, non-finite XML attributes as NaN/Infinity
- epoch-seconds timestamps decode exactly as DateTime(double) does, for
  parity with legacy clients
- JSON: unpaired surrogates, bad \u hex and unknown escapes are errors;
  a leading byte-order mark is skipped; non-object roots are errors and
  a null root is an empty struct
- XML: numeric character references decode, CDATA is literal, comments
  and processing instructions are not text, line ends normalize as
  tinyxml2 does, CR is written as &#xD;; invalid names, duplicate or
  unquoted attributes are errors; attribute members are delivered only
  when present; number, boolean and timestamp text is trimmed and
  booleans are read in any case, as legacy clients read them
- CBOR: negative integers convert to double with one rounding; text and
  byte strings, including keys, may be indefinite-length
- query: floating-point values are URL-encoded; awsQuery sends an empty
  list as a bare key (EmptyQueryLists), ec2Query omits it

Locale: StringUtils::ParseDouble and the FormatRoundTripDouble/Float
reparse read '.' whatever the C numeric locale, and FormatRoundTrip* and
StringUtils::to_string format in the classic locale.

Build conventions: the fuzz binary compiles with set_compiler_warnings;
FuzzTest is fetched as SYSTEM (CMake >= 3.25) so its headers stay out of
that warning set. The image lives in
tools/CI/docker-file/Ubuntu/24.04-schema-fuzz/.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant