[Bug 60115] New: d2d_cdt_insert_segment() recurses without a global termination guard, stack overflow on degenerate geometry
http://bugs.winehq.org/show_bug.cgi?id=60115 Bug ID: 60115 Summary: d2d_cdt_insert_segment() recurses without a global termination guard, stack overflow on degenerate geometry Product: Wine Version: 11.13 Hardware: x86-64 OS: Linux Status: UNCONFIRMED Severity: normal Priority: P2 Component: d2d Assignee: wine-bugs@list.winehq.org Reporter: Seth2313@gmail.com Target Milestone: --- Distribution: --- IN PLAIN WORDS An application that draws complex vector graphics through Direct2D can kill Wine's main thread. A helper function in Wine's Direct2D triangulation code calls itself over and over without ever stopping when the input geometry is degenerate, until the 1 MB thread stack is used up and the process dies with a stack overflow. There is a safety check in the function, but it only guards a single call level, so it never fires. SUMMARY d2d_cdt_insert_segment() in dlls/d2d1/geometry.c:2287 recurses without a global termination guard. For degenerate geometry (collinear or duplicate vertices) the recursion oscillates between two origins and never terminates, exhausting the thread stack: EXCEPTION_STACK_OVERFLOW (0xC00000FD). The application's main thread dies while its audio threads keep running, so the process turns into a half-alive zombie rather than exiting. ENVIRONMENT Wine: wine-staging 11.9 when analysed; the affected code is byte-identical in 11.13 Application: FL Studio (64-bit) with two or more Serum 2 instances (JUCE/IPlug GUI drawing via Direct2D) d2d1: Wine builtin THE DEFECT static BOOL d2d_cdt_insert_segment(struct d2d_cdt *cdt, struct d2d_geometry *geometry, const struct d2d_cdt_edge_ref *origin, struct d2d_cdt_edge_ref *edge, size_t end_vertex) { for (current = *origin;; current = next) { ... if (<collinear case>) { d2d_cdt_edge_sym(&new_origin, ¤t); return d2d_cdt_insert_segment(cdt, geometry, &new_origin, edge, end_vertex); /* :2312 */ } if (d2d_cdt_rightof(...) && d2d_cdt_leftof(...)) { ... new_origin = *edge; return d2d_cdt_insert_segment(cdt, geometry, &new_origin, edge, end_vertex); /* :2335 */ } if (next.idx == origin->idx) { ERR("Triangle not found.\n"); return FALSE; } } } The only termination guard is "next.idx == origin->idx", which detects a full revolution around THE ORIGIN OF THE CURRENT INVOCATION. Both recursive calls at :2312 and :2335 enter with a NEW origin, so each level starts its guard from scratch. When the traversal cycles between two origins (A -> B -> A -> ...), no level ever completes a revolution, the guard never triggers, and the recursion is unbounded. EVIDENCE 1. Stack overflow (original code). With WINEDEBUG=+seh: code=c00000fd ... stack 0x108c0 (0x10000-0x11000-0x110000) i.e. the 1 MB thread stack is fully consumed. The main thread dies; the process stays alive because its audio threads are unaffected, which presents to the user as a total UI freeze. 2. The non-termination is real, not merely deep recursion. Both recursive calls are tail calls, so they can be rewritten as a loop ("origin = &new_origin; goto restart;") without any change in semantics. With that rewrite the stack overflow disappears and is replaced by an INFINITE LOOP: the thread spins at 100 % CPU with the instruction pointer stable inside d2d_cdt_insert_segment (RVA 0x2e9a3), confirmed with gdb across many samples. That rules out "recursion is merely too deep" and establishes non-termination. WORKAROUND IN USE HERE Tail-call elimination plus a generous iteration budget, bailing out the same way the existing "Triangle not found" path does: --- a/dlls/d2d1/geometry.c +++ b/dlls/d2d1/geometry.c @@ struct d2d_cdt_edge_ref base_edge, current, new_origin, next, target; size_t current_destination, current_origin; + unsigned int iterations = 0; +restart: for (current = *origin;; current = next) { + /* Guard against non-terminating cycles caused by degenerate geometry + * (e.g. collinear/duplicate vertices). The original code recursed here + * and died with a stack overflow; bail out and skip the segment instead. + * A correct segment insertion is O(edge_count), so a generous multiple + * of it is a safe upper bound that legitimate geometry never reaches. */ + if (++iterations > 16 * cdt->edge_count + 0x10000) + { + WARN("Iteration limit exceeded, skipping segment.\n"); + return TRUE; + } with both "return d2d_cdt_insert_segment(...)" calls replaced by "origin = &new_origin; goto restart;". This has been in daily production use since 2026-05 with no visible rendering artefacts. Note this returns TRUE (skip the segment and carry on) rather than FALSE. FALSE propagates up through d2d_cdt_insert_segments() and fails the whole tessellation, which in practice loses far more than the one bad segment. Which of the two is the right upstream behaviour is a call for the d2d1 maintainers - the important part is that the function must terminate. NO MINIMAL REPRODUCER The degenerate geometry is produced at runtime by a closed-source plugin GUI, so I cannot supply a small test case. A targeted test would presumably construct a path geometry containing duplicate and collinear vertices and tessellate it. I am happy to test any candidate patch against the real workload that triggers this reliably. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60115 synatedic@gmail.com changed: What |Removed |Added ---------------------------------------------------------------------------- CC| |synatedic@gmail.com --- Comment #1 from synatedic@gmail.com --- Created attachment 82419 --> http://bugs.winehq.org/attachment.cgi?id=82419 Wine console log: FL Studio 2025 + Serum 2 (VST3) crash on animated LFO rendering Confirming this on Wine 11.15 (kron4ek build) running Serum 2 (VST3) inside FL Studio 2025 under Bottles/Flatpak on Arch (CachyOS), 64-bit prefix, Windows 11 mode, DXVK and VKD3D enabled. Steps to reproduce: Load FL Studio, add Serum 2 as a VST3 instrument. Assign an LFO to an EQ parameter inside Serum. Play a note so the LFO animation starts rendering. Result: immediate crash. Static GUI rendering works fine — the crash only occurs when the modulation animation redraws continuously. Log output: hundreds of repeated err:d2d:d2d_cdt_insert_segment Triangle not found. lines, followed by: err:virtual:virtual_setup_exception stack overflow 1856 bytes addr 0x6ffffff75c2a stack 0x108c0 (0x10000-0x11000-0x110000) err:seh:call_seh_handlers invalid frame 00007FFFFE21E210 (0000000001D32000-0000000001E30000) err:seh:NtRaiseException Exception frame is not in stack limits => unable to dispatch exception. Switching the wavetable display from 3D to 2D does not help. Disabling DXVK/VKD3D prevents Serum 2 from loading at all. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60115 --- Comment #2 from Seth2313@gmail.com <Seth2313@gmail.com> --- hanks for confirming, and especially for the steps - "LFO assigned to an EQ parameter in Serum 2, then play a note" is the most reliable trigger anyone has reported for this bug. Your log matches mine exactly: hundreds of "Triangle not found" lines followed by the stack overflow at stack 0x108c0 (0x10000-0x11000-0x110000). The animation matters because every redraw builds new path geometry, so every frame is another chance to hit a degenerate one. UPDATE SINCE THE ORIGINAL REPORT 1. Still present upstream. dlls/d2d1/geometry.c is byte-identical in 11.15, 11.19 and 11.20, so your build and current Wine are affected alike. 2. The workaround in the description is not enough, and it has a bug of its own. Fuzzing the triangulation with random degenerate paths found four more places that don't terminate or crash: - d2d_cdt_insert_segment: the skip paths of my first workaround returned TRUE without setting *edge, so the caller carried on with a stale or freed edge (page fault). Fixed by returning a valid edge at the target vertex, or failing cleanly. Please don't use the snippet from the description any more. - d2d_cdt_cut_edges: unbounded non-tail recursion on cyclic topology (stack overflow). Now iterative - collect the crossed edges, delete them in reverse order (same order as the recursion), bounded by edge_count. - d2d_cdt_fixup: the face walk never gets back to its origin (infinite loop). Now bounded, returns FALSE like the existin path. - d2d_geometry_resolve_beziers: overlapping control forever and memory grows without bound. Now a budg 16 * bezier_count + 1024 splits per geometry; any gives a slightly imprecise fill instead of a hang. Every bail-out ends in an existing error path (one segment skipped, or that single geometry's fill fails with E_FAIL) - nev 3. Results: stock 11.19 overflows the stack or hangs in 10,000 fuzz seeds; with the patch, 240,000 seeds run conformance tests (17,475 checks) give identical res without the patch. REPRODUCER WITHOUT FL STUDIO The original report had no minimal test case. There is one now: tools/d2d1_fuzz.cpp in https://github.com/Sethtekk/fl-studio-wine-linux It is a ~30-line MinGW program that builds random fille (many duplicate and collinear points, 1-4 figures, line fill modes) and closes them. It prints "seed N" before last line before a crash or hang identifies the input. x86_64-w64-mingw32-g++ -O2 d2d1_fuzz.cpp -ld2d1 -stat wine d2d1_fuzz.exe 1 10000 THE FIX Patch against vanilla 11.19 (applies unchanged to 11.15 https://github.com/Sethtekk/fl-studio-wine-linux/bloby-fix.patch Technical notes per defect, measurements and test-suite https://github.com/Sethtekk/fl-studio-wine-linux/bloby-fix.md The patch also contains an optional, separate hunk that d2d_geometry_intersect_self() for D2D1_FIGURE_BEGIN_HOL strokePath() creates one on every call and only draws i micro-benchmark that cuts Close() from 3.32 ms to <0.01 but inside FL Studio I could not measure a difference, so it is not part of the bug fix. I'm happy to split this into separate patches and turn seeds into conformance tests for dlls/d2d1/tests if tha upstream. USING THE FIX TODAY (ANY WINE PREFIX) Until this is fixed upstream, the patched DLL can be dr existing 64-bit prefix. d2d1 is a pure PE DLL, so it do exact Wine build: the DLL built from 11.19 runs here on and the earlier one built from 11.9 ran unchanged up to version of the fix has been in daily use with FL Studio May, the current one since mid-October. Easiest way, with the script from the repository (backs DLL, installs the patched one and sets the override): git clone https://github.com/Sethtekk/fl-studio-wine- cd fl-studio-wine-linux WINEPREFIX=/path/to/your/prefix bash scripts/04-apply Or by hand - prebuilt x86_64 DLL from 11.19 with the pa fd197f30b36451714e5fc5a7afc148c9: https://github.com/Sethtekk/fl-studio-wine-linux/raw/ed cp $WINEPREFIX/drive_c/windows/system32/d2d1.dll d2d1 cp d2d1.dll.patched $WINEPREFIX/drive_c/windows/syste WINEPREFIX=... wine reg add 'HKCU\Software\Wine\DllOvuiltin /f The "Wine builtin DLL" marker at offset 0x40 is already If you build the DLL yourself, zero it, otherwise Wine copy: dd if=/dev/zero of=d2d1.dll bs=1 seek=64 count=16 conv=notrunc To confirm the guard is working, run once with WINEDEBU "Iteration limit exceeded, skipping segment" lines inst it caught the degenerate geometry. @synatedic - in Bottles it is the same: copy the DLL into the bottle's drive_c/windows/system32 and add d2d1 = native,builtin DLL overrides. It should work with your kron4ek 11.15 r haven't tested that exact runner, so I'd be glad to hea test survives with it. Two related notes: - Wine's d2d logs "fixme:d2d:d2d_device_context_DrawGeo style" for every line Serum 2 draws (I measured ~11,000 lines/s with the plugin window open). Not a crash, but the logging cos WINEDEBUG=fixme-all is worth setting. - I haven't tested Serum 2 with DXVK disabled, so I can part. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60115 --- Comment #3 from synatedic@gmail.com --- Confirmed working. I built d2d1.dll from your patch against vanilla wine-11.19 (applied cleanly, no fuzz) and dropped it into my Bottles prefix running kron4ek-wine-11.15-amd64 with d2d1 = native,builtin. You asked whether it survives on that runner — it does. Setup: CachyOS (Arch), AMD RX 9070 XT, Mesa/RADV, DXVK and VKD3D enabled, 64-bit prefix, Windows 11 mode, FL Studio 2025, Serum 2 (VST3). Before the patch: Serum 2: crash when assigning an LFO to an EQ parameter and playing a note, and when dragging the wavetable position on an oscillator. Static GUI rendering was fine. Serum 1: crashed immediately on opening the plugin window, so the whole Serum line was unusable here. Bridging the plugin in FL did not contain it — FL went down with it either way. After the patch: both of the Serum 2 triggers above are gone, and Serum 1 opens normally. No visual artifacts in the plugin UI that I can see. Thanks for the patch and the detailed writeup — the step-by-step in the repo made it straightforward to build this myself rather than use the prebuilt DLL. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
http://bugs.winehq.org/show_bug.cgi?id=60115 --- Comment #4 from Seth2313@gmail.com <Seth2313@gmail.com> --- Thank you - that's exactly the independent confirmation this bug needed. Summary for whoever picks this up: - Reproduced on two unrelated setups: NVIDIA (proprietary driver, wine-staging 11.19/11.20) and AMD RX 9070 XT (Mesa/RADV, kron4ek 11.15). So it is not driver- or build-specific. - Triggers in Serum 2: an LFO modulating an EQ parameter while a note plays, and dragging the wavetable position. On the second setup Serum 1 also crashed as soon as its window opened. - Bridging the plugin in FL doesn't help (same result here). - With the patch, built from source on both setups, all of these are gone, without visible rendering artefacts. Unless someone prefers otherwise, my plan is to split the patch into one commit per defect (insert_segment, cut_edges, fixup, resolve_beziers, plus the optional hollow-figure change kept separate), add regression tests derived from the fuzzer seeds to dlls/d2d1/tests, and open a merge request on gitlab.winehq.org referencing this bug. -- Do not reply to this email, post in Bugzilla using the above URL to reply. You are receiving this mail because: You are watching all bug changes.
participants (1)
-
WineHQ Bugzilla