summaryrefslogtreecommitdiffhomepage
path: root/patches/server/0969-Prevent-sending-oversized-item-data-in-equipment-and.patch
blob: 121b75eca68bd183b4aad40eb2775ac2dfa35fdb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Nassim Jahnke <nassim@njahnke.dev>
Date: Wed, 1 Dec 2021 12:36:25 +0100
Subject: [PATCH] Prevent sending oversized item data in equipment and metadata

Co-authored-by: Jake Potrebic <jake.m.potrebic@gmail.com>

diff --git a/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java b/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java
new file mode 100644
index 0000000000000000000000000000000000000000..72483dedd3b1864fca3463d3ba3f6fad22680b97
--- /dev/null
+++ b/src/main/java/io/papermc/paper/util/DataSanitizationUtil.java
@@ -0,0 +1,108 @@
+package io.papermc.paper.util;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.UnaryOperator;
+import it.unimi.dsi.fastutil.objects.ObjectArrayList;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.util.Mth;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.item.component.BundleContents;
+import net.minecraft.world.item.component.ChargedProjectiles;
+import net.minecraft.world.item.component.ItemContainerContents;
+import org.apache.commons.lang3.math.Fraction;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.framework.qual.DefaultQualifier;
+
+@DefaultQualifier(NonNull.class)
+public final class DataSanitizationUtil {
+
+    private static final ThreadLocal<DataSanitizer> DATA_SANITIZER = ThreadLocal.withInitial(DataSanitizer::new);
+
+    public static DataSanitizer start(final boolean sanitize) {
+        final DataSanitizer sanitizer = DATA_SANITIZER.get();
+        if (sanitize) {
+            sanitizer.start();
+        }
+        return sanitizer;
+    }
+
+    public static final StreamCodec<RegistryFriendlyByteBuf, ChargedProjectiles> CHARGED_PROJECTILES = codec(ChargedProjectiles.STREAM_CODEC, DataSanitizationUtil::sanitizeChargedProjectiles);
+    public static final StreamCodec<RegistryFriendlyByteBuf, BundleContents> BUNDLE_CONTENTS = codec(BundleContents.STREAM_CODEC, DataSanitizationUtil::sanitizeBundleContents);
+    public static final StreamCodec<RegistryFriendlyByteBuf, ItemContainerContents> CONTAINER = codec(ItemContainerContents.STREAM_CODEC, contents -> ItemContainerContents.EMPTY);
+
+    private static ChargedProjectiles sanitizeChargedProjectiles(final ChargedProjectiles projectiles) {
+        if (projectiles.isEmpty()) {
+            return projectiles;
+        }
+
+        return ChargedProjectiles.of(List.of(
+            new ItemStack(projectiles.contains(Items.FIREWORK_ROCKET) ? Items.FIREWORK_ROCKET : Items.ARROW)
+        ));
+    }
+
+    private static BundleContents sanitizeBundleContents(final BundleContents contents) {
+        if (contents.isEmpty()) {
+            return contents;
+        }
+
+        // Bundles change their texture based on their fullness.
+        // A bundles content weight may be anywhere from 0 to, basically, infinity.
+        // A weight of 1 is the usual maximum case
+        int sizeUsed = Mth.mulAndTruncate(contents.weight(), 64);
+        // Early out, *most* bundles should not be overfilled above a weight of one.
+        if (sizeUsed <= 64) return new BundleContents(List.of(new ItemStack(Items.PAPER, Math.max(1, sizeUsed))));
+
+        final List<ItemStack> sanitizedRepresentation = new ObjectArrayList<>(sizeUsed / 64 + 1);
+        while (sizeUsed > 0) {
+            final int stackCount = Math.min(64, sizeUsed);
+            sanitizedRepresentation.add(new ItemStack(Items.PAPER, stackCount));
+            sizeUsed -= stackCount;
+        }
+        // Now we add a single fake item that uses the same amount of slots as all other items.
+        // Ensure that potentially overstacked bundles are not represented by empty (count=0) itemstacks.
+        return new BundleContents(sanitizedRepresentation);
+    }
+
+    private static <B, A> StreamCodec<B, A> codec(final StreamCodec<B, A> delegate, final UnaryOperator<A> sanitizer) {
+        return new DataSanitizationCodec<>(delegate, sanitizer);
+    }
+
+    private record DataSanitizationCodec<B, A>(StreamCodec<B, A> delegate, UnaryOperator<A> sanitizer) implements StreamCodec<B, A> {
+
+        @Override
+        public @NonNull A decode(final @NonNull B buf) {
+            return this.delegate.decode(buf);
+        }
+
+        @Override
+        public void encode(final @NonNull B buf, final @NonNull A value) {
+            if (!DATA_SANITIZER.get().value().get()) {
+                this.delegate.encode(buf, value);
+            } else {
+                this.delegate.encode(buf, this.sanitizer.apply(value));
+            }
+        }
+    }
+
+    public record DataSanitizer(AtomicBoolean value) implements AutoCloseable {
+
+        public DataSanitizer() {
+            this(new AtomicBoolean(false));
+        }
+
+        public void start() {
+            this.value.compareAndSet(false, true);
+        }
+
+        @Override
+        public void close() {
+            this.value.compareAndSet(true, false);
+        }
+    }
+
+    private DataSanitizationUtil() {
+    }
+}
diff --git a/src/main/java/net/minecraft/core/component/DataComponents.java b/src/main/java/net/minecraft/core/component/DataComponents.java
index c9aef759c1485da753e820f9b509117ca50a31e4..60757f8df706cba92350d73503b73913cff3bcfc 100644
--- a/src/main/java/net/minecraft/core/component/DataComponents.java
+++ b/src/main/java/net/minecraft/core/component/DataComponents.java
@@ -138,10 +138,10 @@ public class DataComponents {
         "map_post_processing", builder -> builder.networkSynchronized(MapPostProcessing.STREAM_CODEC)
     );
     public static final DataComponentType<ChargedProjectiles> CHARGED_PROJECTILES = register(
-        "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(ChargedProjectiles.STREAM_CODEC).cacheEncoding()
+        "charged_projectiles", builder -> builder.persistent(ChargedProjectiles.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CHARGED_PROJECTILES).cacheEncoding() // Paper - sanitize charged projectiles
     );
     public static final DataComponentType<BundleContents> BUNDLE_CONTENTS = register(
-        "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(BundleContents.STREAM_CODEC).cacheEncoding()
+        "bundle_contents", builder -> builder.persistent(BundleContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.BUNDLE_CONTENTS).cacheEncoding() // Paper - sanitize bundle contents
     );
     public static final DataComponentType<PotionContents> POTION_CONTENTS = register(
         "potion_contents", builder -> builder.persistent(PotionContents.CODEC).networkSynchronized(PotionContents.STREAM_CODEC).cacheEncoding()
@@ -208,7 +208,7 @@ public class DataComponents {
         "pot_decorations", builder -> builder.persistent(PotDecorations.CODEC).networkSynchronized(PotDecorations.STREAM_CODEC).cacheEncoding()
     );
     public static final DataComponentType<ItemContainerContents> CONTAINER = register(
-        "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(ItemContainerContents.STREAM_CODEC).cacheEncoding()
+        "container", builder -> builder.persistent(ItemContainerContents.CODEC).networkSynchronized(io.papermc.paper.util.DataSanitizationUtil.CONTAINER).cacheEncoding() // Paper - sanitize container contents
     );
     public static final DataComponentType<BlockItemStateProperties> BLOCK_STATE = register(
         "block_state", builder -> builder.persistent(BlockItemStateProperties.CODEC).networkSynchronized(BlockItemStateProperties.STREAM_CODEC).cacheEncoding()
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
index 59c1c103545f04fd35e6932df64a9910a1d74cd7..56bde49e6b7790155b032d0be40961d566ab89e9 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEntityDataPacket.java
@@ -19,9 +19,11 @@ public record ClientboundSetEntityDataPacket(int id, List<SynchedEntityData.Data
     }
 
     private static void pack(List<SynchedEntityData.DataValue<?>> trackedValues, RegistryFriendlyByteBuf buf) {
+        try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(true)) { // Paper - data sanitization
         for (SynchedEntityData.DataValue<?> dataValue : trackedValues) {
             dataValue.write(buf);
         }
+        } // Paper - data sanitization
 
         buf.writeByte(255);
     }
diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
index 2ea7e90d582866b4e29db80836e250163d811763..d152871142d3def2ac04f50037db53b0527f7894 100644
--- a/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundSetEquipmentPacket.java
@@ -19,6 +19,13 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
     private final List<Pair<EquipmentSlot, ItemStack>> slots;
 
     public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList) {
+        // Paper start - data sanitization
+        this(entityId, equipmentList, false);
+    }
+    private boolean sanitize;
+    public ClientboundSetEquipmentPacket(int entityId, List<Pair<EquipmentSlot, ItemStack>> equipmentList, boolean sanitize) {
+        this.sanitize = sanitize;
+        // Paper end - data sanitization
         this.entity = entityId;
         this.slots = equipmentList;
     }
@@ -41,6 +48,7 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
         buf.writeVarInt(this.entity);
         int i = this.slots.size();
 
+        try (var ignored = io.papermc.paper.util.DataSanitizationUtil.start(this.sanitize)) {  // Paper - data sanitization
         for (int j = 0; j < i; j++) {
             Pair<EquipmentSlot, ItemStack> pair = this.slots.get(j);
             EquipmentSlot equipmentSlot = pair.getFirst();
@@ -49,6 +57,7 @@ public class ClientboundSetEquipmentPacket implements Packet<ClientGamePacketLis
             buf.writeByte(bl ? k | -128 : k);
             ItemStack.OPTIONAL_STREAM_CODEC.encode(buf, pair.getSecond());
         }
+        } // Paper - data sanitization
     }
 
     @Override
diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java
index 0e7ace92522fbd4cef7b2c2b8a0f8b86c2cce192..1d849ce4e2c85f149af25318b8ffb6dcef6c6788 100644
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
@@ -349,7 +349,7 @@ public class ServerEntity {
             }
 
             if (!list.isEmpty()) {
-                sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list));
+                sender.accept(new ClientboundSetEquipmentPacket(this.entity.getId(), list, true)); // Paper - data sanitization
             }
             ((LivingEntity) this.entity).detectEquipmentUpdatesPublic(); // CraftBukkit - SPIGOT-3789: sync again immediately after sending
         }
diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 5215783353021583e7a726d281e4d1734398f073..0fe4c3ff5dd855aea58292c16ba9a2f5d23c00f5 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -2731,7 +2731,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl
                                 entity.refreshEntityData(ServerGamePacketListenerImpl.this.player);
                                 // SPIGOT-7136 - Allays
                                 if (entity instanceof Allay || entity instanceof net.minecraft.world.entity.animal.horse.AbstractHorse) { // Paper - Fix horse armor desync
-                                    ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) entity).getItemBySlot(slot).copy())).collect(Collectors.toList())));
+                                    ServerGamePacketListenerImpl.this.send(new ClientboundSetEquipmentPacket(entity.getId(), Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()).map((slot) -> Pair.of(slot, ((LivingEntity) entity).getItemBySlot(slot).copy())).collect(Collectors.toList()), true)); // Paper - sanitize
                                 }
 
                                 ServerGamePacketListenerImpl.this.player.containerMenu.sendAllDataToRemote(); // Paper - fix slot desync - always refresh player inventory
diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java
index b1e894e9a9cd87f7259302d15d5b5b0e2b32c4ea..7b6d067eb8b22a8e0a82a2969c92ba243230733a 100644
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
@@ -3353,7 +3353,7 @@ public abstract class LivingEntity extends Entity implements Attackable {
             }
 
         });
-        ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list));
+        ((ServerLevel) this.level()).getChunkSource().broadcast(this, new ClientboundSetEquipmentPacket(this.getId(), list, true)); // Paper - data sanitization
     }
 
     private ItemStack getLastArmorItem(EquipmentSlot slot) {