mirror of
https://github.com/AutismDevelopment/Autism-Client.git
synced 2026-10-28 17:29:46 +00:00
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:
parent
9e00f40996
commit
ed2e1b24c5
6 changed files with 257 additions and 3 deletions
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue