Skip to content

sql(mysql): reject out-of-range binary TIME fields instead of wrapping - #34706

Open
robobun wants to merge 2 commits into
mainfrom
farm/131c1b4c/mysql-time-binary-range-check
Open

sql(mysql): reject out-of-range binary TIME fields instead of wrapping#34706
robobun wants to merge 2 commits into
mainfrom
farm/131c1b4c/mysql-time-binary-range-check

Conversation

@robobun

@robobun robobun commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator

What does this PR do?

The MySQL binary-protocol TIME decoder read the 4-byte days field as a raw u32 and computed total_hours = hours + days * 24 in u32 with no range check. MySQL TIME is bounded to -838:59:59 .. 838:59:59 (days <= 34), so a hostile or buggy server/proxy sending days = 178956971 made days * 24 wrap past 2^32 to 8 and Bun returned the string "08:05:06" with no error. The adjacent DATETIME arm already validates month/day/hour/minute/second and surfaces Invalid Date; the TIME arm now rejects days > 34 / hours > 23 / minutes > 59 / seconds > 59 with ERR_MYSQL_INVALID_BINARY_VALUE the same way.

Repro

// mock MySQL server sends a binary TIME with days=178956971
// 178956971 * 24 = 4294967304 = 2^32 + 8  ->  wraps to total_hours = 8
const sql = new Bun.SQL(`mysql://u:p@127.0.0.1:${srv.port}/db`);
console.log(await sql`select ${1} as t`.values());
// before: [["08:05:06"]]
// after:  MySQLError { code: "ERR_MYSQL_INVALID_BINARY_VALUE" }

Other shapes of the same bug:

  • days = 0xFFFFFFFF decoded as "4294967272:05:06"
  • days = 178956970, hours = 255 decoded as "239:05:06"

How did you verify your code works?

New fault-injection test test/js/sql/sql-mysql-time-binary-range.test.ts drives a mock server that emits binary TIME cells with each field out of range and asserts the query rejects with ERR_MYSQL_INVALID_BINARY_VALUE. A boundary case (838:59:59, i.e. days = 34, hours = 22) confirms the documented maximum still decodes.

USE_SYSTEM_BUN=1 bun test <file>  -> 6 fail (resolve with "08:05:06", "00:00:60", ...), 1 pass
bun bd test <file>                -> 7 pass

The existing container round-trip in test/js/sql/sql-mysql.test.ts ("time" test, -838:59:59 / 838:59:59) is within the new bound.

Note

The assert build profile sets CARGO_PROFILE_RELEASE_DEBUG_ASSERTIONS=true (scripts/build/rust.ts) but not CARGO_PROFILE_RELEASE_OVERFLOW_CHECKS, so integer wraps in this whole class stay silent under ASAN/assert builds too. Enabling overflow-checks there would let fuzzing catch these; left out of this PR since it is a build-profile change with broader blast radius.

Time::from_binary reads the 4-byte days field as a raw u32 and the
TIME arm of decode_binary_value computed total_hours = hours + days*24
in u32 with no range check. MySQL TIME is bounded to +/-838:59:59
(days <= 34), so a hostile or buggy server/proxy sending days=178956971
made days*24 wrap past 2^32 to 8 and Bun returned the string
"08:05:06" with no error.

Reject days>34 / hours>23 / minutes>59 / seconds>59 with
InvalidBinaryValue, matching the range validation the DATETIME path
already does.
@robobun

robobun commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

Status

Reproduced with a mock server sending a binary TIME cell with days = 178956971: stock Bun decodes [["08:05:06"]] (u32 wrap in days * 24).

Fix: range-check the TIME fields in decode_binary_value before computing total_hours, and reject total_hours > 838 after, returning InvalidBinaryValue on violation.

  • USE_SYSTEM_BUN=1 bun test test/js/sql/sql-mysql-time-binary-range.test.ts: 7 fail, 1 pass
  • bun bd test test/js/sql/sql-mysql-time-binary-range.test.ts: 8 pass

CI #75769: the new test passes on all lanes. Remaining failures are unrelated to this diff: node-net.test.ts (pre-existing on main, mimalloc page-count threshold on alpine aarch64) and four flaky tests (terminal.test.ts / terminal-spawn.test.ts on Windows, no-orphans.test.ts on darwin, test-fs-read-stream-pos.js on ubuntu). Ready for review.

@robobun

robobun commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 7:52 AM PT - Jul 19th, 2026

@robobun, your commit 4526a95 has 4 failures in Build #75769 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 34706

That installs a local version of the PR into your bun-34706 executable, so you can run:

bun-34706 --bun

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6818ccbe-3344-4808-a2c3-0f2eb979b9e4

📥 Commits

Reviewing files that changed from the base of the PR and between 09ed21d and 4526a95.

📒 Files selected for processing (2)
  • src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs
  • test/js/sql/sql-mysql-time-binary-range.test.ts

