Repository navigation
fix(expression): report a character outside the grammar without closing the connection - #567
Open
LeeroyHannigan wants to merge 6 commits into
Open
LeeroyHannigan wants to merge 6 commits into
LeeroyHannigan wants to merge 6 commits into
Conversation
…e does, without a panic The tokenizer behind condition and filter expressions read the offending byte as a char and sliced the input five bytes either side of it. When that range ended inside a multi-byte character (`#v = ééé`, `#v = 日本語`) the slice panicked, the request task ended and the connection closed; the server's key condition parse ahead of authorization calls the same tokenizer, so a Query key condition did the same for every principal. Other multi-byte input answered a token made of the first byte read as Latin-1. The tokenizer behind update, projection and the engine's key condition parse did not panic and answered the character with one byte after it as the window. The error now names the character as sent and shows the service's window: the source text from the start of the previous token to the end of the next one, with each character outside the grammar counted as one token (measured 2026-10-25, each request twice: `#v = ééé` answers `token: "é", near: "= éé"`, `abéc = :v` answers `near: "abéc"`, `#v = $x` answers `near: "= $x"`). Both tokenizer entry points share the rule.
Pure move, using the #[path] pattern evaluator_tests.rs uses; tokenizer.rs had grown past 600 lines with the previous commit.
…rammar Every row is a request measured against the service on 2026-10-25, each sent twice: Scan filters with multi-byte and ASCII characters outside the grammar in each position, and one or two rows each for PutItem conditions, Query key conditions, UpdateItem update expressions and GetItem projections.
…drop U+0000, as the service does When no token comes before the character, the service starts the window at the start of the expression and keeps the leading whitespace (` é = :v` answers `near: " é ="`), and it removes U+0000 from the token and the window (`#v = \0x` answers `token: "", near: "= x"`). Measured 2026-10-25 at 16:34, 16:38 and 16:43 UTC, each request twice. The unit table gains those rows and the rows that pin the next-token rules for two-character operators, name runs and digit runs.
…ken rules and the Query paths Rows measured on 2026-10-25 at 16:34, 16:38, 16:43 and 16:54 UTC, each request twice: a character with no token before it, U+0000, two-character operators, name runs and digit runs after the character, and the panic shape on a Query key condition, a Query filter and an UpdateItem condition. The prefix the service adds on write paths is stripped only on those paths.
LeeroyHannigan
requested review from
amrith,
c33howard,
jcshepherd,
pdf-amzn,
robinnsc and
yesyayen
as code owners
October 25, 2026 17:34
crates/core/src/expression/tokenizer.rs: main fixed the panic in tokenize_with_limit by moving both ends of the five-byte window out to character boundaries and reporting the whole character as the token. This branch answers the window the service answers, measured on 2026-10-25: from the start of the previous token to the end of the next one, from the start of the expression when no token comes before the character, with U+0000 removed. The result keeps this branch's unknown_character and next_token_end, which slice on character boundaries and so cover the panic main fixed. main's five-byte window code is superseded. The tests stay in tokenizer_tests.rs, where this branch moved them. main's three new tests move from tokenizer.rs into tokenizer_tests.rs. tokenize_multibyte_utf8_near_an_invalid_byte_does_not_panic stays as main wrote it; its one window assertion, "$ 日" for the input "$ 日", matches the measured rule. tokenize_reports_the_whole_invalid_character keeps its token assertion, and its window for "a = é" changes from "a = é" to "= é", the window the measured row for "#v = é" gives. tokenize_ascii_error_context_is_five_bytes_either_side is dropped: it asserted the window "s(a) $ b" for "attribute_exists(a) $ b", five bytes either side of the character, and the measured rule answers ") $ b" for that input.
LeeroyHannigan
requested review from
eliseong,
hitesh96db and
navareamol
as code owners
November 5, 2026 15:38
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A character outside the expression grammar, such as
é, now gets the syntax error DynamoDB gives, with the same token and window. The token is the character as sent. The window runs from the start of the previous token to the end of the next one. Both tokenizers follow this rule:ConditionExpressionon PutItem, UpdateItem, DeleteItem and TransactWriteItems,FilterExpressionon Scan and Query, andKeyConditionExpressionon Query. The key condition goes through the server'sdynamodb:LeadingKeysparse, which runs before the authorization decision for every caller.UpdateExpression,ProjectionExpressionand the engine's key condition parse.Not changed here
These differences from the service are on main and stay as they are:
#v = = é), the service reports that fault. ExtendDB reports the character, because it tokenizes the whole expression before parsing.#or:followed by a character that cannot start a name keeps main's texts (empty name reference '#',empty value placeholder ':'). The service answerstoken: "#", near: "#é"by the same window rule.#v = 1: the service refuses a bare number as an operand. ExtendDB reads it as an attribute name.; expression size: <n>on filter and condition expressions. ExtendDB does.1 validation error detected:prefix the service adds on write paths.Not measured: the service's answer to a caller fenced by
dynamodb:LeadingKeysfor such a key condition. ExtendDB answers ValidationException underForAllValuesand AccessDeniedException underForAnyValue, with no items either way.Files
tokenizer.rspassed 600 lines with the fix, so the second commit moves its tests intotokenizer_tests.rswith no other change, using the#[path]pattern ofevaluator_tests.rs.tokenizer.rsis now 449 lines.Other PRs
git merge-tree --write-treeis clean against #558 and #479. #511 is already on main. #558 builds a window for parser faults from the same rule. Its window also starts at the first token when nothing comes before the fault. The service starts at the start of the expression. #558 fixes that separately.Why
This PR began as a fix for a panic. The tokenizer behind condition and filter expressions sliced the input five bytes either side of the offending byte. When that range ended inside a multi-byte character (
#v = ééé,#v = 日本語), the slice panicked, the request task ended and the client saw the connection close. #791 has since fixed that panic on main. It moves both ends of the five-byte window out to character boundaries and reports the whole character as the token.The windows on main still differ from the service's. The condition and filter tokenizer answers five bytes either side of the character. The tokenizer behind
UpdateExpression,ProjectionExpressionand the engine's key condition parse answers the character with one byte after it as the window (near: "é",near: "$x").What the service answers
Measured on 2026-10-25 at 16:09, 16:34, 16:38, 16:43 and 16:54 UTC in us-east-1, on all five expression kinds. Each request went out twice and got the same answer both times. When the character is the first fault in the expression:
é,日,😀, or$,@and!for ASCII characters outside the grammar.#v = éééanswerstoken: "é", near: "= éé".abéc = :vanswersnear: "abéc".#v = é abcanswersnear: "= é abc".#v é<> :vanswersnear: "#v é<>".contains(#v, é)answersnear: ", é)".é = :vanswersnear: " é =".#v = \^@xanswerstoken: "", near: "= x".The merge with main
The merge of main (807937e) keeps this rule and drops main's five-byte window. #791 added three tests:
tokenize_multibyte_utf8_near_an_invalid_byte_does_not_panicstays as fix(core): keep the tokenizer's error context on character boundaries #791 wrote it. Its one window assertion,$ 日for the input$ 日, matches the measured rule.tokenize_reports_the_whole_invalid_characterkeeps its token assertion. Its window fora = échanges froma = éto= é, the window that the measured row for#v = égives.tokenize_ascii_error_context_is_five_bytes_either_side. It expects the windows(a) $ bforattribute_exists(a) $ b, and the measured rule answers) $ b.Testing done
crates/core/src/expression/tokenizer_tests.rs: one table test with 44 measured rows, run through both tokenizers. The test fails on each of these broken versions of the rule:tests/test_expression_unknown_character.py: 50 dual-target wire tests on Scan and Query filters, PutItem and UpdateItem conditions, Query key conditions, UpdateItem update expressions and GetItem projections. The Query key condition rows include the shape that closed the connection before fix(core): keep the tokenizer's error context on character boundaries #791.On the merge commit 807937e, on 2026-11-05, against a SQLite debug build:
cargo +1.97.0 fmt --all -- --check: no changes.cargo +1.97.0 clippy --locked -p extenddb-core --all-targets -- -D warnings: no warnings.cargo test --locked -p extenddb-core --lib: 765 passed, 1 ignored.tests/test_expression_unknown_character.py: 50 passed, and the server log has no panic.On 2026-10-25, before #791 merged, against a SQLite release build:
cargo test -p extenddb-coreon stable (521 passed) and on 1.88.0 (520 passed).The full SQLite wire suite at the third commit had 2 failures, in
tests/test_gsi_async.py. That file starts a server from./target/release/extenddb, a path that did not exist in that run. main failed the same 2. CI runs the new file inrun-integration,run-integration-sqlite,mongodb-pytestandcassandra-pytest.Checklist
cargo test --workspace)cargo fmt --check)cargo clippy -- -W clippy::pedantic)Storagetrait, auth model, on-diskformat, or public CLI surface, an RFC has been accepted or is linked
below. Otherwise, an ADR captures the decision (link below).
ADR / RFC: n/a
By submitting this pull request, I confirm that my contribution is made under
the terms of the Apache License 2.0 and I agree to the Developer Certificate of
Origin (DCO). See CONTRIBUTING.md for details.