View Javadoc
1   /*
2    * Copyright 2026 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.handler.codec;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufAllocator;
21  import io.netty.buffer.CompositeByteBuf;
22  import io.netty.handler.codec.ByteToMessageDecoder.Cumulator;
23  import io.netty.util.internal.ObjectUtil;
24  
25  /**
26   * "Adaptive" cumulator: cumulate {@link ByteBuf}s by dynamically switching
27   * between merge and compose strategies.
28   */
29  public final class AdaptiveCumulator implements Cumulator {
30      private final int composeMinSize;
31  
32      /**
33       * @param composeMinSize Determines the minimal size of the buffer that should
34       *                       be composed (added as a new component of the
35       *                       {@link CompositeByteBuf}). If the total size of the
36       *                       last component (tail) and the incoming buffer is
37       *                       below this value, the incoming buffer is appended to
38       *                       the tail, and the new component is not added.
39       */
40      public AdaptiveCumulator(int composeMinSize) {
41          ObjectUtil.checkPositiveOrZero(composeMinSize, "composeMinSize");
42          this.composeMinSize = composeMinSize;
43      }
44  
45      /**
46       * "Adaptive" cumulator: cumulate {@link ByteBuf}s by dynamically switching
47       * between merge and compose strategies.
48       *
49       * <p>
50       * This cumulator applies a heuristic to make a decision whether to track a
51       * reference to the buffer with bytes received from the network stack in an
52       * array ("zero-copy"), or to merge into the last component (the tail) by
53       * performing a memory copy.
54       *
55       * <p>
56       * It is necessary as a protection from a potential attack on the
57       * {@link io.netty.handler.codec.ByteToMessageDecoder#COMPOSITE_CUMULATOR}.
58       * Consider a pathological case when an attacker sends TCP packages containing a
59       * single byte of data, and forcing the cumulator to track each one in a
60       * separate buffer. The cost is memory overhead for each buffer, and extra
61       * compute to read the cumulation.
62       *
63       * <p>
64       * Implemented heuristic establishes a minimal threshold for the total size of
65       * the tail and incoming buffer, below which they are merged. The sum of the
66       * tail and the incoming buffer is used to avoid a case where attacker
67       * alternates the size of data packets to trick the cumulator into always
68       * selecting compose strategy.
69       *
70       * <p>
71       * Merging strategy attempts to minimize unnecessary memory writes. When
72       * possible, it expands the tail capacity and only copies the incoming buffer
73       * into available memory.
74       * Otherwise, when both tail and the buffer must be copied, the tail is
75       * reallocated (or fully replaced) with a new buffer of exponentially increasing
76       * capacity (bounded to {@link #composeMinSize}) to ensure runtime
77       * {@code O(n^2)} is amortized to {@code O(n)}.
78       */
79      @Override
80      @SuppressWarnings("ReferenceEquality")
81      public ByteBuf cumulate(ByteBufAllocator alloc, ByteBuf cumulation, ByteBuf in) {
82          if (cumulation == in) {
83              in.release();
84              return cumulation;
85          }
86          if (!cumulation.isReadable()) {
87              cumulation.release();
88              return in;
89          }
90          CompositeByteBuf composite = null;
91          boolean cumulationTransferred = false;
92          try {
93              if (isOwnedCompositeBuf(cumulation)) {
94                  composite = (CompositeByteBuf) cumulation;
95                  cumulationTransferred = true;
96                  // Writer index must equal capacity if we are going to "write"
97                  // new components to the end
98                  if (composite.writerIndex() != composite.capacity()) {
99                      composite.capacity(composite.writerIndex());
100                 }
101             } else {
102                 composite = alloc.compositeBuffer(Integer.MAX_VALUE);
103                 composite.addFlattenedComponents(true, cumulation);
104                 cumulationTransferred = true;
105             }
106             ByteBuf b = in;
107             in = null;
108             addInput(alloc, composite, b);
109 
110             CompositeByteBuf result = composite;
111             composite = null;
112             return result;
113         } catch (Throwable t) {
114             // If an exception was thrown AFTER cumulation was successfully wrapped,
115             // calling composite.release() in 'finally' will drop its refCount to 0.
116             // We prevent this by calling retain() here on the exception path to keep it alive.
117             if (cumulationTransferred && composite != null && composite != cumulation) {
118                 cumulation.retain();
119             }
120             throw t;
121         } finally {
122             if (in != null) {
123                 // We must release if the ownership was not transferred as otherwise it may
124                 // produce a leak
125                 in.release();
126             }
127             // Also release any new buffer allocated if we're not returning it
128             if (composite != null && composite != cumulation) {
129                 composite.release();
130             }
131         }
132     }
133 
134     private static boolean isOwnedCompositeBuf(ByteBuf buf) {
135         return buf instanceof CompositeByteBuf && buf.refCnt() == 1;
136     }
137 
138     private void addInput(ByteBufAllocator alloc, CompositeByteBuf composite, ByteBuf in) {
139         if (shouldCompose(composite, in, composeMinSize)) {
140             composite.addFlattenedComponents(true, in);
141         } else {
142             // The total size of the new data and the last component are below the
143             // threshold. Merge them.
144             mergeWithCompositeTail(alloc, composite, in);
145         }
146     }
147 
148     private static boolean shouldCompose(CompositeByteBuf composite, ByteBuf in, int composeMinSize) {
149         int componentCount = composite.numComponents();
150         if (componentCount == 0) {
151             return true;
152         }
153         int inputSize = in.readableBytes();
154         int tailStart = composite.toByteIndex(componentCount - 1);
155         long tailSize = composite.writerIndex() - tailStart;
156         return tailSize + inputSize >= composeMinSize;
157     }
158 
159     /**
160      * Append the given {@link ByteBuf} {@code in} to {@link CompositeByteBuf}
161      * {@code composite} by expanding or replacing the tail component of the {@link
162      * CompositeByteBuf}.
163      *
164      * <p>
165      * The goal is to prevent {@code O(n^2)} runtime in a pathological case, that
166      * forces copying the tail component into a new buffer, for each incoming
167      * single-byte buffer.
168      * We append the new bytes to the tail, when a write (or a fast write) is
169      * possible.
170      *
171      * <p>
172      * Otherwise, the tail is replaced with a new buffer, with the capacity
173      * increased enough to achieve runtime amortization.
174      *
175      * <p>
176      * We assume that implementations of
177      * {@link ByteBufAllocator#calculateNewCapacity(int, int)},
178      * are similar to
179      * {@link io.netty.buffer.AbstractByteBufAllocator#calculateNewCapacity(int, int)},
180      * which doubles buffer capacity by normalizing it to the closest power of two.
181      * This assumption is verified in unit tests for this method.
182      */
183     private static void mergeWithCompositeTail(
184             ByteBufAllocator alloc, CompositeByteBuf composite, ByteBuf in) {
185         int inputSize = in.readableBytes();
186         int tailComponentIndex = composite.numComponents() - 1;
187         int tailStart = composite.toByteIndex(tailComponentIndex);
188         int tailSize = composite.writerIndex() - tailStart;
189         int newTailSize = inputSize + tailSize;
190 
191         ByteBuf tail = composite.component(tailComponentIndex);
192         ByteBuf newTail = null;
193         // Use componentSlice() to get the correct view of the indices.
194         ByteBuf componentView = composite.componentSlice(tailComponentIndex);
195         try {
196             // Ideal case: The tail is not shared and can be expanded in-place.
197             // In-place expansion should happen only if the component represents the full
198             // capacity of the underlying buffer because if tail.capacity() >
199             // componentView.capacity(), it indicates the component is a partial slice
200             // containing hidden "discarded" bytes. Expanding such a slice in-place would
201             // "resurrect" those discarded bytes leading to silent data corruption.
202             if (tail.refCnt() == 1 && !tail.isReadOnly() && tail.capacity() == componentView.capacity()
203                     && newTailSize <= tail.maxCapacity()) {
204                 // Take ownership of the tail.
205                 newTail = tail.retain();
206 
207                 // Synchronize indices based on the component's view in the composite buffer.
208                 newTail.setIndex(componentView.readerIndex(), componentView.writerIndex());
209 
210                 /*
211                  * The tail is a readable non-composite buffer, so writeBytes() handles
212                  * everything for us.
213                  *
214                  * - ensureWritable() performs a fast resize when possible (f.e. PooledByteBuf
215                  * simply updates its boundary to the end of consecutive memory run assigned to
216                  * this buffer)
217                  * - when the required size doesn't fit into writableBytes(), a new buffer is
218                  * allocated, and the capacity is calculated with alloc.calculateNewCapacity()
219                  * - note that maxFastWritableBytes() would normally allow a fast expansion of
220                  * PooledByteBuf is not called because CompositeByteBuf.component() returns a
221                  * duplicate, wrapped buffer.
222                  * Unwrapping buffers is unsafe, and potential benefit of fast writes may not be
223                  * as pronounced because the capacity is doubled with each reallocation.
224                  */
225                 newTail.writeBytes(in);
226             } else {
227                 // Fallback strategy: Reallocate a new buffer to merge the tail and input.
228                 // This ensures absolute index consistency and prevents data corruption
229                 // from hidden offsets in sliced or derived buffers.
230                 newTail = alloc.buffer(alloc.calculateNewCapacity(newTailSize, Integer.MAX_VALUE));
231                 newTail.setBytes(0, composite, tailStart, tailSize)
232                         .setBytes(tailSize, in, in.readerIndex(), inputSize)
233                         .writerIndex(newTailSize);
234                 in.readerIndex(in.writerIndex());
235             }
236 
237             // Store readerIndex to avoid out-of-bounds writerIndex during replacement.
238             int prevReader = composite.readerIndex();
239 
240             // Remove the old tail and add the new one.
241             composite.removeComponent(tailComponentIndex).setIndex(0, tailStart);
242 
243             // newTail ownership successfully transferred to the composite buffer.
244             // We null out newTail before adding it to avoid a double-release if addFlattenedComponents throws.
245             ByteBuf b = newTail;
246             newTail = null;
247             composite.addFlattenedComponents(true, b);
248 
249             // Restore the reader. We do this before releasing 'in' so that if it fails,
250             // the caller's finally block will handle releasing 'in' without a double-free.
251             composite.readerIndex(prevReader);
252         } finally {
253             in.release();
254             // If new tail's ownership isn't transferred to the composite buf.
255             // Release it to prevent a leak.
256             if (newTail != null) {
257                 newTail.release();
258             }
259         }
260     }
261 }