Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ public long increaseV2(AccountCapsule accountCapsule, ResourceCode resourceCode,
}

long newUsage = getUsage(averageLastUsage, oldWindowSize, averageUsage, this.windowSize);
// remainUsage is the decayed lastUsage
long remainUsage = getUsage(averageLastUsage, oldWindowSize);
if (remainUsage == 0) {
accountCapsule.setNewWindowSizeV2(resourceCode, this.windowSize * WINDOW_SIZE_PRECISION);
Expand Down
14 changes: 8 additions & 6 deletions chainbase/src/main/java/org/tron/core/db/TransactionTrace.java
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,14 @@ private void resetAccountUsage(AccountCapsule accountCap,
}
long currentSize = accountCap.getWindowSize(ENERGY);
long currentUsage = accountCap.getEnergyUsage();
// Drop the pre consumed frozen energy
// Drop the preconsumed frozen energy
long newArea = currentUsage * currentSize
- (mergedUsage * mergedSize - usage * size);
// If area merging happened during suicide, use the current window size
long newSize = mergedSize == currentSize ? size : currentSize;
// Calc new usage by fixed x-axes
long newUsage = max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath());
// A zero window size means no valid time window exists and thus zero usage
long newUsage = newSize == 0 ? 0
: max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath());
Comment on lines +304 to +305
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify zero-size persistence and denominator usage paths.
rg -n -C2 --type=java \
'newUsage == 0 \? 0L : newSize|newUsage == 0 \? 0L : newSize2|divideCeil\(lastUsage \* this\.precision, oldWindowSize\)|divideCeil\(lastUsage \* precision, windowSize\)'

Repository: Sunny6889/java-tron

Length of output: 3476


🏁 Script executed:

#!/bin/bash
# Get divideCeil implementation to check for zero-handling
rg -A10 "^\s*long divideCeil\(" --type=java chainbase/src/main/java/org/tron/core/db/

Repository: Sunny6889/java-tron

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Check for guards before divideCeil in ResourceProcessor.increase() method (lines 47-50)
sed -n '40,60p' chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java

Repository: Sunny6889/java-tron

Length of output: 1022


🏁 Script executed:

#!/bin/bash
# Check the full context of line 78-80 in ResourceProcessor
sed -n '70,90p' chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java

Repository: Sunny6889/java-tron

Length of output: 941


🏁 Script executed:

#!/bin/bash
# Check variables available at line 308 (currentSize context)
sed -n '290,310p' chainbase/src/main/java/org/tron/core/db/TransactionTrace.java

Repository: Sunny6889/java-tron

Length of output: 1127


🏁 Script executed:

#!/bin/bash
# Find divideCeil implementation
rg -B2 -A8 "private.*divideCeil|protected.*divideCeil|public.*divideCeil" --type=java chainbase/src/main/java/org/tron/core/db/

Repository: Sunny6889/java-tron

Length of output: 1117


🏁 Script executed:

#!/bin/bash
# Check V2 version around line 322
sed -n '310,340p' chainbase/src/main/java/org/tron/core/db/TransactionTrace.java

Repository: Sunny6889/java-tron

Length of output: 1392


🏁 Script executed:

#!/bin/bash
# Check for any guards before divideCeil calls in ResourceProcessor - full increase method
sed -n '73,85p' chainbase/src/main/java/org/tron/core/db/ResourceProcessor.java

Repository: Sunny6889/java-tron

Length of output: 755


Persisted zero window sizes cause division by zero in ResourceProcessor.

The zero window size persistence at line 308 (newUsage == 0 ? 0L : newSize) and line 326 (newUsage == 0 ? 0L : newSize2) creates a critical issue. When ResourceProcessor.increase() retrieves the stored window size and calls divideCeil(lastUsage * precision, oldWindowSize) at line 78, a persisted zero will cause ArithmeticException: / by zero since divideCeil performs direct division without checking the denominator.

The available variables (currentSize at line 295, currentSize2 at line 313) enable the proposed fallback: persist a positive value (e.g., currentSize > 0 ? currentSize : 1L) when usage is zero, rather than persisting 0L.

Proposed fix
    accountCap.setEnergyUsage(newUsage);
-   accountCap.setNewWindowSize(ENERGY, newUsage == 0 ? 0L : newSize);
+   long persistedSize = newUsage == 0 ? (currentSize > 0 ? currentSize : 1L) : newSize;
+   accountCap.setNewWindowSize(ENERGY, persistedSize);

and

    accountCap.setEnergyUsage(newUsage);
