[ 
https://issues.apache.org/jira/browse/NIFI-16025?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Rob Fellows reassigned NIFI-16025:
----------------------------------

    Assignee: Rob Fellows

> 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
>
> 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)

Reply via email to