From de2ed3b7267a36eb71d67981dcf37dc714c71136 Mon Sep 17 00:00:00 2001 From: Joshua Richter Date: Mon, 31 Aug 2026 19:57:11 -0400 Subject: [PATCH] fix(grammar): stop the Swift scanner shifting past the width of an int The Swift scanner keeps a 64-bit mask of the symbols that suppress a match -- the rule that stops `try!` emitting its `!` as a token of its own. It tests one bit per candidate: uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match]; for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) { if (!(suppressing_symbols & 1 << suppressor)) { The mask is uint64_t but the literal `1` is an int, so the shift is an int shift. TOKEN_COUNT is larger than 32, so once suppressor reaches 31 the shift runs past the width of the type. That is undefined behavior, and every bit above 31 is tested against a value the standard does not define. Nothing caught it because nothing in the tree reached the suppressor path. Any Swift force-unwrap does: `cached!` is enough. UBSan reports it as: scanner.c:514:47: runtime error: left shift of 1 by 31 places cannot be represented in type 'int' `1ULL` makes the literal as wide as the mask it is tested against. The new test in tests/test_extraction.c cannot go red on its own. The normal test build prints the UBSan message and carries on, which is why this survived. The Windows CLANGARM64 leg runs UBSan in trap mode, and there the same shift is an illegal-instruction crash -- so the test exists to make sure that leg keeps parsing a force-unwrap at all. scripts/vendored-checksums.txt records the new hash for the one changed file, as scripts/security-vendored.sh --update writes it. Layer 8 of the security gate compares vendored content against that manifest, so the edit and its recorded hash belong in the same commit. Found while adding Swift URL extraction in #1892 / #1976, and split out of that PR so the vendored change can be reviewed on its own. Signed-off-by: Joshua Richter --- .../cbm/vendored/grammars/swift/scanner.c | 2 +- scripts/vendored-checksums.txt | 2 +- tests/test_extraction.c | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/internal/cbm/vendored/grammars/swift/scanner.c b/internal/cbm/vendored/grammars/swift/scanner.c index 2615afe43..bb2dcac58 100644 --- a/internal/cbm/vendored/grammars/swift/scanner.c +++ b/internal/cbm/vendored/grammars/swift/scanner.c @@ -511,7 +511,7 @@ static bool eat_operators( uint64_t suppressing_symbols = OP_SYMBOL_SUPPRESSOR[full_match]; if (suppressing_symbols) { for (uint64_t suppressor = 0; suppressor < TOKEN_COUNT; suppressor++) { - if (!(suppressing_symbols & 1 << suppressor)) { + if (!(suppressing_symbols & 1ULL << suppressor)) { continue; } diff --git a/scripts/vendored-checksums.txt b/scripts/vendored-checksums.txt index 991a684aa..9531f343d 100644 --- a/scripts/vendored-checksums.txt +++ b/scripts/vendored-checksums.txt @@ -787,7 +787,7 @@ c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4 internal/cbm/v 2db740ea1be6b71014d2be1385491f01bf2d15b607e61c5b30b4128535efe68a internal/cbm/vendored/grammars/sway/tree_sitter/parser.h 3533cec129bb4bba015c0d61d86dd7c3b7e82110e4d2ff7837a01eff5bad5ccc internal/cbm/vendored/grammars/swift/LICENSE 93e6b39fc5b16ef9d5869862ec8d56380a838355c362481942942d59ca666de2 internal/cbm/vendored/grammars/swift/parser.c -b835c1ded068e902944fe82c3770b9dcf85f67dcaab8fbf772f1606007da7373 internal/cbm/vendored/grammars/swift/scanner.c +f3d6271d64f58c39eed544104a70ca2cf9ecbf80c5d900620f1afd38836542cb internal/cbm/vendored/grammars/swift/scanner.c b29c1c9fb7cc82f58c84b376df1297d6e2737a1d655fd356db0859e3c29c2fea internal/cbm/vendored/grammars/swift/tree_sitter/alloc.h 5bdf6ed1a78e3409fd443e085ca967a64c188a5d082aaf7f819bccd53a471c94 internal/cbm/vendored/grammars/swift/tree_sitter/array.h a1f6ef161fbaf48a0e10fca90ef5290a062462b307b3898aa562993853b9f80a internal/cbm/vendored/grammars/swift/tree_sitter/parser.h diff --git a/tests/test_extraction.c b/tests/test_extraction.c index 5b8d16f61..cb7a1a8c8 100644 --- a/tests/test_extraction.c +++ b/tests/test_extraction.c @@ -1810,6 +1810,24 @@ TEST(swift_chained_call) { PASS(); } +/* A Swift force-unwrap is the one thing that reaches the scanner's suppressor + * path -- the rule that stops `try!` emitting its `!` as a token of its own. + * That path shifted an int by up to TOKEN_COUNT bits, which runs past the + * width of the type once the index reaches 31. + * + * This test cannot go red here. The normal test build prints the UBSan + * message and carries on, which is why the bug survived. The Windows + * CLANGARM64 leg runs UBSan in trap mode, where the same shift is an + * illegal-instruction crash, so parsing this file at all is the check. */ +TEST(swift_force_unwrap_scanner_shift) { + CBMFileResult *r = + extract("func load() { let u = cached! }\n", CBM_LANG_SWIFT, "t", "Load.swift"); + ASSERT_NOT_NULL(r); + ASSERT_FALSE(r->has_error); + cbm_free_result(r); + PASS(); +} + /* --- Objective-C --- */ TEST(objc_interface) { CBMFileResult *r = @@ -6661,6 +6679,7 @@ SUITE(extraction) { RUN_TEST(swift_method_call); RUN_TEST(swift_constructor_call); RUN_TEST(swift_chained_call); + RUN_TEST(swift_force_unwrap_scanner_shift); RUN_TEST(objc_interface); RUN_TEST(objc_implementation); RUN_TEST(dart_top_level_function);