Bug #101054 Batched Query > maxAllowedPacket size causes an ArrayIndexOutOfBoundsException
Submitted: 5 Oct 2020 14:54 Modified: 7 Oct 21:25
Reporter: Mark Rohan Email Updates:
Status: Closed Impact on me:
None 
Category:Connector / J Severity:S3 (Non-critical)
Version:8.0.21 OS:Any
Assigned to: CPU Architecture:Any

[5 Oct 2020 14:54] Mark Rohan
Description:
Adding a long SQL string to a java.sql.Statement batch will cause an ArrayIndexOutOfBoundsException if the length of the SQL statement is greater than the configured value of maxAllowedPacket.

A calculation is performed at https://github.com/mysql/mysql-connector-j/blob/release/8.0/src/main/user-impl/java/com/my... to determine if the next query will exceed the value of maxAllowedPacket.

If so, then the current query buffer is executed and the query buffer is reset. 

However, the current query buffer is empty if the first batched SQL statement size calculation exceeds maxAllowedPacket.

The execution of the empty query causes the "counter" variable to be incremented beyond the length of the "updateCounts" array when calling processMultiCountsAndKeys and assigning the returned value to the "count" variable.

When the subsequent execution of the actual SQL statement occurs and processMultiCountsAndKeys is called again using the previously incremented "count" variable, an ArrayIndexOutOfBoundsException is thrown.

In our case, at StatementImpl line 998:
- the character encoding passed to the driver is UTF-8, which makes numberOfBytesPerChar=3
- the value of maxAllowedPacket is 1048576, which the driver is providing as the default

The attached test case illustrates the issue.

How to repeat:
// Maven POM
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>connectorj-bug</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.20</version>
        </dependency>
        <dependency>
            <groupId>org.testcontainers</groupId>
            <artifactId>mysql</artifactId>
            <version>1.14.3</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>11</source>
                    <target>11</target>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

// Java Test Class
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.UUID;

import com.mysql.cj.jdbc.MysqlDataSource;
import org.junit.ClassRule;
import org.junit.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.shaded.org.apache.commons.lang.StringUtils;

public class ConnectorJBugTest {

    @ClassRule
    public static final MySQLContainer<?> mysql = new MySQLContainer<>()
            .withConnectTimeoutSeconds(1)
            .withUsername("cjbugttest")
            .withPassword("cjbugttest")
            .withDatabaseName("cjbugttest")
            .withTmpFs(new HashMap<>(){{ put("/var/lib/mysql", ""); }});

    @Test
    public void test() throws SQLException {

        final String url = String.format("jdbc:mysql://%s:%d/%s?allowMultiQueries=true&characterEncoding=UTF-8",
            mysql.getContainerIpAddress(),
            mysql.getFirstMappedPort(),
            mysql.getDatabaseName());

        final MysqlDataSource dataSource = new MysqlDataSource();
        dataSource.setUrl(url);
        dataSource.setUser(mysql.getUsername());
        dataSource.setPassword(mysql.getPassword());

        // Create table
        try (final Connection connection = dataSource.getConnection()) {
            try (final Statement statement = connection.createStatement()) {
                statement.execute("CREATE TABLE `my_table` ("
                    + "  `my_table_id` varchar(36) NOT NULL,"
                    + "  `json_blob` json NOT NULL,"
                    + "  CONSTRAINT my_table_pk PRIMARY KEY (`my_table_id`)"
                    + ") ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;");
            }
        }

        // Insert
        final String insert = String.format("insert into `my_table` (`my_table_id`, `json_blob`) values ('%s', '%s')",
            UUID.randomUUID().toString(),
            "{\"fieldA\": \"" + StringUtils.repeat("X", 175000) + "\"}");

        try (final Connection connection = dataSource.getConnection()) {
            try (final Statement statement = connection.createStatement()) {
                statement.addBatch(insert);
                statement.executeBatch();
            }
        }
    }
}

Suggested fix:
At https://github.com/mysql/mysql-connector-j/blob/release/8.0/src/main/user-impl/java/com/my..., add a check to ensure queryBuf is not empty as part of the condition for executing.
[23 Feb 2021 18:14] Filipe Silva
Hi Mark,

Thank you for your interest in MySQL and Connector/J.

This has been verified as described.
[7 Oct 21:25] Daniel So
Posted by developer:
 
Added the following entry to the C/J 9.1.0 changelog:

"When adding a long SQL string to a batched Statement, if the string's length was greater than the value of maxAllowedPacket, an ArrayIndexOutOfBoundsException was thrown. With this fix, checks are now in place so that a SQLException is thrown instead in the situation."