Skip to content

feat(fuzz): fuzz the driver's own parsers, and fix a silent time-zone overflow - #7

Merged
maltesander merged 3 commits into
mainfrom
feat/fuzz-targets
Aug 6, 2026
Merged

feat(fuzz): fuzz the driver's own parsers, and fix a silent time-zone overflow#7
maltesander merged 3 commits into
mainfrom
feat/fuzz-targets

Conversation

@maltesander

Copy link
Copy Markdown
Member

Description

Core fuzzes write_column_value (ColumnValue → caller's buffer). Nothing
covered the step before it, where a coordinator's JSON becomes that
ColumnValue, nor the type-signature parsers, the Trino escape dialect, or the
connection-string value parsing — those are this crate's, and three of them read
text chosen by the server.

Four targets in a fuzz/ workspace mirroring core's: json_value, type_name,
escape, connect_params. Each builds input from a grammar, since random bytes
never form a balanced {fn CONVERT(x, SQL_INTEGER)}. They reach the private
modules through a fuzz_api module behind a default-off fuzzing feature,
following how core gates test-support.

json_value found an unchecked i32 multiply on the time-zone offset in
parse_trino_time_with_tz, with the same defect in shift_time on the
time-of-day hour. Both fields are free-form text. This crate declares no
[profile.release], so overflow checks are off in the shipped driver: it did not
panic, it wrapped and returned a different time.
Both now use checked
arithmetic and fall back to text. A second commit hardens
parse_precision_param / parse_scale_param, which indexed a range built from
independent find/rfind calls — unreachable behind the TrinoTypeName::parse
gate, but the parsers should hold that property themselves.

Checklist

  • pre-commit run --all-files passes. It is the single source of truth for what must pass.
  • CHANGELOG.md has an entry under ## [Unreleased], if an ODBC application can observe the difference. A changed SQLSTATE, a changed SQLGetInfo value, a new connection-string key or a different type mapping all count.
  • Every client error added or moved goes through map_trino_error, which is the single place that decides the SQLSTATE and carries Trino's own error code through to SQLGetDiagRec.
  • New tests were checked by breaking the line they cover and watching them fail. A test that cannot fail reports coverage that does not exist.

If it applies

  • A new connection-string key is two edits: the parser in src/backend/types/connect_params.rs and the table in README.md. The Windows dialog is generated from the parser, and a test in src/lib.rs fails if the two disagree.
  • The integration suite was run against a live Trino: ./integration-tests/setup.sh then ./integration-tests/run-tests.sh. CI runs the Linux suites, so this is about anything you added to them.
  • The Windows suites were run in the VM, for anything touching the Windows Driver Manager, the installer or the DSN dialog. They do not run in CI. See integration-tests/windows/WINDOWS.md.
  • A defect found in stackable-odbc-core or in the Trino client was fixed where it lives, not worked around here, and this pull request says which version it needs.
  • The Power BI connector was rebuilt and loaded in Power BI Desktop, for anything under connector/.

Notes for the reviewer

maltesander and others added 3 commits August 5, 2026 23:34
stackable-odbc-core fuzzes `write_column_value`, which turns a `ColumnValue`
into the caller's buffer. Nothing covered the step before it, where a
coordinator's JSON becomes that `ColumnValue`, nor the type-signature parsing,
the Trino escape dialect or the connection-string value parsing. Those are this
crate's, and three of them read text chosen by the server.

Four targets in a `fuzz/` workspace, mirroring core's: `json_value`,
`type_name`, `escape` and `connect_params`. Each builds its input from a
grammar rather than from raw bytes, because a random byte string is not a
balanced `{fn CONVERT(x, SQL_INTEGER)}` and never reaches the parsers at all.

They reach the code through a new `fuzz_api` module behind a default-off
`fuzzing` feature, following how core gates `test-support`: the modules are
private on purpose, and one gated re-export keeps the fuzzed surface readable
in one screen instead of widening three modules item by item.

`json_value` found an unchecked `i32` multiply on the time-zone offset in
`parse_trino_time_with_tz`, and `shift_time` carried the same defect one frame
down on the time-of-day hour. Both fields are free-form text, so a value no
zone or clock could hold still parses as an `i32`. This crate declares no
`[profile.release]`, so overflow checks are off in the shipped driver: rather
than panicking it wrapped and reported a different time. Both now use checked
arithmetic and fall back to text, which is what every other unparseable
temporal value here already does.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…parsers

`parse_precision_param` and `parse_scale_param` locate the two ends of the
argument list independently, with `find('(')` and `rfind(')')`. Nothing about
the string guarantees the opening parenthesis comes first, and indexing a
reversed range panics.

Both callers gate on `TrinoTypeName::parse`, which no such name survives: the
stray `)` lands in the base name the gate matches on. So this is not a
reachable defect, and the `type_name` fuzz target confirms it across eighteen
million executions with a generator arm built to emit exactly that shape. It
does mean the gate is load-bearing for a property the parsers should hold on
their own.

`name.get(..)` rather than `name[..]`, which is what `parse_fraction_nanos`
already does a few lines down and for the same stated reason. Both functions
are now total: no input reaches a panic. Behaviour on every well-formed
signature is unchanged, and the tests assert both halves of that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@maltesander
maltesander requested a review from adwk67 August 6, 2026 06:45
@maltesander maltesander self-assigned this Aug 6, 2026

@adwk67 adwk67 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@maltesander
maltesander merged commit 6cebd52 into main Aug 6, 2026
9 checks passed
@maltesander
maltesander deleted the feat/fuzz-targets branch August 6, 2026 09:25
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.

2 participants