-   accountCap.setNewWindowSizeV2(ENERGY, newUsage == 0 ? 0L : newSize2);
+   long persistedSize2 = newUsage == 0 ? (currentSize2 > 0 ? currentSize2 : 1L) : newSize2;
+   accountCap.setNewWindowSizeV2(ENERGY, persistedSize2);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@chainbase/src/main/java/org/tron/core/db/TransactionTrace.java` around lines
304 - 305, Persisted zero window sizes in TransactionTrace lead to
divide-by-zero later in ResourceProcessor.increase() when divideCeil(lastUsage *
precision, oldWindowSize) is called; modify the code that writes window sizes
(the expressions that set newUsage/newSize and newUsage/newSize2 in
TransactionTrace) so that when usage is zero you persist a positive fallback
(e.g., use currentSize > 0 ? currentSize : 1L and currentSize2 > 0 ?
currentSize2 : 1L) instead of 0L, ensuring divideCeil never receives a zero
denominator; update the logic around the newUsage computations to use these
non-zero fallbacks while keeping the existing semantics when usage is non-zero.

// Reset account usage and window size
accountCap.setEnergyUsage(newUsage);
accountCap.setNewWindowSize(ENERGY, newUsage == 0 ? 0L : newSize);
Expand All @@ -312,13 +313,14 @@ private void resetAccountUsageV2(AccountCapsule accountCap,
long currentSize = accountCap.getWindowSize(ENERGY);
long currentSize2 = accountCap.getWindowSizeV2(ENERGY);
long currentUsage = accountCap.getEnergyUsage();
// Drop the pre consumed frozen energy
// Drop the preconsumed frozen energy
long newArea = currentUsage * currentSize - (mergedUsage * mergedSize - usage * size);
// If area merging happened during suicide, use the current window size
long newSize = mergedSize == currentSize ? size : currentSize;
long newSize2 = mergedSize == currentSize ? size2 : currentSize2;
// Calc new usage by fixed x-axes
long newUsage = max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath());
// A zero window size means no valid time window exists and thus zero usage
long newUsage = newSize == 0 ? 0
: max(0, newArea / newSize, dynamicPropertiesStore.disableJavaLangMath());
// Reset account usage and window size
accountCap.setEnergyUsage(newUsage);
accountCap.setNewWindowSizeV2(ENERGY, newUsage == 0 ? 0L : newSize2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import com.google.common.base.Strings;
import io.prometheus.client.Histogram;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
Expand Down Expand Up @@ -31,7 +33,25 @@
@Slf4j
public abstract class RateLimiterServlet extends HttpServlet {
private static final String KEY_PREFIX_HTTP = "http_";
private static final String ADAPTER_PREFIX = "org.tron.core.services.ratelimiter.adapter.";

// Whitelist of allowed rate limiter adapter class names.
// Keys are derived from Class.getSimpleName() so they stay in sync if classes are renamed.
// Class.forName() is intentionally NOT used; only these pre-approved classes may be loaded.
private static final Map<String, Class<? extends IRateLimiter>> ALLOWED_ADAPTERS;
private static final String DEFAULT_ADAPTER_NAME = DefaultBaseQqsAdapter.class.getSimpleName();

static {
Map<String, Class<? extends IRateLimiter>> m = new HashMap<>();
for (Class<? extends IRateLimiter> c : new Class[]{
GlobalPreemptibleAdapter.class,
QpsRateLimiterAdapter.class,
IPQPSRateLimiterAdapter.class,
DefaultBaseQqsAdapter.class
}) {
m.put(c.getSimpleName(), c);
}
ALLOWED_ADAPTERS = Collections.unmodifiableMap(m);
}

@Autowired
private RateLimiterContainer container;
Expand All @@ -40,42 +60,22 @@ public abstract class RateLimiterServlet extends HttpServlet {
private void addRateContainer() {
RateLimiterInitialization.HttpRateLimiterItem item = Args.getInstance()
.getRateLimiterInitialization().getHttpMap().get(getClass().getSimpleName());
boolean success = false;
final String name = getClass().getSimpleName();
if (item != null) {
String cName = "";
String params = "";
Object obj;
try {
cName = item.getStrategy();
params = item.getParams();
// add the specific rate limiter strategy of servlet.
Class<?> c = Class.forName(ADAPTER_PREFIX + cName);
Constructor constructor;
if (c == GlobalPreemptibleAdapter.class || c == QpsRateLimiterAdapter.class
|| c == IPQPSRateLimiterAdapter.class) {
constructor = c.getConstructor(String.class);
obj = constructor.newInstance(params);
container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj);
} else {
constructor = c.getConstructor();
obj = constructor.newInstance(QpsStrategy.DEFAULT_QPS_PARAM);
container.add(KEY_PREFIX_HTTP, name, (IRateLimiter) obj);
}
success = true;
} catch (Exception e) {
this.throwTronError(cName, params, name, e);
}
}
if (!success) {
// if the specific rate limiter strategy of servlet is not defined or fail to add,
// then add a default Strategy.
try {
IRateLimiter rateLimiter = new DefaultBaseQqsAdapter(QpsStrategy.DEFAULT_QPS_PARAM);
container.add(KEY_PREFIX_HTTP, name, rateLimiter);
} catch (Exception e) {
this.throwTronError("DefaultBaseQqsAdapter", QpsStrategy.DEFAULT_QPS_PARAM, name, e);

// If no config for this servlet, fall back to the default adapter.
String cName = (item != null) ? item.getStrategy() : DEFAULT_ADAPTER_NAME;
String params = (item != null) ? item.getParams() : QpsStrategy.DEFAULT_QPS_PARAM;

try {
Class<? extends IRateLimiter> c = ALLOWED_ADAPTERS.get(cName);
if (c == null) {
throw new IllegalArgumentException(
"Unknown rate limiter adapter (not in whitelist): " + cName);
}
IRateLimiter rateLimiter = (IRateLimiter) c.getConstructor(String.class).newInstance(params);
container.add(KEY_PREFIX_HTTP, name, rateLimiter);
} catch (Exception e) {
this.throwTronError(cName, params, name, e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package org.tron.core.services.http;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.lang.reflect.Field;
import java.util.Map;
import org.junit.BeforeClass;
import org.junit.Test;
import org.tron.core.services.ratelimiter.adapter.DefaultBaseQqsAdapter;
import org.tron.core.services.ratelimiter.adapter.GlobalPreemptibleAdapter;
import org.tron.core.services.ratelimiter.adapter.IPQPSRateLimiterAdapter;
import org.tron.core.services.ratelimiter.adapter.IRateLimiter;
import org.tron.core.services.ratelimiter.adapter.QpsRateLimiterAdapter;

/**
* Security test: verifies that RateLimiterServlet uses a strict whitelist
* instead of Class.forName(), preventing arbitrary class loading (RCE)
* via a tampered config file.
*/
public class RateLimiterServletWhitelistTest {

// Derive names from the classes themselves — stays in sync if classes are renamed.
private static final String GLOBAL_PREEMPTIBLE = GlobalPreemptibleAdapter.class.getSimpleName();
private static final String QPS_RATE_LIMITER = QpsRateLimiterAdapter.class.getSimpleName();
private static final String IP_QPS_RATE_LIMITER = IPQPSRateLimiterAdapter.class.getSimpleName();
private static final String DEFAULT_BASE_QPS = DefaultBaseQqsAdapter.class.getSimpleName();

private static Map<String, Class<? extends IRateLimiter>> allowedAdapters;

@SuppressWarnings("unchecked")
@BeforeClass
public static void loadWhitelist() throws Exception {
Field f = RateLimiterServlet.class.getDeclaredField("ALLOWED_ADAPTERS");
f.setAccessible(true);
allowedAdapters = (Map<String, Class<? extends IRateLimiter>>) f.get(null);
}

// Verifies all 4 legitimate adapters are present and map to the correct classes.
@Test
public void testWhitelistContents() {
assertNotNull(allowedAdapters.get(GLOBAL_PREEMPTIBLE));
assertTrue(allowedAdapters.get(GLOBAL_PREEMPTIBLE)
.isAssignableFrom(GlobalPreemptibleAdapter.class));

assertNotNull(allowedAdapters.get(QPS_RATE_LIMITER));
assertTrue(allowedAdapters.get(QPS_RATE_LIMITER)
.isAssignableFrom(QpsRateLimiterAdapter.class));

assertNotNull(allowedAdapters.get(IP_QPS_RATE_LIMITER));
assertTrue(allowedAdapters.get(IP_QPS_RATE_LIMITER)
.isAssignableFrom(IPQPSRateLimiterAdapter.class));

assertNotNull(allowedAdapters.get(DEFAULT_BASE_QPS));
assertTrue(allowedAdapters.get(DEFAULT_BASE_QPS)
.isAssignableFrom(DefaultBaseQqsAdapter.class));
Comment on lines +43 to +58
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find framework -name "RateLimiterServletWhitelistTest.java" -type f

Repository: Sunny6889/java-tron

Length of output: 151


🏁 Script executed:

cat -n framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java

Repository: Sunny6889/java-tron

Length of output: 4556


🏁 Script executed:

# Check if the adapter classes exist
find framework -name "GlobalPreemptibleAdapter.class" -o -name "GlobalPreemptibleAdapter.java" | head -20

Repository: Sunny6889/java-tron

Length of output: 159


🏁 Script executed:

# Search for the adapter class definitions
rg "class GlobalPreemptibleAdapter|class QpsRateLimiterAdapter|class IPQPSRateLimiterAdapter|class DefaultBaseQqsAdapter" --type java

Repository: Sunny6889/java-tron

Length of output: 703


🏁 Script executed:

# Verify the current imports in the test file
head -25 framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java

Repository: Sunny6889/java-tron

Length of output: 1066


Use exact assertions for the whitelist contract.

The isAssignableFrom() checks are too permissive. If the map accidentally stored IRateLimiter.class instead of the specific adapter class, the test would still pass because IRateLimiter.class.isAssignableFrom(GlobalPreemptibleAdapter.class) returns true. Use assertSame() to enforce exact class identity, and use assertEquals() for the size comparison to provide clearer assertion semantics.

🧪 Tighten the assertions
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
 import static org.junit.Assert.assertTrue;
@@
-    assertNotNull(allowedAdapters.get(GLOBAL_PREEMPTIBLE));
-    assertTrue(allowedAdapters.get(GLOBAL_PREEMPTIBLE)
-        .isAssignableFrom(GlobalPreemptibleAdapter.class));
+    assertSame(GlobalPreemptibleAdapter.class, allowedAdapters.get(GLOBAL_PREEMPTIBLE));
@@
-    assertNotNull(allowedAdapters.get(QPS_RATE_LIMITER));
-    assertTrue(allowedAdapters.get(QPS_RATE_LIMITER)
-        .isAssignableFrom(QpsRateLimiterAdapter.class));
+    assertSame(QpsRateLimiterAdapter.class, allowedAdapters.get(QPS_RATE_LIMITER));
@@
-    assertNotNull(allowedAdapters.get(IP_QPS_RATE_LIMITER));
-    assertTrue(allowedAdapters.get(IP_QPS_RATE_LIMITER)
-        .isAssignableFrom(IPQPSRateLimiterAdapter.class));
+    assertSame(IPQPSRateLimiterAdapter.class, allowedAdapters.get(IP_QPS_RATE_LIMITER));
@@
-    assertNotNull(allowedAdapters.get(DEFAULT_BASE_QPS));
-    assertTrue(allowedAdapters.get(DEFAULT_BASE_QPS)
-        .isAssignableFrom(DefaultBaseQqsAdapter.class));
+    assertSame(DefaultBaseQqsAdapter.class, allowedAdapters.get(DEFAULT_BASE_QPS));
@@
-    assertTrue("Whitelist must contain exactly 4 adapters", allowedAdapters.size() == 4);
+    assertEquals("Whitelist must contain exactly 4 adapters", 4, allowedAdapters.size());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@framework/src/test/java/org/tron/core/services/http/RateLimiterServletWhitelistTest.java`
around lines 43 - 58, The testWhitelistContents() assertions are too permissive:
replace the isAssignableFrom() checks on
allowedAdapters.get(GLOBAL_PREEMPTIBLE), get(QPS_RATE_LIMITER),
get(IP_QPS_RATE_LIMITER), and get(DEFAULT_BASE_QPS) with assertSame(...) against
the exact classes GlobalPreemptibleAdapter.class, QpsRateLimiterAdapter.class,
IPQPSRateLimiterAdapter.class, and DefaultBaseQqsAdapter.class respectively, and
add an assertEquals(expectedSize, allowedAdapters.size()) to verify the map size
for the whitelist contract.

}

