Skip to content

canvas: Apply large size limitations#36445

Merged
mrobinson merged 1 commit intoservo:mainfrom
tharkum:canvas2d-size-large
Apr 29, 2025
Merged

canvas: Apply large size limitations#36445
mrobinson merged 1 commit intoservo:mainfrom
tharkum:canvas2d-size-large

Conversation

@tharkum
Copy link
Copy Markdown
Contributor

@tharkum tharkum commented Apr 10, 2025

To prevent any potential crash/OOM issues with "canvas" element
from "rogue" applications let's apply large size limitations for context
canvas2d's draw target to Servo (similar approach in Firefox/Chromium -
they limits width and height to 32767/65535 pixels).

Fixes: #36155, #34117, #30164, #24710

--

  • ./mach build -d does not report any errors
  • ./mach test-tidy does not report any errors
  • There are tests for these changes
    tests/wpt/tests/html/canvas/element/canvas-host/2d.canvas.host.size.large.html
    tests/wpt/tests/html/canvas/offscreen/canvas-host/2d.canvas.host.size.large.html
    tests/wpt/tests/html/canvas/offscreen/canvas-host/2d.canvas.host.size.large.worker.js

@tharkum tharkum requested a review from gterzian as a code owner April 10, 2025 12:52
@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 10, 2025

cc @xiaochengh, @sagudev < Please review.

@tharkum tharkum force-pushed the canvas2d-size-large branch from c3d8cdb to 50dd0c8 Compare April 11, 2025 11:50
@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 11, 2025

@xiaochengh < I decided to simplify PR and remove the "lazy allocation of canvas draw target" functionality
and prepare another PR for it.
So now this PR has only "is_paintable" validation for HTMLCanvasElement.

Copy link
Copy Markdown
Contributor

@xiaochengh xiaochengh left a comment

Choose a reason for hiding this comment

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

Thanks for splitting it up and making it easier to review!

_ => false,
};

(paintable && !size.is_empty()) || is_supported_canvas_size(size)
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.

Could you explain why OffScreenCanvas needs to check more than paintable?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Contributor

@xiaochengh xiaochengh Apr 11, 2025

Choose a reason for hiding this comment

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

In an early message you mentioned that WebGL needs this check as a fixup, in which case I believe it should be moved to WebGL related types.

If it's general, then could you elaborate the difference between the paintability of a DrawTarget and that of a canvas (be it HTMLCanvas or offscreen)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

HTMLCanvasElement "paintable" (top level)

  • ContextCanvas2D DrawTarget "paintable" (low level)
  • WebGL/WebGPU DrawingBuffer "paintable" (low level)

There are some places in code there we could/must check possibility to snapshot image data OR draw from other canvas and current proposal was to up checking of "paintability" to top level instead of waiting until it will be done at context level (but it will be more CPU resourceble because we can do validation twice, but could help on some edge cases).

NOTE:
"Paintable" on low level (per context) will be validated against draw target size limitations + context lost status (for GPU accelerated rendering - not yet supported on 100%)

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 11, 2025

Canvas context is paintable if:

"2d": draw target is paintable (depends from canvas size)
"webgl": always true (no context lost tracking)
"webgpu": always true

Hm, do we really need paintable concept? Looking at this PR give me idea to simply move canvas size handling to internal get_image_data of contexts: sagudev@1f34641 that builds on top of #36119 (while still not fixing max size, but that should be only mater of additional checks in get_image_data of 2d canvas context). WDYT?

@tharkum tharkum force-pushed the canvas2d-size-large branch from 50dd0c8 to af0e53a Compare April 11, 2025 13:27
@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 11, 2025

Canvas context is paintable if:
"2d": draw target is paintable (depends from canvas size)
"webgl": always true (no context lost tracking)
"webgpu": always true

Hm, do we really need paintable concept? Looking at this PR give me idea to simply move canvas size handling to internal get_image_data of contexts: sagudev@1f34641 that builds on top of #36119 (while still not fixing max size, but that should be only mater of additional checks in get_image_data of 2d canvas context). WDYT?

Your change with snapshot concept is a bit large, so need time to check it well, but at first view "paintable" concept is more top-level compare with "snapshot".

Instead of doing "paintable" validation inside each context method implementation we will do it inside HTMLCanvasElement.

"Paintable" concept will allow to do:

  1. Preliminary check before call "get_image_data" from "createImageBitmap/toBlob/toDataURL" methods
    and to return transparent black image ("snapshot::cleared") if canvas is not paintable.
  2. Don't execute CanvasContext2D operations on non paintable draw target

Anyway I will check your PR and return back with more feedback.

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 11, 2025

transparent black image ("snapshot::cleared") if canvas is not paintable

The problem here is that snapshot::cleared will do allocation, so in case of too big canvas we would still get OOM.

@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 11, 2025

transparent black image ("snapshot::cleared") if canvas is not paintable

The problem here is that snapshot::cleared will do allocation, so in case of too big canvas we would still get OOM.

https://github.com/servo/servo/pull/36119/files#diff-7483a0f0ae501ab9da1432c28321c22f39647521072498f773903f360b026ce3R382

How about to replace it to this "is_paintable" check (included validation of max canvas size) to avoid OOM?
if !self.paintable() ~~~~ ! (context_paintable || is_supported_canvas_size(size))

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 11, 2025

transparent black image ("snapshot::cleared") if canvas is not paintable

The problem here is that snapshot::cleared will do allocation, so in case of too big canvas we would still get OOM.

https://github.com/servo/servo/pull/36119/files#diff-7483a0f0ae501ab9da1432c28321c22f39647521072498f773903f360b026ce3R382

How about to replace it to this "is_paintable" check (included validation of max canvas size) to avoid OOM? if !self.paintable() ~~~~ ! (context_paintable || is_supported_canvas_size(size))

That, would work, but that's included in sagudev@1f34641 (which I pushed just now, but already linked before). Actually rather than putting spotlight on my PR I just wanted to put it on sagudev@1f34641, which does similar as paintable but for each context in get_image_data. I think this is important for two reasons:

  • context have context specific requirements
  • makes get_image_data on specific context without HTMLCanvasElement actually usable in edge cases.

So I am questioning reasoning for this:

Instead of doing "paintable" validation inside each context method implementation we will do it inside HTMLCanvasElement.

Is there any reason why we cannot do this in each context and I am missing it?

@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 14, 2025

transparent black image ("snapshot::cleared") if canvas is not paintable

The problem here is that snapshot::cleared will do allocation, so in case of too big canvas we would still get OOM.

https://github.com/servo/servo/pull/36119/files#diff-7483a0f0ae501ab9da1432c28321c22f39647521072498f773903f360b026ce3R382
How about to replace it to this "is_paintable" check (included validation of max canvas size) to avoid OOM? if !self.paintable() ~~~~ ! (context_paintable || is_supported_canvas_size(size))

That, would work, but that's included in sagudev@1f34641 (which I pushed just now, but already linked before). Actually rather than putting spotlight on my PR I just wanted to put it on sagudev@1f34641, which does similar as paintable but for each context in get_image_data. I think this is important for two reasons:

  • context have context specific requirements
  • makes get_image_data on specific context without HTMLCanvasElement actually usable in edge cases.

So I am questioning reasoning for this:

Instead of doing "paintable" validation inside each context method implementation we will do it inside HTMLCanvasElement.

Is there any reason why we cannot do this in each context and I am missing it?

@sagudev < Sorry for long reply...

I viewed your "snapshot" PR and "yes" after your unification of "get_image_data" my proposed "paintable" concept
for HTMLCanvasElement could be omitted, because we have only one useful consumer (get image data)
of this change on top-level and it is more convenient to check restrictions on context level instead
of HTML canvas element.

There are one possible (but not mandatory) consumer of it is "CanvasState::draw_image_internal" for HTMLCanvasElement/OffscreenCanvas, but it could also be done per context level right now (with software image drawing).

So I can propose to cut-off "HTMLCanvasElement::is_paintable" change from PR and preserve only "CanvasState::is_paintable" for DrawTarget (to validate canvas size).

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 14, 2025

So I can propose to cut-off "HTMLCanvasElement::is_paintable" change from PR and preserve only "CanvasState::is_paintable" for DrawTarget (to validate canvas size).

Sounds good to me.

BTW I created test site that allow to set custom canvas size on different context, to see how other browsers respond to edge cases (and to see what they return on toDataUrl), but I haven't got the time to test any browser https://sagudev.github.io/briefcase/canvas-sizes.html (it does not work on servo tho).

@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 14, 2025

So I can propose to cut-off "HTMLCanvasElement::is_paintable" change from PR and preserve only "CanvasState::is_paintable" for DrawTarget (to validate canvas size).

Sounds good to me.

