diff --git a/src/main/java/autismclient/mixin/security/AutismProtectorPacketCrashGuardMixin.java b/src/main/java/autismclient/mixin/security/AutismProtectorPacketCrashGuardMixin.java new file mode 100644 index 0000000..bf7d68e --- /dev/null +++ b/src/main/java/autismclient/mixin/security/AutismProtectorPacketCrashGuardMixin.java @@ -0,0 +1,140 @@ +package autismclient.mixin.security; + +import autismclient.security.AutismProtector; +import autismclient.security.AutismProtectorNumericSanity; + +import com.mojang.logging.LogUtils; + +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; +import net.minecraft.network.protocol.game.ClientboundExplodePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.world.entity.PositionMoveRotation; +import net.minecraft.world.phys.Vec3; + +import org.slf4j.Logger; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Optional; + +/** + * Packet crash guard — a defensive protector feature that drops an incoming play packet whose raw position or + * velocity is malformed (NaN / Infinity) or absurdly large (beyond {@link AutismProtectorNumericSanity#SANE_LIMIT}, + * far past the world border) before the game applies it and overflows its own section/collision math and crashes. + * + *

Some servers send such values deliberately to crash connected clients; observed crash reports carried + * per-axis values around {@code 1.8e38 / 2.8e38 / 2.1e38}. The crash reproduces even with rendering optimisation + * mods disabled, so it is a vanilla-math overflow rather than an optimisation-mod bug. Covered here: + * + *

+ * + * All share the one {@link AutismProtectorNumericSanity} check, so a new overflow vector is caught by the same + * mechanism rather than needing another one-off fix. + * + *

Purely defensive and local: it only drops a malformed packet (the client keeps its current, valid + * state) — it never edits anything the client sends and never fabricates a teleport acknowledgement. It only + * fires on values orders of magnitude past anything legitimate, so real gameplay is never touched. Gated by + * {@link AutismProtector#shouldGuardCrashPackets()} (and, like every {@code security} mixin, stood down when an + * external protector such as opsec is present — see {@code AutismMixinPlugin}). + * + *

Injections use {@code require = 0} so that if a future mapping renames one of these handlers the guard + * degrades to "that one packet is not guarded" rather than failing the whole required mixin config — a + * deliberate choice for a defensive safeguard that must never be the reason a client won't launch. + * + *

Adapted for AUTISM from BossCrashGuard (https://github.com/WaterBoss11/BossCrashGuard). + */ +@Mixin(ClientPacketListener.class) +public abstract class AutismProtectorPacketCrashGuardMixin { + + @Unique + private static final Logger AUTISM$LOGGER = LogUtils.getLogger(); + + @Unique + private static long autism$dropCount = 0L; + + @Inject(method = "handleExplosion", at = @At("HEAD"), cancellable = true, require = 0) + private void autism$guardExplosion(ClientboundExplodePacket packet, CallbackInfo ci) { + if (!AutismProtector.shouldGuardCrashPackets()) return; + + boolean bad = autism$vecInsane(packet.center()) + || AutismProtectorNumericSanity.isInsane(packet.radius(), AutismProtectorNumericSanity.SANE_LIMIT); + if (!bad) { + Optional knockback = packet.playerKnockback(); + bad = knockback.isPresent() && autism$vecInsane(knockback.get()); + } + if (bad) { + autism$drop("explosion", "center=" + packet.center() + " r=" + packet.radius() + + " knockback=" + packet.playerKnockback().orElse(null)); + ci.cancel(); + } + } + + @Inject(method = "handleMovePlayer", at = @At("HEAD"), cancellable = true, require = 0) + private void autism$guardMovePlayer(ClientboundPlayerPositionPacket packet, CallbackInfo ci) { + if (autism$rejectMove(packet.change(), "player teleport")) ci.cancel(); + } + + @Inject(method = "handleTeleportEntity", at = @At("HEAD"), cancellable = true, require = 0) + private void autism$guardTeleportEntity(ClientboundTeleportEntityPacket packet, CallbackInfo ci) { + if (autism$rejectMove(packet.change(), "entity teleport")) ci.cancel(); + } + + @Inject(method = "handleEntityPositionSync", at = @At("HEAD"), cancellable = true, require = 0) + private void autism$guardPositionSync(ClientboundEntityPositionSyncPacket packet, CallbackInfo ci) { + if (autism$rejectMove(packet.values(), "entity position sync")) ci.cancel(); + } + + @Inject(method = "handleAddEntity", at = @At("HEAD"), cancellable = true, require = 0) + private void autism$guardAddEntity(ClientboundAddEntityPacket packet, CallbackInfo ci) { + if (!AutismProtector.shouldGuardCrashPackets()) return; + if (AutismProtectorNumericSanity.isInsane(packet.getX(), packet.getY(), packet.getZ())) { + autism$drop("entity spawn", + "pos=(" + packet.getX() + ", " + packet.getY() + ", " + packet.getZ() + ")"); + ci.cancel(); + } + } + + /** + * Shared drop decision for a {@link PositionMoveRotation}-carrying packet: true (and recorded) when its + * position or delta-movement is out of range. + */ + @Unique + private static boolean autism$rejectMove(PositionMoveRotation change, String kind) { + if (!AutismProtector.shouldGuardCrashPackets() || change == null) return false; + if (autism$vecInsane(change.position()) || autism$vecInsane(change.deltaMovement())) { + autism$drop(kind, "pos=" + change.position() + " delta=" + change.deltaMovement()); + return true; + } + return false; + } + + @Unique + private static boolean autism$vecInsane(Vec3 v) { + return v != null && AutismProtectorNumericSanity.isInsane(v.x, v.y, v.z); + } + + /** Record a dropped packet: the first drop is logged loudly, the rest at debug to avoid a flood vector. */ + @Unique + private static void autism$drop(String kind, String detail) { + autism$dropCount++; + if (autism$dropCount == 1L) { + AUTISM$LOGGER.warn("[AutismProtector] crash guard dropped a malformed {} packet ({}); " + + "further drops logged at debug", kind, detail); + } else { + AUTISM$LOGGER.debug("[AutismProtector] crash guard dropped a malformed {} packet #{} ({})", + kind, autism$dropCount, detail); + } + } +} diff --git a/src/main/java/autismclient/security/AutismProtector.java b/src/main/java/autismclient/security/AutismProtector.java index 6623011..2b51e95 100644 --- a/src/main/java/autismclient/security/AutismProtector.java +++ b/src/main/java/autismclient/security/AutismProtector.java @@ -104,6 +104,10 @@ public final class AutismProtector { return state().skipChatSigning; } + public static boolean shouldGuardCrashPackets() { + return state().guardCrashPackets; + } + public static void refreshRuntimeState() { runtimeState = buildRuntimeState(); runtimeStateExpiresAt = System.nanoTime() + STATE_REFRESH_NANOS; @@ -138,7 +142,10 @@ public final class AutismProtector { !overlap && config.protectorBlockLocalUrls, !overlap && config.protectorIsolatePackCache, config.protectorStripServerPacks, - config.protectorChatSigningOff + config.protectorChatSigningOff, + // Not overlap-gated: a crash guard is cheap and worth keeping even alongside another protector, and + // AutismMixinPlugin likewise keeps non-overlap security mixins applied under exploitpreventer. + config.protectorCrashGuard ); } @@ -151,10 +158,11 @@ public final class AutismProtector { boolean blockLocalUrls, boolean isolatePackCache, boolean stripServerPacks, - boolean skipChatSigning + boolean skipChatSigning, + boolean guardCrashPackets ) { static RuntimeState inactive() { - return new RuntimeState(false, false, false, false, false, false, false, false, false); + return new RuntimeState(false, false, false, false, false, false, false, false, false, false); } } diff --git a/src/main/java/autismclient/security/AutismProtectorNumericSanity.java b/src/main/java/autismclient/security/AutismProtectorNumericSanity.java new file mode 100644 index 0000000..4bc8842 --- /dev/null +++ b/src/main/java/autismclient/security/AutismProtectorNumericSanity.java @@ -0,0 +1,61 @@ +package autismclient.security; + +/** + * Numeric sanity checks for incoming packet data — a single place that decides whether a raw {@code double} or + * {@code float} a server sent is safe to feed into the game's own position/velocity/section math, or whether it + * is malformed (NaN / infinite) or absurdly large enough to overflow that math and crash the client. + * + *

Rather than a one-off fix per exploit packet, any incoming packet handler can call these predicates on the + * fields it is about to apply and drop a packet carrying a bad value. Observed overflow-crash reports carried + * per-axis values around {@code 1.8e38 / 2.8e38 / 2.1e38} (near {@code Float.MAX_VALUE}); the ceiling here sits + * many orders of magnitude below that and far above any legitimate value: + * + *

+ * + *

Purely defensive: these never alter anything the client sends to the server and never fabricate + * acknowledgements — they only let a handler avoid processing a value that would crash the local client. No + * Minecraft types are referenced, so the whole decision surface is unit-testable without a client. + * + *

Adapted for AUTISM from BossCrashGuard (https://github.com/WaterBoss11/BossCrashGuard). + */ +public final class AutismProtectorNumericSanity { + + private AutismProtectorNumericSanity() { + } + + /** Ceiling for a position/velocity component the client applies (blocks or blocks/tick). */ + public static final double SANE_LIMIT = 1.0e9; + + /** True when {@code v} is NaN or infinite — i.e. not a usable finite number. */ + public static boolean isNonFinite(double v) { + return Double.isNaN(v) || Double.isInfinite(v); + } + + /** Float overload of {@link #isNonFinite(double)}. */ + public static boolean isNonFinite(float v) { + return Float.isNaN(v) || Float.isInfinite(v); + } + + /** True when {@code v} is non-finite, or its magnitude exceeds {@code limit} — unsafe to feed to game math. */ + public static boolean isInsane(double v, double limit) { + return isNonFinite(v) || Math.abs(v) > limit; + } + + /** Float overload of {@link #isInsane(double, double)}. */ + public static boolean isInsane(float v, double limit) { + return isNonFinite(v) || Math.abs((double) v) > limit; + } + + /** + * True when any of {@code x/y/z} is insane against {@link #SANE_LIMIT} — the check every position/velocity + * packet path uses before letting the client apply the triple. + */ + public static boolean isInsane(double x, double y, double z) { + return isInsane(x, SANE_LIMIT) || isInsane(y, SANE_LIMIT) || isInsane(z, SANE_LIMIT); + } +} diff --git a/src/main/java/autismclient/util/AutismConfig.java b/src/main/java/autismclient/util/AutismConfig.java index 3987efa..53ee229 100644 --- a/src/main/java/autismclient/util/AutismConfig.java +++ b/src/main/java/autismclient/util/AutismConfig.java @@ -63,6 +63,8 @@ public final class AutismConfig { public boolean protectorStripServerPacks = false; public boolean protectorChatSigningOff = false; + + public boolean protectorCrashGuard = true; public boolean performanceDebug = false; public boolean inventoryMove = false; public boolean xCarry = true; diff --git a/src/main/resources/autism.mixins.json b/src/main/resources/autism.mixins.json index 13f6937..03089d5 100644 --- a/src/main/resources/autism.mixins.json +++ b/src/main/resources/autism.mixins.json @@ -164,6 +164,7 @@ "security.AutismProtectorResourceLoaderImplMixin", "security.AutismProtectorPacketProcessorMixin", "security.AutismProtectorPacketDecoderMixin", + "security.AutismProtectorPacketCrashGuardMixin", "security.AutismRegistryDataCollectorMixin", "security.AutismItemPropertiesMixin", "security.AutismDataComponentInitializerMixin", diff --git a/src/test/java/autismclient/security/AutismProtectorNumericSanityTest.java b/src/test/java/autismclient/security/AutismProtectorNumericSanityTest.java new file mode 100644 index 0000000..9349d58 --- /dev/null +++ b/src/test/java/autismclient/security/AutismProtectorNumericSanityTest.java @@ -0,0 +1,42 @@ +package autismclient.security; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AutismProtectorNumericSanityTest { + + @Test + void finiteInRangeValuesAreSane() { + assertFalse(AutismProtectorNumericSanity.isNonFinite(0.0)); + assertFalse(AutismProtectorNumericSanity.isNonFinite(1.0f)); + assertFalse(AutismProtectorNumericSanity.isInsane(0.0, 0.0, 0.0)); + // A far-but-legitimate coordinate near the world border stays sane. + assertFalse(AutismProtectorNumericSanity.isInsane(2.9e7, 320.0, -2.9e7)); + // Right at the ceiling is still allowed (strictly-greater is the reject test). + assertFalse(AutismProtectorNumericSanity.isInsane(AutismProtectorNumericSanity.SANE_LIMIT, + AutismProtectorNumericSanity.SANE_LIMIT)); + } + + @Test + void nonFiniteValuesAreInsane() { + assertTrue(AutismProtectorNumericSanity.isNonFinite(Double.NaN)); + assertTrue(AutismProtectorNumericSanity.isNonFinite(Double.POSITIVE_INFINITY)); + assertTrue(AutismProtectorNumericSanity.isNonFinite(Float.NaN)); + assertTrue(AutismProtectorNumericSanity.isNonFinite(Float.NEGATIVE_INFINITY)); + assertTrue(AutismProtectorNumericSanity.isInsane(Double.NaN, AutismProtectorNumericSanity.SANE_LIMIT)); + assertTrue(AutismProtectorNumericSanity.isInsane(Float.POSITIVE_INFINITY, + AutismProtectorNumericSanity.SANE_LIMIT)); + } + + @Test + void overflowMagnitudesAreInsane() { + // The values from real crash reports. + assertTrue(AutismProtectorNumericSanity.isInsane(1.8e38, 2.8e38, 2.1e38)); + assertTrue(AutismProtectorNumericSanity.isInsane(-2.8e38, AutismProtectorNumericSanity.SANE_LIMIT)); + // Just past the ceiling on any single axis trips the triple check. + double justOver = Math.nextUp(AutismProtectorNumericSanity.SANE_LIMIT); + assertTrue(AutismProtectorNumericSanity.isInsane(0.0, justOver, 0.0)); + } +}