sql(mysql): fix truncated Time::to_binary when microseconds != 0#34457
sql(mysql): fix truncated Time::to_binary when microseconds != 0#34457robobun wants to merge 1 commit into
Conversation
The encoder writes buffer[0..=12] (1 length byte + 12 payload bytes) but returned 12, so Value::to_data copied only buffer[0..12] and dropped the high byte of the u32 microseconds. The length prefix in buffer[0] is 12, so the emitted packet would advertise 12 payload bytes but carry only 11, desynchronizing the COM_STMT_EXECUTE stream. The TIME encode path is currently unreachable from JS: field_type_from_js maps JS Date to MYSQL_TYPE_DATETIME, so Value::Time is never constructed and this branch never runs today. Fixed so enabling TIME param binding in the future does not hit it.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughChangesMySQL TIME encoding
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
|
Status: one-character correctness fix in an encoder that is currently unreachable from JS (see the PR body for the call-chain analysis). No fail-before JS test is constructible, and |
There was a problem hiding this comment.
LGTM — one-byte off-by-one fix, verifiably correct by inspection.
What was reviewed:
- Byte accounting:
buffer[0..=12]is written (length prefix + 12 payload bytes), so returning 13 matches; the siblingmicroseconds == 0branch (len=8, return 9) andDateTime::to_binary(len=11, return 12) follow the same length+1 pattern. to_data's stack buffer is[0u8; 15], so the new 13-byte slice is in bounds.- Confirmed the no-test justification:
field_type_from_jsnever yieldsMYSQL_TYPE_TIME, so this encoder is currently unreachable from JS.
Extended reasoning...
Overview
Single-character change in src/sql_jsc/mysql/MySQLValue.rs: the return value of Time::to_binary's microseconds-present branch changes from 12 to 13. The function writes a 1-byte length prefix (buffer[0] = 12) followed by 12 payload bytes (negative flag, 4-byte days, hours, minutes, seconds, 4-byte microseconds) into buffer[0..=12], so the caller (Value::to_data, which does Data::create(&buffer[0..pos])) needs pos = 13 to copy the full record. Returning 12 would drop the high byte of the little-endian microseconds and leave the on-wire record one byte short of its own length prefix.
Security risks
None. This is an internal binary-protocol encoder for MySQL TIME parameters; no untrusted input parsing, auth, or crypto is touched. The only bounds concern — whether index 12 fits in the caller's buffer — is satisfied ([0u8; 15]).
Level of scrutiny
Low. The fix is mechanical and self-evidently consistent with the two adjacent branches: microseconds == 0 writes 9 bytes and returns 9; DateTime::to_binary's 11-byte DATETIME branch writes 12 and returns 12. The MySQL binary protocol spec for TIME (length-prefixed 8 or 12 payload bytes) matches. There are no other call sites or sibling encoders sharing this bug.
Other factors
The PR ships without a test, which normally blocks approval per the repo's review rules. However, the description gives a concrete, checkable reason: Value::Time is only constructed via Value::from_js with FieldType::MYSQL_TYPE_TIME, and field_type_from_js (the only source of param field types via Signature::generate) maps JS Date to MYSQL_TYPE_DATETIME, never TIME. I verified this in the preloaded source. The code path is therefore dead today, a fail-before test is not constructible, and the crate can't host a standalone Rust unit test. Given the fix is a one-byte constant that makes an unreachable encoder internally consistent, deferring on the test requirement is reasonable here.
|
Closing: this cannot carry a regression test. The The off-by-one itself is real: the branch writes The diff stays on the branch |
What
Time::to_binary(the MySQL TIME binary-parameter encoder) writes 13 bytes whenmicroseconds != 0but returned 12, soValue::to_datacopiedbuffer[0..12]and droppedbuffer[12](the high byte of the little-endianu32microseconds). The length prefix inbuffer[0]is 12, so the packet would advertise 12 payload bytes but carry only 11, desynchronizing the COM_STMT_EXECUTE stream the moment this encoder runs.The
microseconds == 0branch (writes 9, returns 9) andDateTime::to_binary's DATETIME-with-microseconds branch (writes 12, returns 12) are already consistent; only this branch is off by one. The same bug was present in the pre-Rust Zig implementation.Why this has no JS test
This encode path is currently unreachable from JS.
Value::Timeis only constructed whenValue::from_jsis called withFieldType::MYSQL_TYPE_TIME, and the only source of that field type isSignature::generate→field_type_from_js, which maps a JSDatetoMYSQL_TYPE_DATETIME(neverTIME). The server-reported param types (statement.params, filled from the StmtPrepareOK column definitions) are decoded but only used for count checks; they never feed back into encoding. So today aDatebound to aTIME(6)column is sent as a DATETIME and coerced server-side, andTime::to_binarynever executes.A fail-before JS test is therefore not constructible, and
cargo test -p bun_sql_jsccannot link standalone (the crate transitively pulls in Windows link stubs on Linux), so a Rust unit test would not run either. The fix is applied so whoever later wires up TIME parameter binding (for example by honouring the server-reported param type) does not inherit a latent protocol desync.Spotted during code review on #34452.