Skip to content

macOS crash: NSBezierPath.appendArc with NaN/inf angles from SVG arc conversion #81

@leosebben

Description

@leosebben

Description

On macOS, rendering certain SVGs causes a crash with NSInvalidArgumentException ("illegal angle argument") when NSBezierPath.appendArc(withCenter:radius:startAngle:endAngle:clockwise:) receives NaN or inf values.

The root cause is in the SVG arc endpoint-to-center parameterization (as per W3C SVG spec) inside SVGPathReader.swift. Certain arc parameters produce degenerate geometry during the mathematical conversion, resulting in non-finite values that propagate to NSBezierPath.

On iOS this goes unnoticed because UIBezierPath silently tolerates invalid parameters. On macOS, NSBezierPath raises an exception.

How to reproduce

Any SVG containing arc commands (A/a) where:

  • The radii are very small relative to the distance between endpoints
  • Start and end points coincide or nearly coincide
  • The denominator rx²·y1'² + ry²·x1'² approaches zero

When rendered on macOS via SVGView, the app crashes.

Root cause

In SVGPathReader.swift, the A() function computes:

let denominator = rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_
// If denominator ≈ 0, the division below produces NaN/inf:
let underroot = (rx * rx * ry * ry - rx * rx * y1_ * y1_ - ry * ry * x1_ * x1_) / denominator

This cascades through cx_, cy_, cx, cy, angles t1 and delta, eventually reaching NSBezierPath.appendArc(...) with non-finite angles.

Additionally, in MBezierPath+Extension_macOS.swift, the addArc(withCenter:...) method converts radians to degrees without validating that the converted values are finite.

Proposed fix

Add guard clauses that validate intermediate values are finite. When an arc is degenerate, skip it by moving the current point to the endpoint without drawing (moveTo), rather than drawing a straight line which would introduce visual artifacts. This also keeps the path's current point in the correct position for subsequent commands.

SVGPathReader.swift — function A()

let denominator = rx * rx * y1_ * y1_ + ry * ry * x1_ * x1_
guard denominator.isFinite, denominator > .leastNonzeroMagnitude else {
    bezierPath.move(to: CGPoint(x: x, y: y))
    setPoint(CGPoint(x: x, y: y))
    return
}

// ... after computing cx, cy:
guard areFinite(rx, ry, cx_, cy_, cx, cy) else {
    bezierPath.move(to: CGPoint(x: x, y: y))
    setPoint(CGPoint(x: x, y: y))
    return
}

// ... when E() fails:
if E(...) {
    setPoint(CGPoint(x: CGFloat(x), y: CGFloat(y)))
} else {
    bezierPath.move(to: CGPoint(x: x, y: y))
    setPoint(CGPoint(x: x, y: y))
}

SVGPathReader.swift — function E()

Change return type to Bool and add validation:

@discardableResult
func E(...) -> Bool {
    guard areFinite(x, y, w, h, startAngle, arcAngle, rotation), w > 0, h > 0 else { return false }
    let end = extent + CGFloat(arcAngle)
    guard end.isFinite else { return false }
    guard areFinite(cx, cy) else { return false }
    // ...
    return true
}

MBezierPath+Extension_macOS.swift — function addArc()

func addArc(withCenter:...) {
    guard withCenter.x.isFinite, withCenter.y.isFinite,
          radius.isFinite, radius > 0,
          startAngle.isFinite, endAngle.isFinite else { return }
    let startAngleRadian = startAngle * (180.0 / .pi)
    let endAngleRadian = endAngle * (180.0 / .pi)
    guard startAngleRadian.isFinite, endAngleRadian.isFinite else { return }
    // ...
}

Helper

func areFinite(_ values: CGFloat...) -> Bool {
    values.allSatisfy { $0.isFinite }
}

Environment

  • macOS 15+ / Xcode 26
  • SVGView 1.0.6
  • Crash does not occur on iOS (UIBezierPath tolerates invalid parameters)

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