uaf: Dupe blobs before using them, or else risk revokeObjectURL

When using a blob that might live through a JS call, dupe it so that any
subsequent revokeObjectURL doesn't invalidate the memory.
This commit is contained in:
Karl Seguin
2026-07-15 17:24:35 +08:00
parent e3c41d152e
commit fbe229c4bd
4 changed files with 34 additions and 5 deletions

View File

@@ -670,8 +670,11 @@ pub fn navigate(self: *Frame, request_url: [:0]const u8, opts: NavigateOpts) !vo
};
const parse_arena = try self.getArena(.medium, "Frame.parseBlob");
defer self.releaseArena(parse_arena);
// A script executed mid-parse can revoke the blob URL, letting GC
// free the buffer under the parser; parse a copy.
const html = try parse_arena.dupe(u8, blob._slice);
var parser = Parser.init(parse_arena, self.document.asNode(), self, .{ .allow_declarative_shadow = true });
parser.parse(blob._slice);
parser.parse(html);
} else {
self.document.injectBlank(self) catch |err| {
log.err(.browser, "inject blank", .{ .err = err });

View File

@@ -483,3 +483,26 @@
});
}
</script>
<script id="worker_from_blob_url_revoked_before_delivery" type=module>
// Revoking right after construction must not invalidate the queued
// script response: it's delivered on a later tick, after the blob's
// last reference may be gone.
{
const state = await testing.async();
const blob = new Blob(["onmessage = (e) => postMessage({ echo: e.data, from: 'revoked-blob-worker' });"], { type: 'text/javascript' });
const blobUrl = URL.createObjectURL(blob);
const worker = new Worker(blobUrl);
URL.revokeObjectURL(blobUrl);
worker.onmessage = function(event) {
state.resolve(event.data);
};
worker.postMessage({ greeting: 'still-works' });
await state.done((response) => {
testing.expectEqual('still-works', response.echo.greeting);
testing.expectEqual('revoked-blob-worker', response.from);
});
}
</script>

View File

@@ -199,8 +199,10 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
return;
}
// Perform the read (synchronous since data is in memory)
const data = blob._slice;
// Perform the read (synchronous since data is in memory). _result
// outlives this call and the blob can be GC'd before JS reads it,
// so the result must not borrow the blob's memory.
const data = try self._arena.dupe(u8, blob._slice);
const size = data.len;
try self.dispatch(.progress, .{ .loaded = size, .total = size }, exec);
if (self._aborted) {

View File

@@ -2959,8 +2959,9 @@ const Synthetic = struct {
const owner = transfer.owner orelse return error.BlobNotFound;
const blob_urls = owner.blob_urls orelse return error.BlobNotFound;
const blob = blob_urls.get(url) orelse return error.BlobNotFound;
content_type = blob._mime;
body = blob._slice;
// blob can be removed by the time we run, dupe it.
content_type = try arena.dupe(u8, blob._mime);
body = try arena.dupe(u8, blob._slice);
}
const has_content_type = content_type.len > 0;