Skip to content

fix: saturate digit parsing, check gmtime_r, pin clang-tidy (0.4.2) - #17

Merged
jwinarske merged 1 commit into
mainfrom
jw/overflow-and-tidy
Aug 14, 2026
Merged

fix: saturate digit parsing, check gmtime_r, pin clang-tidy (0.4.2)#17
jwinarske merged 1 commit into
mainfrom
jw/overflow-and-tidy

Conversation

@jwinarske

Copy link
Copy Markdown
Contributor

Chasing the intermittent clang-tidy warning turned up undefined behavior in the parser. 0.4.2.

The flake was not a race

run-clang-tidy resolves clang-tidy from PATH, and this machine has two: LLVM 18 from the workspace environment and 22 from the distribution. Whether the warning appeared depended on whether the shell had sourced setup_env.sh. 18 reported it, 22 did not — perfect correlation across every run.

The warning itself was a false positive: it flagged currentScreenshot = {} on the line after std::move(currentScreenshot), which is precisely how a moved-from object is restored. But 18 was also emitting 20 clang-diagnostic-errors against this host's libstdc++ and stopping with "too many errors emitted", so it was reporting from a half-parsed translation unit.

Pinning to a version that parses cleanly replaced that false positive with a real defect.

Undefined behavior on untrusted input

Both confirmed with UBSan on the real code path, driving the actual parser over a crafted catalog:

AppStreamParser.cpp:57 runtime error: signed integer overflow:
  999999999999999999 * 10 cannot be represented in type 'long long int'
AppStreamParser.cpp:37 runtime error: signed integer overflow:
  999999999 * 10 cannot be represented in type 'int'

Reached from <release timestamp="999..."> and <icon width="999..."> — attribute values in a catalog downloaded over the network. The same helper also parses priority, image/video dimensions, and icon scale.

Accumulation now saturates, so a hostile value is clamped rather than wrapped. CI already runs UBSan across a Debug/Release x asan/ubsan matrix, but no test fed an oversized number, so nothing ever fired. Three regression tests do now.

gmtime_r returns null for a time_t it cannot represent, which a saturated epoch reaches. The return was ignored, leaving the std::tm zero-initialized and silently producing 1900-01-01T00:00:00Z. It now yields no timestamp.

Pinning clang-tidy

scripts/tidy.sh pins clang-tidy as scripts/format.sh pins clang-format, and the CI job is no longer continue-on-error.

Two things worth calling out in review, both found while testing the gate rather than assumed:

  1. It invokes clang-tidy directly instead of via run-clang-tidy. The pip package ships no run-clang-tidy, and run-clang-tidy resolves the binary from PATH — the exact drift being fixed here.
  2. It passes --warnings-as-errors. Plain clang-tidy exits 0 even when it reports diagnostics, so the first version of this gate passed with the gmtime_r defect still present. A check that cannot fail is worse than no check, because it reads as coverage.

Verified by reintroducing the defect: gate exits 123. Restored: exits 0.

Version Parses host libstdc++ use-after-move false positive Found gmtime_r defect
18 (what CI installed) ✗ 20 errors, bails yes no
20 (pinned) no
22 no no

Verification

shellcheck clean; UBSan reports no errors on either hostile input after the fix; 152/152 C++ (three new) and 45/45 Dart; clang-tidy clean at the pinned version; clang-format run last; dart analyze --fatal-infos clean; dart pub publish --dry-run 0 warnings.

Chasing the intermittent clang-tidy warning turned up undefined behavior
in the parser.

The flake itself was not a race. run-clang-tidy resolves `clang-tidy` from
PATH, and this machine has two: LLVM 18 from the workspace environment and
22 from the distribution. Whether the warning appeared depended on whether
the shell had sourced the environment. Version 18 reported it; 22 did not.

The warning was a false positive. It flagged `currentScreenshot = {}` on
the line after `std::move(currentScreenshot)`, which is how a moved-from
object is restored. But 18 was also emitting twenty clang-diagnostic-errors
against this host's libstdc++ and stopping early, so it was reporting from
a half-parsed translation unit. Pinning to a version that parses cleanly
replaced that false positive with a real defect.

Undefined behavior, both confirmed with UBSan on the real code path:

  AppStreamParser.cpp:57 runtime error: signed integer overflow:
    999999999999999999 * 10 cannot be represented in type 'long long int'
  AppStreamParser.cpp:37 runtime error: signed integer overflow:
    999999999 * 10 cannot be represented in type 'int'

Reached from `<release timestamp="999...">` and `<icon width="999...">`.
Those are attribute values in a catalog downloaded over the network, and
the same helper also parses priority, image and video dimensions, and icon
scale. Accumulation now saturates. CI already runs UBSan, but no test fed
an oversized number, so nothing fired; three regression tests do now.

gmtime_r returns null for a time_t it cannot represent, which a saturated
epoch reaches. The return value was ignored, leaving the std::tm zero
initialized and silently yielding 1900-01-01T00:00:00Z. It now yields no
timestamp.

scripts/tidy.sh pins clang-tidy as scripts/format.sh pins clang-format, and
the CI job is no longer advisory. It invokes clang-tidy directly, because
the pip package ships no run-clang-tidy and run-clang-tidy would resolve
the binary from PATH — the exact drift being fixed. It passes
--warnings-as-errors: plain clang-tidy exits 0 even when it reports
diagnostics, so the first version of this gate passed with the gmtime_r
defect still present. Verified the gate now exits non-zero with that defect
reintroduced and zero once restored.

Verified: shellcheck clean; UBSan reports no errors on either hostile input
after the fix; 152/152 C++ (three new) and 45/45 Dart; clang-tidy clean at
the pinned version; clang-format run last; dart analyze --fatal-infos clean.
@jwinarske
jwinarske merged commit c44e741 into main Aug 14, 2026
12 checks passed
@jwinarske
jwinarske deleted the jw/overflow-and-tidy branch August 14, 2026 21:37
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