Add packet crash guard to the protector

Drop incoming play packets carrying malformed (NaN/Infinity) or absurdly
large positions/velocities before the game applies them and overflows its
own section/collision math and crashes.

Some servers send such values deliberately to crash connected clients
(observed per-axis values around 1.8e38/2.8e38/2.1e38, reproducible with
optimisation mods disabled, so it is a vanilla-math overflow). A single
AutismProtectorNumericSanity check guards five vectors on
ClientPacketListener: explosion, player teleport, entity teleport, entity
position sync and entity spawn.

- AutismProtectorNumericSanity: pure, unit-tested sanity predicates.
- AutismProtectorPacketCrashGuardMixin: drops a packet whose position or
  velocity is out of range; require = 0 so a mapping rename degrades to
  "that packet unguarded" rather than failing the required mixin config.
- Gated by AutismProtector.shouldGuardCrashPackets() / config field
  protectorCrashGuard (default on); inherits the security package's
  opsec/external-protector deferral.

Adapted from BossCrashGuard (github.com/WaterBoss11/BossCrashGuard).
This commit is contained in:
WaterBoss11 2026-07-21 15:34:34 -07:00
parent 9e00f40996
commit ed2e1b24c5
6 changed files with 257 additions and 3 deletions

View file

@ -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.
*
* <p>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:
*
* <ul>
* <li>explosion knockback ({@link ClientboundExplodePacket}) — center, radius and player-knockback vector;</li>
* <li>the local player teleport ({@link ClientboundPlayerPositionPacket}) — position and delta-movement;</li>
* <li>entity teleports ({@link ClientboundTeleportEntityPacket}) — position and delta-movement;</li>
* <li>entity position syncs ({@link ClientboundEntityPositionSyncPacket}) — position and delta-movement; and</li>
* <li>entity spawns ({@link ClientboundAddEntityPacket}) — spawn position.</li>
* </ul>
*
* 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.
*
* <p>Purely defensive and local: it only <em>drops</em> 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}).
*
* <p>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.
*
* <p>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<Vec3> 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);
}
}
}

View file

@ -104,6 +104,10 @@ public final class AutismProtector {
return state().skipChatSigning; return state().skipChatSigning;
} }
public static boolean shouldGuardCrashPackets() {
return state().guardCrashPackets;
}
public static void refreshRuntimeState() { public static void refreshRuntimeState() {
runtimeState = buildRuntimeState(); runtimeState = buildRuntimeState();
runtimeStateExpiresAt = System.nanoTime() + STATE_REFRESH_NANOS; runtimeStateExpiresAt = System.nanoTime() + STATE_REFRESH_NANOS;
@ -138,7 +142,10 @@ public final class AutismProtector {
!overlap && config.protectorBlockLocalUrls, !overlap && config.protectorBlockLocalUrls,
!overlap && config.protectorIsolatePackCache, !overlap && config.protectorIsolatePackCache,
config.protectorStripServerPacks, 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 blockLocalUrls,
boolean isolatePackCache, boolean isolatePackCache,
boolean stripServerPacks, boolean stripServerPacks,
boolean skipChatSigning boolean skipChatSigning,
boolean guardCrashPackets
) { ) {
static RuntimeState inactive() { 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);
} }
} }

View file

@ -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.
*
* <p>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:
*
* <ul>
* <li>{@link #SANE_LIMIT} ({@code 1e9}) — the ceiling for a position or velocity component the client will
* apply. The world border caps coordinates near {@code 3.0e7} and real per-tick speeds stay well under
* ~100, so {@code 1e9} clears every legitimate value by a wide margin while still sitting far below the
* values that overflow the game's math.</li>
* </ul>
*
* <p>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.
*
* <p>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);
}
}

View file

@ -63,6 +63,8 @@ public final class AutismConfig {
public boolean protectorStripServerPacks = false; public boolean protectorStripServerPacks = false;
public boolean protectorChatSigningOff = false; public boolean protectorChatSigningOff = false;
public boolean protectorCrashGuard = true;
public boolean performanceDebug = false; public boolean performanceDebug = false;
public boolean inventoryMove = false; public boolean inventoryMove = false;
public boolean xCarry = true; public boolean xCarry = true;

View file

@ -164,6 +164,7 @@
"security.AutismProtectorResourceLoaderImplMixin", "security.AutismProtectorResourceLoaderImplMixin",
"security.AutismProtectorPacketProcessorMixin", "security.AutismProtectorPacketProcessorMixin",
"security.AutismProtectorPacketDecoderMixin", "security.AutismProtectorPacketDecoderMixin",
"security.AutismProtectorPacketCrashGuardMixin",
"security.AutismRegistryDataCollectorMixin", "security.AutismRegistryDataCollectorMixin",
"security.AutismItemPropertiesMixin", "security.AutismItemPropertiesMixin",
"security.AutismDataComponentInitializerMixin", "security.AutismDataComponentInitializerMixin",

View file

@ -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));
}
}