Skip to content

Recursive update crash in RapierVoxelColliderBakery when chunk post-processing triggers reentrant block change #1162

@zanderson88

Description

@zanderson88

Description

IllegalStateException: Recursive update thrown in ConcurrentHashMap.computeIfAbsent inside RapierVoxelColliderBakery.getPhysicsDataForBlock() during chunk post-processing generation. Sables physics handler re-enters itself: a block change triggers physics computation, which calls getCollisionShape(), which loads a chunk, which triggers postProcessGeneration, which calls setBlock(), which fires back into Sables block change handler.

Stack Trace (top)

java.lang.IllegalStateException: Recursive update
    at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1742)
    at net.minecraft.Util$8.apply(Util.java:795)
    at dev.ryanhcode.sable.physics.impl.rapier.collider.RapierVoxelColliderBakery.getPhysicsDataForBlock(RapierVoxelColliderBakery.java:98)
    at dev.ryanhcode.sable.physics.impl.rapier.RapierPhysicsPipeline.handleBlockChange(RapierPhysicsPipeline.java:513)
    at dev.ryanhcode.sable.sublevel.system.SubLevelPhysicsSystem.handleBlockChange(SubLevelPhysicsSystem.java:451)
    at dev.ryanhcode.sable.SableCommonEvents.handleBlockChange(SableCommonEvents.java:91)
    at net.minecraft.world.level.chunk.LevelChunk.wrapOperation$geo000$sable$setBlockState(LevelChunk.java:4802)
    at net.minecraft.world.level.chunk.LevelChunk.setBlockState(LevelChunk.java:249)
    at net.minecraft.world.level.Level.setBlock(Level.java:236)
    at net.minecraft.world.level.Level.setBlock(Level.java:212)
    at net.minecraft.world.level.chunk.LevelChunk.postProcessGeneration(LevelChunk.java:547)
    at net.minecraft.server.level.ChunkMap.lambda$prepareTickingChunk$27(ChunkMap.java:678)
    at net.minecraft.server.level.ChunkResult$Success.ifSuccess(ChunkResult.java:60)
    at net.minecraft.server.level.ChunkMap.lambda$prepareTickingChunk$28(ChunkMap.java:677)

Recursive chain

  1. ChunkMap.prepareTickingChunk -> LevelChunk.postProcessGeneration -> Level.setBlock()
  2. Sable mixin intercepts via wrapOperation$geo000$sable$setBlockState -> SableCommonEvents.handleBlockChange()
  3. RapierPhysicsPipeline.handleBlockChange() -> RapierVoxelColliderBakery.getPhysicsDataForBlock() -> ConcurrentHashMap.computeIfAbsent()
  4. Inside the lambda: buildPhysicsDataForBlock() -> BlockStateBase.getCollisionShape()
  5. ShetiPhianCore handler -> LevelAccelerator.getBlockState() -> Level.getChunk() -> loads chunk
  6. Chunk load fires prepareTickingChunk again -> back to step 1
  7. ConcurrentHashMap.computeIfAbsent in step 3 detects recursion -> throws IllegalStateException

Environment

  • Sable version: 1.2.2 (neoforge-1.21.1)
  • Minecraft: 1.21.1
  • Modloader: NeoForge 21.1.230
  • Dimension: twilightforest:twilight_forest
  • Crash UUID: 79f9e0a9-bdf4-47c7-a1e9-68cd3b0cc7b1

Impact

  • Server crash with world ticking exception (no graceful recovery)
  • Chunks [947, 384], [947, 385], [947, 386] failed to save during shutdown
  • Server process hung in shutdown state indefinitely

Reproduction

A player logged into the Twilight Forest at coordinates (14886, 23, 5922). The crash occurred approximately 8 seconds after login during chunk post-processing, likely as new chunks were generated or loaded around the player. Corrupted Sable UDP snapshot packets were also seen from the players client around the same time.

Suggested fix

RapierVoxelColliderBakery.getPhysicsDataForBlock() at line 98 needs a reentrancy guard - either:

  • Cache the currently-computing block and return a default/fallback physics shape instead of recursing
  • Defer the physics computation outside the computeIfAbsent lock (e.g., compute ahead of time, or use a separate lock that allows reentrancy)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions