Added test to verify the int overflow happen (#17353)

Use long instead of int type to keep the length of the first token.

The size limit validation requires to sum two integers, one with the length of the accumulated chars till now plus the next fragment head part. If any of the two sizes is close to the max integer it generates an overflow and could successfully fail the test 9c0e50faac/logstash-core/src/main/java/org/logstash/common/BufferedTokenizerExt.java (L123).

To fall in this case it's required that sizeLimit is bigger then 2^32 bytes (2GB) and data fragments without any line delimiter is pushed to the tokenizer with a total size close to 2^32 bytes.
This commit is contained in:
Andrea Selva 2025-03-19 16:50:04 +01:00 committed by GitHub
parent 9c0e50faac
commit afde43f918
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 34 additions and 2 deletions

View file

@ -124,6 +124,8 @@ tasks.register("javaTests", Test) {
exclude '/org/logstash/plugins/factory/PluginFactoryExtTest.class'
exclude '/org/logstash/execution/ObservedExecutionTest.class'
maxHeapSize = "12g"
jacoco {
enabled = true
destinationFile = layout.buildDirectory.file('jacoco/test.exec').get().asFile

View file

@ -48,7 +48,7 @@ public class BufferedTokenizerExt extends RubyObject {
private RubyString delimiter = NEW_LINE;
private int sizeLimit;
private boolean hasSizeLimit;
private int inputSize;
private long inputSize;
private boolean bufferFullErrorNotified = false;
private String encodingName;

View file

@ -38,14 +38,19 @@ import static org.logstash.RubyUtil.RUBY;
@SuppressWarnings("unchecked")
public final class BufferedTokenizerExtWithSizeLimitTest extends RubyTestBase {
public static final int GB = 1024 * 1024 * 1024;
private BufferedTokenizerExt sut;
private ThreadContext context;
@Before
public void setUp() {
initSUTWithSizeLimit(10);
}
private void initSUTWithSizeLimit(int sizeLimit) {
sut = new BufferedTokenizerExt(RubyUtil.RUBY, RubyUtil.BUFFERED_TOKENIZER);
context = RUBY.getCurrentContext();
IRubyObject[] args = {RubyUtil.RUBY.newString("\n"), RubyUtil.RUBY.newFixnum(10)};
IRubyObject[] args = {RubyUtil.RUBY.newString("\n"), RubyUtil.RUBY.newFixnum(sizeLimit)};
sut.init(context, args);
}
@ -108,4 +113,29 @@ public final class BufferedTokenizerExtWithSizeLimitTest extends RubyTestBase {
RubyArray<RubyString> tokens = (RubyArray<RubyString>) sut.extract(context, RubyUtil.RUBY.newString("ccc\nddd\n"));
assertEquals(List.of("ccccc", "ddd"), tokens);
}
@Test
public void givenMaliciousInputExtractDoesntOverflow() {
assertEquals("Xmx must equals to what's defined in the Gradle's javaTests task",
12L * GB, Runtime.getRuntime().maxMemory());
// re-init the tokenizer with big sizeLimit
initSUTWithSizeLimit((int) (2L * GB) - 3);
// Integer.MAX_VALUE is 2 * GB
String bigFirstPiece = generateString("a", Integer.MAX_VALUE - 1024);
sut.extract(context, RubyUtil.RUBY.newString(bigFirstPiece));
// add another small fragment to trigger int overflow
// sizeLimit is (2^32-1)-3 first segment length is (2^32-1) - 1024 second is 1024 +2
// so the combined length of first and second is > sizeLimit and should throw an expection
// but because of overflow it's negative and happens to be < sizeLimit
Exception thrownException = assertThrows(IllegalStateException.class, () -> {
sut.extract(context, RubyUtil.RUBY.newString(generateString("a", 1024 + 2)));
});
assertThat(thrownException.getMessage(), containsString("input buffer full"));
}
private String generateString(String fill, int size) {
return fill.repeat(size);
}
}