// Verifies that arbitrary / malicious class names are rejected by the whitelist.
@Test
public void testInvalidClassNameIsRejected() {
assertNull(allowedAdapters.get("com.evil.MaliciousAdapter"));
assertNull(allowedAdapters.get("../../../../evil.Payload"));
assertNull(allowedAdapters.get(Runtime.class.getName()));
assertNull(allowedAdapters.get(ProcessBuilder.class.getName()));
assertNull(allowedAdapters.get(""));
assertNull(allowedAdapters.get(null));
}

// Verifies the whitelist cannot be modified at runtime (unmodifiable map).
@Test(expected = UnsupportedOperationException.class)
public void testWhitelistIsImmutable() {
allowedAdapters.put("Injected", DefaultBaseQqsAdapter.class);
}

// Verifies structural invariants: exact size and all entries implement IRateLimiter.
@Test
public void testWhitelistStructure() {
assertFalse("Whitelist must not be empty", allowedAdapters.isEmpty());
assertTrue("Whitelist must contain exactly 4 adapters", allowedAdapters.size() == 4);
for (Map.Entry<String, Class<? extends IRateLimiter>> entry : allowedAdapters.entrySet()) {
assertTrue(entry.getKey() + " must implement IRateLimiter",
IRateLimiter.class.isAssignableFrom(entry.getValue()));
}
}
}
Loading