Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions src/runtime/webcore/Blob.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1970,10 +1970,12 @@ impl BlobExt for Blob {
return Ok(unsafe { BlobExt::to_js(&*ptr, global_this) });
}

let size_i64 = i64::try_from(self.size.get()).unwrap_or(i64::MAX);

// If the optional start parameter is not used as a parameter, let relativeStart be 0.
let mut relative_start: i64 = 0;
// If the optional end parameter is not used, let relativeEnd be size.
let mut relative_end: i64 = i64::try_from(self.size.get()).expect("int cast");
let mut relative_end: i64 = size_i64;

// Mutate the fixed-3 args array in place to shift the string arg into [2].
if args[0].is_string() {
Expand All @@ -1990,11 +1992,9 @@ impl BlobExt for Blob {
if start_.is_number() {
let start = start_.to_int64();
if start < 0 {
relative_start = (start
.wrapping_add(i64::try_from(self.size.get()).expect("int cast")))
.max(0);
relative_start = start.saturating_add(size_i64).max(0);
} else {
relative_start = start.min(i64::try_from(self.size.get()).expect("int cast"));
relative_start = start.min(size_i64);
}
}
}
Expand All @@ -2003,11 +2003,9 @@ impl BlobExt for Blob {
if end_.is_number() {
let end = end_.to_int64();
if end < 0 {
relative_end = (end
.wrapping_add(i64::try_from(self.size.get()).expect("int cast")))
.max(0);
relative_end = end.saturating_add(size_i64).max(0);
} else {
relative_end = end.min(i64::try_from(self.size.get()).expect("int cast"));
relative_end = end.min(size_i64);
}
}
}
Expand Down Expand Up @@ -4116,10 +4114,13 @@ fn on_structured_clone_deserialize<B: AsRef<[u8]>>(
}
PathOrFileDescriptorSerializeTag::Path => {
let path_len = reader.read_int_le::<u32>()?;
// Same constraints the JS entry (`Valid::path_string_length` /
// `Valid::path_null_bytes`) enforces: the path must fit a
// `PathBuffer` with its NUL and cannot embed a NUL.
if path_len as usize >= bun_paths::MAX_PATH_BYTES {
return Err(crate::Error::InvalidValue);
}
let path = read_slice(reader, path_len as usize)?;
// Same constraint the JS entry (`Valid::path_null_bytes`)
// enforces: a NUL-embedded path cannot be handed to the
// syscall layer (`ZStr::as_cstr` would truncate / panic).
if strings::index_of_char(&path, 0).is_some() {
return Err(crate::Error::InvalidValue);
}
Expand Down Expand Up @@ -4183,8 +4184,9 @@ fn on_structured_clone_deserialize<B: AsRef<[u8]>>(
// make shared_view() slice past the end of the backing store (OOB heap read).
blob.offset.set(offset as SizeType); // intentional truncate
if let Some(size) = file_size {
// resolve_size() clamps this to the actual file size on first use.
if size != MAX_SIZE {
// resolve_size() clamps this to the actual file size on first use;
// anything at or past MAX_SIZE is the "unknown" sentinel.
if size < MAX_SIZE {
blob.size.set(size as SizeType);
}
}
Expand Down
71 changes: 70 additions & 1 deletion test/js/web/structured-clone-blob-file.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { deserialize, serialize } from "bun:jsc";
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, isASAN, isWindows, rss } from "harness";
import { bunEnv, bunExe, isASAN, isWindows, rss, tempDir } from "harness";
import { join } from "node:path";
import v8 from "node:v8";

describe("structuredClone with Blob and File", () => {
Expand Down Expand Up @@ -522,6 +523,74 @@ describe("structuredClone with Blob and File", () => {
});
});

test("file-backed Blob with size past MAX_SIZE deserializes as unknown size and slices", async () => {
using dir = tempDir("blob-deser-size", { "probe.txt": "0123456789abcdefghij" });
const path = join(String(dir), "probe.txt");
// Plant a distinctive slice end so the u64 size field can be located
// in whatever framing the current build emits, then overwrite it.
const sentinel = 0x0badf00d1234;
const good = Buffer.from(serialize(Bun.file(path).slice(0, sentinel)));
const needle = Buffer.alloc(8);
needle.writeBigUInt64LE(BigInt(sentinel));
const at = good.indexOf(needle);
expect(at).toBeGreaterThan(0);
const bad = Buffer.from(good);
bad.writeBigUInt64LE(1n << 63n, at);

await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const { deserialize } = require("bun:jsc");
const v8 = require("node:v8");
const bad = Buffer.from(process.argv[1], "base64");
for (const de of [deserialize, b => v8.deserialize(b)]) {
const blob = de(bad);
const head = await blob.slice(0, 10).text();
process.stdout.write(JSON.stringify({ head, size: blob.size }) + "\\n");
}
`,
bad.toString("base64"),
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);

const lines = stdout
.split("\n")
.filter(Boolean)
.map(l => JSON.parse(l));
expect({ lines, stderr, exitCode, signalCode: proc.signalCode }).toEqual({
lines: [
{ head: "0123456789", size: 20 },
{ head: "0123456789", size: 20 },
],
stderr: "",
exitCode: 0,
signalCode: null,
});
});

test("file-backed Blob path longer than PATH_MAX is rejected at deserialize", () => {
const probe = Buffer.alloc(16, 0x5a);
const good = Buffer.from(serialize(Bun.file(probe.toString("latin1"))));
const at = good.indexOf(probe);
expect(at).toBeGreaterThan(4);
expect(good.readUInt32LE(at - 4)).toBe(probe.length);

// Longer than MAX_PATH_BYTES on every platform (Windows is ~96 KiB).
const long = Buffer.concat([Buffer.from("/"), Buffer.alloc(100_000, "a")]);
const len = Buffer.alloc(4);
len.writeUInt32LE(long.length);
const bad = Buffer.concat([good.subarray(0, at - 4), len, long, good.subarray(at + probe.length)]);

expect(() => deserialize(bad)).toThrow("Unable to deserialize data.");
expect(() => v8.deserialize(bad)).toThrow("Unable to deserialize data.");
});

test("in-process: offset at store boundary yields empty view", async () => {
// offset == store length stays within the allocation on any build, so
// this is safe to assert in-process and covers the boundary directly.
Expand Down
Loading