[
https://issues.apache.org/jira/browse/NIFI-16025?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18093277#comment-18093277
]
ASF subversion and git services commented on NIFI-16025:
--------------------------------------------------------
Commit 48b2987d7109c8736833fac5771abcd1e1f815be in nifi's branch
refs/heads/main from Rob Fellows
[ https://gitbox.apache.org/repos/asf?p=nifi.git;h=48b2987d710 ]
NIFI-16025 Canvas SVG transform overflow guards (#11352)
Prevent browser SVG errors and frozen navigation caused by extreme finite
floating-point coordinate values (e.g. 7.49e+307) stored in NiFi flow
definitions. The root cause is that Number.isFinite() accepts arbitrarily
large finite numbers that overflow float32 rendering and D3 arithmetic;
this change adds a layered defence of magnitude- and range-bounded guards
throughout the frontend canvas pipeline.
## Shared helpers (libs/shared)
Add canvas-bounds.util.ts with:
- MAX_ABS_COORD (1e9) and MAX_ABS_TRANSLATE (1e12) magnitude bounds
- MIN_SCALE (0.2) / MAX_SCALE (8) scale range, centralising the values
previously hardcoded in canvas-view.service.ts
- isFiniteInBound(value, bound) — type guard combining Number.isFinite
and Math.abs check
- isScaleInBound(value) — rejects near-zero and out-of-range scales
- clampScale(scale, fallback?) — NaN-safe clamp
- sanitizePosition(position, opts) — clamps bad coordinates to (0, 0)
with a warn-once, per-component-id console warning
Re-export from services/index.ts. Full spec coverage (30 tests) including
the ~7.49e+307 fingerprint and the warn-once dedupe.
## Data-ingestion sanitization
flow.effects.ts (loadProcessGroup$): walk all positioned entity collections
(processors, process groups, remote process groups, input/output ports,
labels, funnels) and connection bends through sanitizePosition before the
NgRx action is dispatched. A session-scoped warnedPositionIds Set prevents
console spam during polling refreshes.
connector-canvas.effects.ts (loadConnectorFlow$): same sanitizer applied to
all collections. Even the read-only connector view uses the reusable canvas,
so its inputs must be clean.
## Flow-designer transform guards
canvas-view.service.ts:
- Zoom handler: replace !isNaN checks with isFiniteInBound + isScaleInBound
- transform(): early-return guard before behavior.transform() call
- getSelectionBoundingClientRect(): return null on empty selection; swap
Number.MAX_VALUE / MIN_VALUE sentinel seeds for POSITIVE/NEGATIVE_INFINITY
so negative-space selections aggregate correctly
- getCanvasPosition(): reject results outside MAX_ABS_COORD so bad
coordinates cannot be written back to the server
- fit(): guard zero-dimension container; use clampScale helper
birdseye-view.service.ts (refresh()): bail early before divide-by-scale
arithmetic when this.x / this.y / this.k are out of bounds.
transform.effects.ts (restoreViewport$): upgrade isFinite checks to
isFiniteInBound + isScaleInBound; call storage.removeItem(name) when the
persisted entry is invalid, breaking the page-refresh replay loop.
## Reusable canvas / birdseye guards
ui/common/canvas/canvas.component.ts:
- readViewportTransform: upgrade isFinite to magnitude-bounded checks
- Zoom handler: all-or-nothing guard before SVG attribute write and emit
- applyTransform: magnitude+scale guard at top of method
- restoreViewportFromStorage: evict bad localStorage entries on load
- fitContent: clampScale instead of raw Math.min/max
- getCanvasPosition: reject out-of-bounds output
ui/common/birdseye/birdseye.component.ts: add isTransformSafe predicate;
consult it in updateBrush and renderComponents before any divide-by-scale
render path.
## Tests
- canvas-bounds.util.spec.ts: 30 new tests
- canvas-view.service.spec.ts: 12 new tests (mountCanvasDom fixture)
- transform.effects.spec.ts: 5 new tests (new file)
- birdseye-view.service.spec.ts: 3 new tests
- birdseye.component.spec.ts: 4 new tests; update scale-0.1 test to use
valid MAX_SCALE to keep the minimum-brush-size assertion accurate
- canvas.component.spec.ts: 10 new tests
- flow.effects.spec.ts: 4 new tests
- connector-canvas.effects.spec.ts: 3 new tests; update bare label
fixture to carry a position so it survives sanitization unchanged
All 2700 tests pass. nx lint nifi and npm run prettier clean.
> UI - Canvas SVG transform produces scale(Infinity) and components overlap at
> origin when component positions or persisted viewport exceed safe rendering
> bounds
> ---------------------------------------------------------------------------------------------------------------------------------------------------------------
>
> Key: NIFI-16025
> URL: https://issues.apache.org/jira/browse/NIFI-16025
> Project: Apache NiFi
> Issue Type: Task
> Components: Core UI
> Reporter: Rob Fellows
> Assignee: Rob Fellows
> Priority: Major
> Fix For: 2.11.0
>
> Time Spent: 10m
> Remaining Estimate: 0h
>
> h2. Symptom
> Under certain conditions, the NiFi flow canvas becomes visually unusable in
> the
> browser: every component is rendered stacked at the canvas origin, the
> birdseye
> (minimap) is blank or shows {{scale(Infinity)}} errors in the console, drag /
> zoom / pan stops working, and dragging a component to a new location persists
> it at {{(0, 0)}}. The flow itself continues to run correctly -- this is a
> UI-only failure mode -- but the canvas is unrecoverable for the rest of the
> session, and refreshing the page does not fix it (the bad state is replayed
> from {{localStorage}}).
> In the browser console, the failure surfaces as either:
> * {{Error: <g> attribute transform: Expected number,
> "translate(-8.98...e+307, ...)"}}
> * {{Error: <g> attribute transform: Expected number, "scale(Infinity)"}}
> h2. Root cause
> {{Number.isFinite(value)}} (and the weaker {{!isNaN(value)}}) is a *necessary
> but not sufficient* check for any value used in an SVG attribute or in
> multiplication-heavy canvas math. A double like {{Number.MAX_VALUE / 2}}
> (~{{8.99e307}}) is finite in JavaScript but:
> * The browser's SVG parser stores transform coordinates as {{float32}}
> internally (max {{~3.4e38}}). Anything beyond that is silently rejected --
> the attribute write fails and the {{<g>}} sticks at the previous transform
> (typically the identity), so every component visually collapses to the
> origin.
> * Downstream arithmetic in the canvas math
> ({{(rect.x - this.x) / this.k}}, bbox normalization, birdseye brush math)
> overflows to {{Infinity}} on the first multiplication, which then poisons
> d3's internal {{__zoom}} datum for the remainder of the session.
> The canvas pipeline relies on {{isFinite}} / {{!isNaN}} at every transform
> mutation site, so any catastrophic-but-finite value -- whether produced by
> canvas math itself, replayed from {{localStorage}}, or returned from the REST
> API -- flows straight through.
> A few specific failure modes contribute:
> * The zoom handler ({{canvas-view.service.ts}}) gates each axis with
> {{!isNaN(...)}}, which is {{true}} for {{Infinity}}.
> * {{CanvasView.transform()}} has no validation at all, so a corrupted caller
> pushes the bad value straight into d3's {{__zoom}} datum.
> * {{CanvasView.getSelectionBoundingClientRect()}} seeds its reduce sentinel
> with {{Number.MAX_VALUE}} / {{Number.MIN_VALUE}}; on an empty selection
> those seeds leak through unchanged and the normalized result is the
> {{~7.49e307}} fingerprint that surfaces in corrupted flows. (As a separate
> correctness bug, {{Number.MIN_VALUE}} is the smallest *positive* double
> ({{~5e-324}}), not the most-negative value, so non-empty selections in
> negative canvas space aggregate {{right}} / {{bottom}} incorrectly.)
> * {{CanvasView.getCanvasPosition()}} divides by the current scale with no
> output bound, so a corrupted upstream transform produces drop coordinates
> that the next component-create POST persists into the flow.
> * {{TransformEffects.restoreViewport$}} reads the last-known viewport from
> {{localStorage}} with an {{isFinite}}-only check; a once-corrupted entry
> re-poisons the canvas on every subsequent load and is never evicted. This
> is what makes the failure mode survive a hard refresh.
> * The reusable {{<canvas>}} component in {{ui/common/canvas/}} has the same
> {{isFinite}}-only gates, so any host that mounts it (flow-designer, the
> read-only connector-canvas troubleshooting view) is equally exposed.
> h2. Scope
> The fix touches the flow-designer canvas surface and the shared reusable
> {{<canvas>}} / {{<birdseye>}} components. Both currently-known hosts of the
> reusable canvas (flow-designer and the read-only connector-canvas
> troubleshooting view) must be hardened: the read-only host cannot produce a
> bad coordinate, but it displays them through the same components and triggers
> the same overflow.
> h2. Proposed solution (summary)
> In broad strokes:
> # Introduce shared magnitude-bounded numeric-safety helpers in
> {{@nifi/shared}} (replacing ad-hoc {{isFinite}} / {{!isNaN}} checks with
> a finite-AND-magnitude-bounded type guard, plus a scale-range check and a
> NaN-safe scale clamp). Pair the helpers with a small set of named
> constants for the coordinate / translate / scale bounds.
> # Sanitize positions at the data-ingestion boundary, before they are bound
> to a d3 datum or stored in NgRx state. Apply this in flow-designer's
> per-entity-type mappers and in the connector-canvas load effect. The
> reusable {{<canvas>}} itself remains a trusted consumer; the obligation
> to clean data lives with each host page. Emit a deduped console warning
> that names the affected component id so users can drag-and-save to
> repair the persisted value.
> # Harden every transform mutation site (zoom handler, programmatic
> {{transform}}, {{fit}} / {{actualSize}}, drop-coordinate emission,
> bounding-box aggregation, birdseye refresh) with the new
> magnitude-bounded guards. Where the existing code uses an in-band
> sentinel value (the {{MAX_VALUE}} / {{MIN_VALUE}} seeds in the reduce),
> prefer guarding at the source -- e.g. return {{null}} on an empty
> selection -- rather than patching every caller.
> # Evict corrupted {{localStorage}} viewport entries on read and fall back
> to a fit-to-content reset, so the failure mode cannot survive a refresh.
> # Mirror the same guards in the reusable {{<canvas>}} / {{<birdseye>}}
> components so every host page is protected without per-host effort.
> The detailed call sites, constants, and helper signatures are best decided
> during implementation review; the above outlines the approach rather than
> dictating the exact shape.
> h2. Reproduction (manual)
> The fastest path that does not require a corrupted flow on disk:
> # Open any flow.
> # In DevTools, Application -> Local Storage -> find a {{nifi-view-<pg-id>}}
> entry (one is written on the first pan / zoom). Edit its JSON in place,
> setting {{translateX}} to {{8.99e307}}. Reload.
> # Observe: every component renders stacked at the origin, the minimap is
> blank, pan / zoom / drag does nothing, and the console shows the
> {{translate(-X, -Y)}} / {{scale(Infinity)}} errors above. The corruption
> replays on every reload because the viewport-restore effect re-applies
> the {{localStorage}} entry unchanged.
> To reproduce the write-side leak (without preconfiguring localStorage):
> trigger {{actualSize()}} on a process group with no rendered components yet
> (initial-load race) and then drag-and-drop a new component. The
> {{getSelectionBoundingClientRect}} sentinel leaks into the zoom math, and the
> resulting POST carries the {{~7.49e307}} fingerprint.
> h2. Acceptance criteria
> # Under no input -- corrupted {{localStorage}}, corrupted server data, a
> forged zoom event, a fuzzed drop coordinate, an empty / racy selection,
> or division by a near-zero scale -- can the canvas state reach a
> configuration that produces {{translate(<finite-but-huge>, ...)}} or
> {{scale(Infinity)}} on a {{<g>}} attribute.
> # The drop-coordinate emission path returns {{null}} rather than emit a
> coordinate that the rest of the UI would later reject.
> # A corrupted {{localStorage}} viewport entry is evicted on read; the user
> does not need to manually clear browser storage to recover the canvas.
> # The connector-canvas (read-only display) is equally protected against
> bad server data, even though it cannot produce it.
> # A flow containing pre-existing bad positions renders with the affected
> components at {{(0, 0)}} (not at the corrupted coordinate, and not
> collapsing the entire canvas to the origin) and emits a deduped console
> warning identifying each affected component id, so users can drag-and-save
> to repair the persisted value.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)