BTW I created test site that allow to set custom canvas size on different context, to see how other browsers respond to edge cases (and to see what they return on toDataUrl), but I haven't got the time to test any browser https://sagudev.github.io/briefcase/canvas-sizes.html (it does not work on servo tho).

There are some existed application for testing canvas size:
https://jhildenbiddle.github.io/canvas-size/#/

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 14, 2025

So I can propose to cut-off "HTMLCanvasElement::is_paintable" change from PR and preserve only "CanvasState::is_paintable" for DrawTarget (to validate canvas size).

Sounds good to me.
BTW I created test site that allow to set custom canvas size on different context, to see how other browsers respond to edge cases (and to see what they return on toDataUrl), but I haven't got the time to test any browser https://sagudev.github.io/briefcase/canvas-sizes.html (it does not work on servo tho).

There are some existed application for testing canvas size: https://jhildenbiddle.github.io/canvas-size/#/

But they do not test if they actually work or what does canvas methods return, the ones we are most interested for this PR are createImageBitmap, toDataUrl and toBlobUrl.

@tharkum tharkum force-pushed the canvas2d-size-large branch from af0e53a to c5d98ad Compare April 14, 2025 07:46
@tharkum tharkum requested a review from xiaochengh April 14, 2025 13:24
Copy link
Copy Markdown
Contributor

@xiaochengh xiaochengh left a comment

Choose a reason for hiding this comment

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

LGTM.

Thanks for further splitting the PR and making it nice and clean!

@sagudev sagudev self-requested a review April 15, 2025 17:41
Copy link
Copy Markdown
Member

@mrobinson mrobinson left a comment

Choose a reason for hiding this comment

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

Looks good, but I have a few very small suggestions:

Comment on lines +1955 to +1964
if size.is_empty() {
return false;
}
if size.width > MAX_CANVAS_SIZE || size.height > MAX_CANVAS_SIZE {
return false;
}
if size.area() > MAX_CANVAS_AREA {
return false;
}
true
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would just combine this one:

Suggested change
if size.is_empty() {
return false;
}
if size.width > MAX_CANVAS_SIZE || size.height > MAX_CANVAS_SIZE {
return false;
}
if size.area() > MAX_CANVAS_AREA {
return false;
}
true
!size.is_empty() &&
size <= Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE) &&
size.area() < MAX_CANVAS_AREA

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

And size <= Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE) is not supported
(PartialOrd is not implemeted for Size2D), so I replace it to
size.greater_than(Size2D::new(MAX_CANVAS_SIZE, MAX_CANVAS_SIZE)).none()

}
}

fn get_paintable_size(&self) -> Size2D<u64> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Getters in Rust usually don't use get_: https://rust-lang.github.io/api-guidelines/naming.html#getter-names-follow-rust-convention-c-getter:

Suggested change
fn get_paintable_size(&self) -> Size2D<u64> {
fn paintable_size(&self) -> Size2D<u64> {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I know about Rust naming policy, but I was confused by existed getter methods names (like get_ipc_renderer, get_canvas_id), so I decided that in Servo are using non Rust naming policy is OK.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Servo is very old and predates Rust naming policy, so I think general rule of thumb is that new stuff try to follow it, but existing stuff can keep the old names.


#[derive(Clone, Debug, JSTraceable, MallocSizeOf)]
/// Helps observe states of canvas draw target.
struct DrawTarget {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The use of DrawTarget here is a little bit confusing, because this isn't the target of the draw itself, but metadata about it. I would call it DrawTargetSize.

Suggested change
struct DrawTarget {
struct DrawTargetSize {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes, it has mostly metadata in itself, but the goal is to have mirrored version (with size + state) of canvas draw target but on script thread. Later I have plan to add delayed creation of draw target for Canvas2D context (was cutoff from current version of PR) and it will add new filed "created/allocated" to this struct.

I was inspired by existed WebGPU implementation in Servo - it also have the same thing - metadata version of WebGPU drawing buffer on script thread.
https://github.com/servo/servo/blob/main/components/script/dom/webgpu/gpucanvascontext.rs#L54

Canvas2D output bitmap -> CanvasState::DrawTarget
WebGPU drawing buffer -> GPUCanvasContext:DrawingBuffer
WebGL drawing buffer -> None

Anyway if after you and/or @sagudev decide to rename it to DrawTargetSize - no problem.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay. That plan sounds interesting. For now let's give it a name that matches what it does. It's no problem to rename it later.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was inspired by existed WebGPU implementation in Servo - it also have the same thing - metadata version of WebGPU drawing buffer on script thread.

The reason why we named it this way in WebGPU is because it matches the spec and it's also used referenced in spec steps (this way also out code matches spec better).

But here there are no exact steps written out in spec so either name is fine by me.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Okay. Let's call this DrawTargetSize then. Thanks!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@mrobinson < To do member renaming also?

draw_target -> draw_target_size?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Yes. Thanks!

const MAX_CANVAS_SIZE: u64 = 65535;

#[derive(Clone, Debug, JSTraceable, MallocSizeOf)]
/// Helps observe states of canvas draw target.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Please put documentation above the derive line and perhaps expand to:

Suggested change
/// Helps observe states of canvas draw target.
/// Information about the canvas draw target size and whether the size is valid.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done

}
}

pub(crate) fn is_supported_canvas_size(size: Size2D<u64>) -> bool {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Does this need to be pub(crate)? I would actually make this a static method on DrawTargetSize above.

impl DrawTargetSize
 ...
 fn is_supported_canvas_size()
...

Then you can call it using Self::is_supported_canvas_size(...).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would keep this in the impl block of DrawTargetSize.

@tharkum tharkum force-pushed the canvas2d-size-large branch 2 times, most recently from 5e429aa to 9a251f3 Compare April 16, 2025 08:29
@tharkum tharkum requested a review from mrobinson April 16, 2025 09:53
Comment on lines +87 to +93
fn paintable_size(&self) -> Size2D<u64> {
if self.paintable {
self.size
} else {
Size2D::zero()
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

So we will use paintable size for toDataUrl,toBlobUrl and similar (hence in edge cases we will error), but when rendering we will simply return empty htmlcanvassource (transparent black image as handled by WR/Layout), right? Can we document this somewhere in code.

@tharkum tharkum force-pushed the canvas2d-size-large branch from 9a251f3 to f4a8b32 Compare April 18, 2025 07:07
@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 18, 2025

@mrobinson @sagudev < After your suggestions I reviewed PR code and decide to simplify a little bit all this mess "DrawTarget/DrawTargetSize" thing a lot. So i simply replaced it with just "size" field and use it as "paintable" marker for canvas state.

@sagudev < Regarding "toDataUrl,toBlobUrl" - good catch!
I didn't checked it well before and now I added state validation to prevent any pixel reading IPC call for non-paintable canvas (it be handled properly on top level in different places as transparent black pixel data with natural canvas size!).
After your "snapshot" PR will be landed it will be much more better handled in one place as snapshot::cleared().

Comment on lines +144 to +151
if self.canvas_state.is_paintable() {
Some(
self.canvas_state
.get_rect(self.size(), Rect::from_size(self.size())),
)
} else {
None
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we do this as part of get_rect?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed as CanvasState::get_rect() -> Option<Vec<u8>>

Copy link
Copy Markdown
Member

@mrobinson mrobinson left a comment

Choose a reason for hiding this comment

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

Looks good. Sorry for the long delay in reviewing!

@tharkum tharkum force-pushed the canvas2d-size-large branch from de29e50 to d26c041 Compare April 29, 2025 12:49
@tharkum
Copy link
Copy Markdown
Contributor Author

tharkum commented Apr 29, 2025

@mrobinson @sagudev < Please review.

I rebased it on top of "main" branch with "snapshot" feature.

To prevent any potential crash/OOM issues with <canvas> element
from "rogue" applications let's apply upper size limitations
for rendering context's output bitmap (draw target) to Servo
(similar approach in Firefox/Chromium -
they limits width and height to 32767/65535 pixels).

Bug: servo#36155

Signed-off-by: Andrei Volykhin <[email protected]>
@tharkum tharkum force-pushed the canvas2d-size-large branch from d26c041 to 4f7f22f Compare April 29, 2025 12:59
@mrobinson mrobinson added this pull request to the merge queue Apr 29, 2025
Merged via the queue into servo:main with commit 6b2a755 Apr 29, 2025
21 checks passed
@TimvdLippe
Copy link
Copy Markdown
Contributor

FYI the mentioned issues haven't been closed as they weren't part of separate lines. GitHub only closes the first issue in such a list.

@sagudev
Copy link
Copy Markdown
Member

sagudev commented Apr 29, 2025

FYI the mentioned issues haven't been closed as they weren't part of separate lines. GitHub only closes the first issue in such a list.

Done.

@tharkum tharkum deleted the canvas2d-size-large branch April 30, 2025 06:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Intermittent OK in /html/canvas/element/canvas-host/2d.canvas.host.size.large.html

5 participants