From a1dc453fa28e1dc9cc03ce40ea48137c8cb1daa2 Mon Sep 17 00:00:00 2001 From: tastybento Date: Tue, 19 May 2026 07:40:29 -0700 Subject: [PATCH] Harden Island.setRange against distance-mismatch corruption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Island.setRange previously accepted any int value, which let third-party addons silently overwrite an island's range with a value inconsistent with the gamemode's configured distance-between-islands. The next time IslandsManager.load() ran it would refuse to load the island and BentoBox would panic-disable with "Island distance mismatch". This was triggered in the wild by an inverted boolean in StrangerRealms' TeamListener (fixed separately in StrangerRealms#11) which fired resize() on islands belonging to other game modes during / team kick / team leave commands, calling setRange with StrangerRealms' own distance (typically 64) on AOneBlock / BSkyBlock / Boxed islands. setRange now refuses to change range to a value that disagrees with IWM.getIslandDistance(world) when the gamemode enforces equal ranges (the default — isEnforceEqualRanges() returns true). Game modes that legitimately resize claims (StrangerRealms etc.) opt out by returning false from isEnforceEqualRanges() and are unaffected. When the world is not registered (configured distance == 0) we have no authoritative value to validate against and pass through, preserving existing unit-test and deserialization paths. Refusal logs a WARN with the calling stack frame so addon authors can locate and fix the offending code path. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../bentobox/database/objects/Island.java | 32 ++++++++- .../bentobox/database/objects/IslandTest.java | 65 +++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/main/java/world/bentobox/bentobox/database/objects/Island.java b/src/main/java/world/bentobox/bentobox/database/objects/Island.java index 6b747ab66..743c9e1ee 100644 --- a/src/main/java/world/bentobox/bentobox/database/objects/Island.java +++ b/src/main/java/world/bentobox/bentobox/database/objects/Island.java @@ -32,6 +32,7 @@ import com.google.gson.annotations.Expose; import world.bentobox.bentobox.BentoBox; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.commands.CompositeCommand; import world.bentobox.bentobox.api.configuration.WorldSettings; import world.bentobox.bentobox.api.flags.Flag; @@ -1186,10 +1187,35 @@ public void setPurgeProtected(boolean purgeProtected) { * @see #setProtectionRange(int) */ public void setRange(int range) { - if (this.range != range) { - this.range = range; - setChanged(); + if (this.range == range) { + return; + } + // Refuse to mutate range to a value that disagrees with the game mode's + // configured distance-between-islands. Storing a mismatched range corrupts + // the database — on the next restart IslandsManager.load() will reject the + // island with "Island distance mismatch" and panic-disable BentoBox. + // + // Game modes that opt out of the equality check (isEnforceEqualRanges == false, + // e.g. claim-based addons that resize on team changes) are allowed any value. + // A configured distance of 0 means the world isn't currently registered with + // IWM (e.g. unit tests, deserialization), so we also pass through in that case. + BentoBox plugin = BentoBox.getInstance(); + if (plugin != null && world != null && plugin.getIWM() != null) { + int configured = plugin.getIWM().getIslandDistance(world); + boolean enforce = plugin.getIWM().getAddon(world) + .map(GameModeAddon::isEnforceEqualRanges).orElse(true); + if (enforce && configured > 0 && configured != range) { + StackTraceElement[] trace = new Throwable().getStackTrace(); + String caller = trace.length > 1 ? trace[1].toString() : "unknown"; + plugin.logWarning("Refusing Island.setRange(" + range + ") on island " + + uniqueId + " in world '" + world.getName() + + "': value does not match configured distance-between-islands (" + + configured + "). Caller: " + caller); + return; + } } + this.range = range; + setChanged(); } /** diff --git a/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java b/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java index 1482c53c0..196ae410a 100644 --- a/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java +++ b/src/test/java/world/bentobox/bentobox/database/objects/IslandTest.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.Test; import world.bentobox.bentobox.CommonTestSetup; +import world.bentobox.bentobox.api.addons.GameModeAddon; import world.bentobox.bentobox.api.flags.Flag; import world.bentobox.bentobox.api.logs.LogEntry; import world.bentobox.bentobox.api.metadata.MetaDataValue; @@ -45,6 +46,7 @@ class IslandTest extends CommonTestSetup { private Island island; // real Island under test (shadows the mock in CommonTestSetup) private Location center; private UUID ownerUUID; + private GameModeAddon gameModeAddon; @Override @BeforeEach @@ -71,6 +73,9 @@ public void setUp() throws Exception { // Create the real Island under test island = new Island(center, ownerUUID, 50); + + // GameModeAddon mock for setRange validation tests (used per-test as needed) + gameModeAddon = mock(GameModeAddon.class); } @Override @@ -1189,4 +1194,64 @@ void testGetCenterReturnsClone() { assertNotSame(c1, c2); assertEquals(c1.getBlockX(), c2.getBlockX()); } + + // ======================== setRange guarding ======================== + // + // Background: Island.setRange used to accept any value, which let a third-party + // addon (StrangerRealms TeamListener) overwrite the range of islands belonging + // to other game modes whenever a / team kick or leave fired. On the + // next server restart, IslandsManager.load() refused to load those islands + // because their stored range no longer matched the configured + // distance-between-islands, and BentoBox panic-disabled with + // "Island distance mismatch". setRange now refuses to mutate range to a value + // inconsistent with the gamemode's configured distance unless the addon opts + // out via isEnforceEqualRanges() == false. + + @Test + void testSetRangeRefusedWhenMismatchesConfiguredDistance() { + // iwm.getIslandDistance(world) -> 100 (set in @BeforeEach) + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(true); + + int originalRange = island.getRange(); + island.setRange(64); // would corrupt the database — must be rejected + + assertEquals(originalRange, island.getRange(), + "setRange must refuse a value that disagrees with the configured distance"); + } + + @Test + void testSetRangeAcceptedWhenAddonOptsOutOfEqualRanges() { + // Game mode that legitimately resizes claims (e.g. StrangerRealms). + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(false); + + island.setRange(64); + + assertEquals(64, island.getRange()); + } + + @Test + void testSetRangeAcceptedWhenValueMatchesConfiguredDistance() { + // Configured distance is 100 (set in @BeforeEach); island starts at 100. + // Setting to the same value is a no-op but still legal. + when(iwm.getAddon(any())).thenReturn(Optional.of(gameModeAddon)); + when(gameModeAddon.isEnforceEqualRanges()).thenReturn(true); + + island.setRange(100); + + assertEquals(100, island.getRange()); + } + + @Test + void testSetRangeAcceptedWhenWorldNotRegistered() { + // iwm.getIslandDistance returns 0 when the world isn't keyed in gameModes + // (e.g. unit tests, deserialization). In that case we have no authoritative + // value to validate against, so the call should pass through. + when(iwm.getIslandDistance(any())).thenReturn(0); + + island.setRange(64); + + assertEquals(64, island.getRange()); + } }