Fix GenTerra nested buffer restore not checking frame ownership - #288
Conversation
Zaldaryon
left a comment
There was a problem hiding this comment.
This closes the case it targets. taperMap, columnResults, layerFullySolid, and layerFullyEmpty are instance fields, and the old restore in the finally block wrote previousFrame.StratumBuffers into this's own fields whenever hadPrevious was true, regardless of who owned that frame. For a genuinely foreign nested call (B nested inside A's generate()), that write clobbered B's own fields with A's live buffer while A's own generate() was still using it further up the stack, a pure corruption with no upside. The added previousFrame.StratumOwner == this check removes exactly that write and leaves the same-instance case, the only one this restore was ever meant to serve, identical to before. I rebuilt the patch set against pristine baseline (0 errors) and ran a smoke test with this file embedded; the server reaches WorldReady with no crash.
[P2] One level of ownership check is not enough for three-deep nesting
Consider X, then Y nested inside X's generate(), then X nested again inside Y's generate(). Stack top to bottom at that point: X2, Y, X1. X2's own publish overwrites X's instance fields with X2's buffer while X1's generate() is still paused on those same fields further down the stack, exactly the corruption this frame stack exists to prevent. When X2 unwinds, its previousFrame is Y's frame, not X1's, so the new ownership check is false and the restore is skipped entirely. X's fields are left pointing at X2's buffer, which was already returned to the pool one line earlier by ReturnColumnBuffers(buf). When X1's generate() resumes, it reads that pooled, possibly-already-rented buffer instead of its own.
This needs the same reciprocal same-thread nesting the fixed case does, so it sits at the same reachability tier issue #286 already documents as not currently reachable through Stratum's own handler. Given that, the same reasoning that justified this PR covers it too: walk the frame stack for the nearest frame actually owned by this instead of comparing only against the immediate previous frame, and skip the restore only when no such frame exists anywhere on the stack.
One non-blocking note: borderIndicesByCardinal is written by both the publish and the restore this PR touches, but nothing in this file reads it anymore. Its one internal reader lived in the border-smoothing loop, which was already rewritten to use the per-call currentBorderIndicesByCardinal local instead. Restoring it is dead weight today, worth a comment or a removal separately.
|
Zaldaryon
left a comment
There was a problem hiding this comment.
Confirmed the fix. hadPrevious/previousFrame are gone, replaced with a walk of the remaining stack for the nearest frame still owned by this, exactly the fix requested for the three-level nesting case (X calls Y calls X again). Traced it by hand against the actual Push/Pop/finally code: when the innermost X unwinds, the walk correctly skips Y's frame and finds the outer X's, restoring its buffer before that outer call resumes; when Y itself unwinds afterward, the same walk correctly finds no frame it owns and skips the restore, so Y never touches X's fields. taperMap, columnResults, layerFullySolid, layerFullyEmpty, and borderIndicesByCardinal are all plain instance fields, so the restore in one instance's finally can't reach another instance's state, and ReturnColumnBuffers only ever returns the current call's own rented buffer, never one still referenced by a frame further down the stack, so there's no reuse-after-free risk in what the walk reads.
Reconstructed the file against pristine baseline, built (dotnet build VintageStory.slnx -c Release, 0 errors), and ran the smoke test (scripts/smoke-test.sh, reaches WorldReady). No blocking issues remain.
Summary
Added a check for
previousFrame.StratumOwner == thisto the buffer recovery condition in the finally block of theOnChunkColumnGenmethod. This is necessary to prevent a nested call from anotherGenTerra instance on the same thread from writing other instances' buffers and boundary indices to its own fields, which are then read bygenerate(), thereby preventing data substitution between instances.Type
Checklist
.\scripts\extract-patches.ps1ran clean.dotnet build VintageStory.slnx -c Releaseis green.// Stratummarker.Performance numbers
No changes
Related issues
Fixes #286