Walkthrough

The MySQL binary-protocol TIME decoder now rejects out-of-range fields. Tests construct raw TIME rows through a prepared-statement server and verify invalid values fail while the documented maximum decodes successfully.

Changes

MySQL binary TIME validation

Layer / File(s) Summary
Binary TIME bounds validation
src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs
MYSQL_TYPE_TIME decoding rejects invalid days, hours, minutes, and seconds with InvalidBinaryValue.
Binary TIME protocol regression tests
test/js/sql/sql-mysql-time-binary-range.test.ts
Raw prepared-statement responses cover multiple invalid values and confirm successful decoding of 838:59:59.

Possibly related PRs

  • oven-sh/bun#34457 — Updates binary TIME parameter length handling in Time::to_binary, which is related to the decoder’s binary protocol behavior.

Suggested reviewers: alii, jarred-sumner

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: rejecting out-of-range MySQL binary TIME fields instead of wrapping.
Description check ✅ Passed The description matches the template and includes both the change summary and verification details, with additional helpful context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs`:
- Around line 137-138: Extend the TIME validation in DecodeBinaryValue.rs around
the existing per-field guard to reject the cross-field boundary where days=34
and hours exceed 22, or equivalently where the computed total hours exceed 838.
Add the corresponding days=34, hours=23 case to the test.each matrix in
test/js/sql/sql-mysql-time-binary-range.test.ts, expecting invalid binary value
handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f272a3c5-c65d-48ca-ab00-9f76c9cc722f

📥 Commits

Reviewing files that changed from the base of the PR and between 98f6649 and 09ed21d.

📒 Files selected for processing (2)
  • src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs
  • test/js/sql/sql-mysql-time-binary-range.test.ts

Comment thread src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs
The per-field guard allowed days=34 hours=23 (839 hours), one past the
documented +/-838:59:59 cap. days<=34 already bounds days*24 to 816 so
the u32 sum is overflow-free; add a total_hours > 838 check after it.
Comment thread src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs Outdated

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — the total_hours > 838 guard closes the cross-field boundary I flagged earlier, and the (34,23) case is now in the reject matrix.

What was reviewed:

  • Verified the per-field bound (days <= 34) makes days*24 overflow-safe in u32 before the sum, and the follow-up total_hours > 838 check seals the 839h gap.
  • Confirmed InvalidBinaryValue maps to ERR_MYSQL_INVALID_BINARY_VALUE (any_mysql_error_jsc.rs:102) as the test asserts, and Time field types (days: u32, hours/minutes/seconds: u8) match the guard arithmetic.
  • Test follows the existing wire-frames.ts fault-injection pattern used by ~20 sibling files; the 838:59:59 positive case pins the documented maximum so the guard can't over-tighten.
Extended reasoning...

Overview

Two files: a 9-line addition to the MYSQL_TYPE_TIME arm of decode_binary_value in src/sql_jsc/mysql/protocol/DecodeBinaryValue.rs that range-checks the wire fields (days > 34, hours > 23, minutes > 59, seconds > 59) and then rejects total_hours > 838, plus a new fault-injection test test/js/sql/sql-mysql-time-binary-range.test.ts driving a mock MySQL server via the shared wire-frames.ts helpers.

Security risks

The change is strictly defensive — it tightens validation of untrusted server bytes and turns a silent u32 wrap into a typed error. A compliant MySQL server never emits TIME outside ±838:59:59, so no legitimate input is newly rejected. No new parsing, no allocation, no user-controlled sizing; the error path uses the existing crate::Error::InvalidBinaryValue variant already used by sibling arms in this function and mapped to ERR_MYSQL_INVALID_BINARY_VALUE.

Level of scrutiny

Low-to-medium. It's a localized, additive bounds check in a single decode arm with no control-flow or type changes elsewhere. The arithmetic is trivially verifiable: with days <= 34, days*24 <= 816 (fits u32), plus hours <= 23 gives at most 839, which the second guard rejects. The Time struct (days: u32, hours/minutes/seconds: u8) matches the code's assumptions.

Other factors

  • All prior review threads (CodeRabbit's cross-field gap, my own stale nit on the same point) are resolved by commit 4526a95 and confirmed present in the current diff.
  • Test coverage hits the exact-limit-succeeds / one-past-fails contract REVIEW.md asks for: 7 reject cases (including the u32-wrap repro and the 839h boundary) and one accept case at 838:59:59, with sawExecute asserted so the test can't pass by short-circuiting before the row is decoded.
  • The test file reuses the established mock-server harness (listeningServer, mysqlHandshakeV10, mysqlStmtPrepareOk, mysqlReadPackets) already used by many sibling SQL fault-injection tests, and the header comment correctly scopes it as a do-not-copy pattern.
  • The bug-hunting system found nothing this run.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant