Skip to content

Icon::from_rgba accepts a 0x0 icon that to_png then panics on (ZeroWidth) #390

Description

@salarsayyad

Summary

RgbaIcon::from_rgba validates only that the buffer length matches the stated
dimensions, so a 0x0 icon constructs successfully and panics later at render
time. Still present at muda-v0.19.3:

// src/icon.rs:82
pub fn from_rgba(rgba: Vec<u8>, width: u32, height: u32) -> Result<Self, BadIcon> {
    if rgba.len() % PIXEL_SIZE != 0 { /* ByteCountNotDivisibleBy4 */ }
    let pixel_count = rgba.len() / PIXEL_SIZE;
    if pixel_count != (width * height) as usize {
        Err(BadIcon::DimensionsVsPixelCount { .. })
    } else {
        Ok(RgbaIcon { rgba, width, height })
    }
}

from_rgba(vec![], 0, 0) satisfies this: 0 == 0. Rendering it then panics:

// src/platform_impl/macos/icon.rs:34
let mut writer = encoder.write_header().unwrap();
called `Result::unwrap()` on an `Err` value: Format(FormatError { inner: ZeroWidth })

A zero-sized icon is therefore legal to construct and fatal to render, and
the failure surfaces far from the call that created it — in a menu-item action
handler, on the main thread, inside an Objective-C callback where unwinding is
not an option.

Reproduction

let icon = muda::Icon::from_rgba(Vec::new(), 0, 0).unwrap(); // Ok
let item = muda::IconMenuItem::new("x", true, Some(icon), None); // panics on render

Any path reaching PlatformIcon::to_nsimage will do: IconMenuItem::new,
MenuChild::set_icon, or AboutMetadata { icon: Some(..), .. }.

Why this is worth tightening

This is the reason #328 presented the way it did, and it cost real debugging
time. We hit the same ZeroWidth panic through a different route — a window
menu bar via dioxus-desktop, not a tray menu — without our application ever
constructing an icon. The freed MenuChild read back as
Some(RgbaIcon { rgba: [], width: 0, height: 0 }), a shape this validation
accepts, so instead of faulting at the corruption the process aborted inside
the PNG encoder with a message pointing squarely at icons. We spent a while
looking for code that built an icon; there wasn't any.

Rejecting zero dimensions would not have prevented that use-after-free, but it
would have turned a misleading abort into either an honest fault or an error at
the boundary. It also closes the "valid to construct, invalid to use" gap for
legitimate callers, who today get no signal until render time.

Suggested fix

Reject zero dimensions in the constructor, where the caller can still handle it:

if width == 0 || height == 0 {
    return Err(BadIcon::ZeroSized { width, height });
}

A new BadIcon variant is a breaking change to that enum; if that is unwelcome
before the next major, DimensionsVsPixelCount reads acceptably for this case.

Secondarily, the two unwraps in to_png run on the main thread inside an ObjC
callback. Even with a stricter constructor, propagating rather than unwrapping
would stop malformed image data from aborting the host application.

Environment

Reproduced against muda 0.17.2 (via dioxus-desktop 0.7.10) on macOS 26.0 and
27.0, Apple silicon. Validation and to_png are unchanged at muda-v0.19.3.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions