aboutsummaryrefslogtreecommitdiffhomepage
path: root/patches/server/0960-Prevent-sending-oversized-item-data-in-equipment-and.patch
blob: ec6969e742192b9ad0293ea7d8eb4d52a8f9b2e4 (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 a7f6a53cd1168bfc53b616cb0586a546fb9793e7..5e3bfd4a06b4a547a5f813f3d0c533fb7f4451f3 100644
--- a/src/main/java/net/minecraft/core/component/DataComponents.java
+++ b/src/main/java/net/minecraft/core/component/DataComponents.java
@@ -180,10 +180,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()
@@ -250,7 +250,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 e46030178b3b54168a3532def308a08b10888723..830bd76916e26a3a54954d3cf7b7520af52a2258 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;
     }
@@ -40,6 +47,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();
@@ -48,6 +56,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 f3456aeeab7eee5b6d0383a4bf1338dd8cc95bb3..b2fd3e936559c8fcb8b02ae3ef63c4f3bd0edb08 100644
--- a/src/main/java/net/minecraft/server/level/ServerEntity.java
+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java
@@ -388,7 +388,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 af3e0049beb5590520ed84b52d6df85ad22a8f23..b7ff8607cd33d8e6bdab9533792cf43a434210bd 100644
--- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -2751,7 +2751,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 563e008740bf2017a1767470a2e34629dfa5cfa1..51c3ab2baae8cbd246464777b8ddbde2d3d9d0ef 100644
--- a/src/main/java/net/minecraft/world/entity/LivingEntity.java
+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java
@@ -3460,7 +3460,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) {