diff --git a/src/base64/lib.rs b/src/base64/lib.rs index 2775f4b6a5bd..55a68d6e6f96 100644 --- a/src/base64/lib.rs +++ b/src/base64/lib.rs @@ -293,10 +293,14 @@ pub mod vlq { let mut len: u8 = 0; let mut bytes: [u8; VLQ_MAX_IN_BYTES] = [0; VLQ_MAX_IN_BYTES]; + // Sign-magnitude: i32::MIN has no representation (its magnitude + // overflows the u32 VLQ), so it wraps to "-0" instead of panicking. + // The crash handler encodes bitcast u32 address halves through here + // and must not panic while already reporting a crash. let mut vlq: u32 = if value >= 0 { (value << 1) as u32 } else { - ((-value << 1) | 1) as u32 + (value.unsigned_abs() << 1) | 1 }; // source mappings are limited to i32 @@ -404,6 +408,30 @@ pub mod vlq { pub fn decode_assume_valid(encoded: &[u8], start: usize) -> VLQResult { decode_impl::(encoded, start) } + + #[cfg(test)] + mod tests { + use super::*; + + #[test] + fn encode_decode_roundtrip() { + for value in [0, 1, -1, 255, 256, -255, -256, i32::MAX, i32::MIN + 1] { + let encoded = VLQ::encode(value); + let result = decode(encoded.slice(), 0); + assert_eq!(result.value, value); + assert_eq!(result.start, encoded.len as usize); + } + assert_eq!(VLQ::encode(i32::MAX).slice(), b"+/////D"); + assert_eq!(VLQ::encode(i32::MIN + 1).slice(), b"//////D"); + } + + #[test] + fn encode_i32_min_does_not_panic() { + // i32::MIN is outside the sign-magnitude domain; it wraps to "-0". + let encoded = VLQ::encode(i32::MIN); + assert_eq!(decode(encoded.slice(), 0).value, 0); + } + } } pub mod zig_base64 { diff --git a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts index accfe09b6726..1f93f2dde36e 100644 --- a/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts +++ b/test/js/bun/sourcemap/internal-sourcemap-roundtrip.test.ts @@ -251,6 +251,48 @@ describe("InternalSourceMap.fromVLQ validation", () => { }); }); +describe("InternalSourceMap.toVLQ", () => { + test.concurrent("window state of i32::MIN must not crash the VLQ encoder", async () => { + // SyncEntry state is raw i32, so a blob whose first window starts at + // generated column i32::MIN makes appendVLQTo pass a delta of exactly + // i32::MIN to VLQ.encode. i32::MIN has no sign-magnitude representation; + // the encoder's magnitude negation overflowed on it (debug builds abort + // with "attempt to negate with overflow"). The crash handler reaches the + // same encoder edge with bitcast u32 address halves while reporting a + // crash. fromVLQ rejects negative absolutes, so craft the blob by hand. + // Run in a child process so the abort is recorded as a failure here + // instead of taking down the test runner. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + `const { internalSourceMap } = require("bun:internal-for-testing"); + // Blob layout (see src/sourcemap/InternalSourceMap.rs): + // [0..8] total_len u64, [8..16] mapping_count u64, + // [16..24] input_line_count u64, [24..28] sync_count u32, + // [28..32] stream_offset u32, [32..56] SyncEntry, [56..88] window + // header (count=1, no deltas), [88] stream tail pad. + const blob = new Uint8Array(89); + const dv = new DataView(blob.buffer); + dv.setBigUint64(0, 89n, true); // total_len + dv.setBigUint64(8, 1n, true); // mapping_count + dv.setUint32(24, 1, true); // sync_count + dv.setUint32(28, 56, true); // stream_offset + dv.setInt32(36, -2147483648, true); // SyncEntry.generated_column + blob[56] = 1; // window mapping count + console.log("TOVLQ: " + internalSourceMap.toVLQ(blob));`, + ], + env: bunEnv, + stdout: "pipe", + stderr: "ignore", + }); + const [stdout, exitCode] = await Promise.all([proc.stdout.text(), proc.exited]); + // i32::MIN wraps to the "-0" encoding ("B"); the other three fields are 0. + expect(stdout).toBe("TOVLQ: BAAA\n"); + expect(exitCode).toBe(0); + }); +}); + describe("InternalSourceMap round-trip", () => { test("synthetic: fromVLQ → toVLQ preserves all 4-field positions; names dropped, 1-field skipped", () => { const vlqIn = buildSyntheticVLQ();