Animated-TabList

Animated-TabList

65.2k Downloads

Tablist objectives throws ArrayIndexOutOfBoundsException in `HashMap#keysToArray` when accessed in another thread

montlikadani opened this issue ยท 1 comments

commented

Type of bug

Error in console

TabList version

Reproducable in all versions of TL from when the tablist objects has added

Software version

Reproducible in all versions of Spigot and forks from when the objectives added

Relevant plugins

No response

Console error (if applicable)

java.lang.ArrayIndexOutOfBoundsException: Index 112816 out of bounds for length 112816
	at java.util.HashMap.keysToArray(HashMap.java:952) ~[?:?]
	at java.util.HashMap$KeySet.toArray(HashMap.java:995) ~[?:?]
	at java.util.ArrayList.<init>(ArrayList.java:181) ~[?:?]
	at com.google.common.collect.Lists.newArrayList(Lists.java:132) ~[patched_1.17.1.jar:git-Paper-399]
	at net.minecraft.world.scores.Scoreboard.getTrackedPlayers(Scoreboard.java:115) ~[patched_1.17.1.jar:git-Paper-399]
	at org.bukkit.craftbukkit.v1_17_R1.scoreboard.CraftScore.getScore(CraftScore.java:44) ~[patched_1.17.1.jar:git-Paper-399]
	at hu.montlikadani.tablist.Objects.lambda$startTask$1(Objects.java:125) ~[TabList-bukkit-5.6.0.jar:?]

TabList configuration files

Not required, reproducible with the default enabled tablist objectives.

Bug description

The exception refers to this line:

if (object.getScore(entry).getScore() != objectScore.get()) {

Within this is the Objective#getScore method, in which non-thread safe changes are made to map reference. When another asynchronous thread calls the non thread-safe method(s), as in this case, and the size of the Map changes due to one or more new elements added, it throws an ArrayIndexOutOfBoundsException and this by repetition as it runs on a continuously running asynchronous thread. The source of the HashMap#keysToArray method shows that indexing is not handled properly because the value would start counting from 0 (as the initial value of the elements added to the array in C-type languages), but in this implementation it already increases indexing by +1, which will later throwing the above mentioned exception.

The HashMap#keysToArray method implementation is as follow:
L943-L955
This is equally true for the valuesToArray method, as it has only values with the same implementation.

Reproduction process/simple approach

It can also be reproduced in a simple application:

// Create a new empty map
java.util.Map<Integer, java.util.Map<String, Integer>> map = new java.util.HashMap<>();

// Perform the iteration in asynchronous thread
java.util.concurrent.CompletableFuture.supplyAsync(() -> {

    // Can be any number, it is more reproducible if it more than 100+
    for (int i = 0; i <= 112816; i++) {
        java.util.Map<String, Integer> map2 = new java.util.HashMap<>();

        map2.put("fg", 5);
        map.put(i, map2);
    }

    return 1;
}).whenComplete((a, th) -> {
    // Here the thread is done, so this will not throw ArrayIndexOutOfBoundsException
    new java.util.ArrayList<>(map.keySet());
});

// ArrayIndexOutOfBoundsException will be thrown here as the "map" changed in the asynchronous thread and its not done yet
new java.util.ArrayList<>(map.keySet());

Current solution

Not much can be done. For now, the solution is to switch to reflection, which is slower but more reliable (unplanned).

It is not yet reported to the JDK as it is probably just a misunderstood thread problem. On the other hand, it is not easy to report exceptions/errors found by the user to JDK (I think it needs to be confirmed by one of the group member of JDK).

A simple solution to this problem in HashMap is (I only copied two lines):

for (; e != null && idx < r.length; e = e.next) {
  r[idx] = e.key;
  idx++;
}
commented

The linked commit above will hopefully fixes this issue. Probably for sure, since the objects is now uses packets with accessing with reflections, it is slower but more reliable.