From 9a611137b9ccdfe0466b3454459c1ddded41f942 Mon Sep 17 00:00:00 2001 From: maxxedev <5051664+maxxedev@users.noreply.github.com> Date: Sun, 19 Jul 2026 00:06:09 -0700 Subject: [PATCH 1/5] ByteBufferCleaner to check Unsafe access in Java 23+ In Java 23+, ByteBufferCleaner should check for access to deprecated memory-access methods in sun.misc.Unsafe --- .../commons/io/input/ByteBufferCleaner.java | 23 +++++++++- .../io/input/ByteBufferCleanerTest.java | 43 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java index 6debbfb8449..1b1d5e1aefd 100644 --- a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java +++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java @@ -17,6 +17,7 @@ package org.apache.commons.io.input; +import java.lang.management.ManagementFactory; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; @@ -89,7 +90,7 @@ public void clean(final ByteBuffer buffer) throws ReflectiveOperationException { */ static void clean(final ByteBuffer buffer) { try { - if (buffer.isDirect()) { + if (INSTANCE != null && buffer.isDirect()) { Buffers.clearWritable(buffer); INSTANCE.clean(buffer); } @@ -98,7 +99,10 @@ static void clean(final ByteBuffer buffer) { } } - private static Cleaner getCleaner() { + static Cleaner getCleaner() { + if (!unsafeMemoryAccessAllowed()) { + return null; + } try { return new Java8Cleaner(); } catch (final Exception e) { @@ -110,6 +114,21 @@ private static Cleaner getCleaner() { } } + private static boolean unsafeMemoryAccessAllowed() { + final int version; + try { + version = Integer.parseInt(ManagementFactory.getRuntimeMXBean().getSpecVersion()); + } catch (final RuntimeException e) { + return true; + } + if (version < 23) { + return true; + } + // see https://openjdk.org/jeps/471 + return ManagementFactory.getRuntimeMXBean().getInputArguments().stream() + .anyMatch(arg -> arg.equals("--sun-misc-unsafe-memory-access=allow")); + } + /** * Tests if were able to load a suitable cleaner for the current JVM. Attempting to call {@code ByteBufferCleaner#clean(ByteBuffer)} when this method * returns false will result in an exception. diff --git a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java index 4675cd72b10..537d4b71280 100644 --- a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java +++ b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java @@ -16,12 +16,25 @@ */ package org.apache.commons.io.input; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.condition.JRE.JAVA_23; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; import java.nio.ByteBuffer; +import java.util.Arrays; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledForJreRange; +import org.junit.jupiter.api.condition.JRE; +import org.mockito.MockedStatic; +import org.mockito.Mockito; /** * Tests {@code ByteBufferCleaner}. @@ -46,8 +59,38 @@ void testCleanFull() { } @Test + void testCleanNonDirectBuffer() { + assertDoesNotThrow(() -> ByteBufferCleaner.clean(ByteBuffer.allocate(10))); + } + + @Test + @EnabledForJreRange(max = JAVA_23) + void testCleanNullBuffer() { + assertThrows(IllegalStateException.class, () -> ByteBufferCleaner.clean(null)); + } + + @Test + @EnabledForJreRange(max = JAVA_23) void testSupported() { assertTrue(ByteBufferCleaner.isSupported(), "ByteBufferCleaner does not work on this platform, please investigate and fix"); } + @Test + @EnabledForJreRange(min = JAVA_23) + void testUnsupportedByDefaultOnJava23() { + assertNull(ByteBufferCleaner.getCleaner()); + assertFalse(ByteBufferCleaner.isSupported(), "ByteBufferCleaner does not work on this platform, please investigate and fix"); + } + + @Test + @EnabledForJreRange(min = JAVA_23) + void testSupportedIfUnsafeAllowedJava23() { + final RuntimeMXBean mockBean = Mockito.mock(RuntimeMXBean.class); + Mockito.when(mockBean.getSpecVersion()).thenReturn("23"); + Mockito.when(mockBean.getInputArguments()).thenReturn(Arrays.asList("java", "--sun-misc-unsafe-memory-access=allow", "-version")); + try (final MockedStatic managementFactory = Mockito.mockStatic(ManagementFactory.class)) { + managementFactory.when(ManagementFactory::getRuntimeMXBean).thenReturn(mockBean); + assertNotNull(ByteBufferCleaner.getCleaner()); + } + } } From f3b3a5e4cda762fe72ec606aefdecacb30e9e232 Mon Sep 17 00:00:00 2001 From: maxxedev <5051664+maxxedev@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:57:49 -0700 Subject: [PATCH 2/5] ByteBufferCleaner should avoid Unsafe access in Java 23+ --- .../commons/io/input/ByteBufferCleaner.java | 13 +++++-------- .../BufferedFileChannelInputStreamTest.java | 9 +++++++-- .../io/input/ByteBufferCleanerTest.java | 19 ------------------- 3 files changed, 12 insertions(+), 29 deletions(-) diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java index 1b1d5e1aefd..8f4e66e4c8c 100644 --- a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java +++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java @@ -21,6 +21,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; import java.nio.ByteBuffer; +import java.util.List; import org.apache.commons.io.Buffers; @@ -100,7 +101,7 @@ static void clean(final ByteBuffer buffer) { } static Cleaner getCleaner() { - if (!unsafeMemoryAccessAllowed()) { + if (!unsafeMemoryAccessDeprecated()) { return null; } try { @@ -114,19 +115,15 @@ static Cleaner getCleaner() { } } - private static boolean unsafeMemoryAccessAllowed() { + private static boolean unsafeMemoryAccessDeprecated() { final int version; try { - version = Integer.parseInt(ManagementFactory.getRuntimeMXBean().getSpecVersion()); + version = Integer.parseInt(System.getProperty("java.specification.version")); } catch (final RuntimeException e) { return true; } - if (version < 23) { - return true; - } // see https://openjdk.org/jeps/471 - return ManagementFactory.getRuntimeMXBean().getInputArguments().stream() - .anyMatch(arg -> arg.equals("--sun-misc-unsafe-memory-access=allow")); + return version < 23; } /** diff --git a/src/test/java/org/apache/commons/io/input/BufferedFileChannelInputStreamTest.java b/src/test/java/org/apache/commons/io/input/BufferedFileChannelInputStreamTest.java index c03e0fb14a5..d4237047f0a 100644 --- a/src/test/java/org/apache/commons/io/input/BufferedFileChannelInputStreamTest.java +++ b/src/test/java/org/apache/commons/io/input/BufferedFileChannelInputStreamTest.java @@ -17,15 +17,19 @@ package org.apache.commons.io.input; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.InputStream; +import java.lang.management.ManagementFactory; import java.nio.channels.FileChannel; import java.nio.file.StandardOpenOption; +import org.apache.commons.lang3.JavaVersion; +import org.apache.commons.lang3.SystemUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -74,12 +78,13 @@ void testBuilderGet() { */ @Test void testCleanCalledOnlyOnce() throws Exception { + final boolean expectedCleanOnClose = !SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_23); try (BufferedFileChannelInputStream stream = BufferedFileChannelInputStream.builder().setPath(InputPath).get()) { assertFalse(stream.isClean()); stream.close(); - assertTrue(stream.isClean()); + assertEquals(stream.isClean(), expectedCleanOnClose); stream.close(); - assertTrue(stream.isClean()); + assertEquals(stream.isClean(), expectedCleanOnClose); } } diff --git a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java index 537d4b71280..8220d2d492e 100644 --- a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java +++ b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java @@ -18,23 +18,16 @@ import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.condition.JRE.JAVA_23; -import java.lang.management.ManagementFactory; -import java.lang.management.RuntimeMXBean; import java.nio.ByteBuffer; -import java.util.Arrays; import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; -import org.junit.jupiter.api.condition.JRE; -import org.mockito.MockedStatic; -import org.mockito.Mockito; /** * Tests {@code ByteBufferCleaner}. @@ -81,16 +74,4 @@ void testUnsupportedByDefaultOnJava23() { assertNull(ByteBufferCleaner.getCleaner()); assertFalse(ByteBufferCleaner.isSupported(), "ByteBufferCleaner does not work on this platform, please investigate and fix"); } - - @Test - @EnabledForJreRange(min = JAVA_23) - void testSupportedIfUnsafeAllowedJava23() { - final RuntimeMXBean mockBean = Mockito.mock(RuntimeMXBean.class); - Mockito.when(mockBean.getSpecVersion()).thenReturn("23"); - Mockito.when(mockBean.getInputArguments()).thenReturn(Arrays.asList("java", "--sun-misc-unsafe-memory-access=allow", "-version")); - try (final MockedStatic managementFactory = Mockito.mockStatic(ManagementFactory.class)) { - managementFactory.when(ManagementFactory::getRuntimeMXBean).thenReturn(mockBean); - assertNotNull(ByteBufferCleaner.getCleaner()); - } - } } From dda0371dd127d207c9bafa36a627ce086ead6691 Mon Sep 17 00:00:00 2001 From: maxxedev <5051664+maxxedev@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:07:09 -0700 Subject: [PATCH 3/5] fix clearing buffer --- .../commons/io/input/ByteBufferCleaner.java | 6 ++++-- .../io/input/ByteBufferCleanerTest.java | 20 ++++++++++++++++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java index 8f4e66e4c8c..ea2f75dd849 100644 --- a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java +++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java @@ -91,9 +91,11 @@ public void clean(final ByteBuffer buffer) throws ReflectiveOperationException { */ static void clean(final ByteBuffer buffer) { try { - if (INSTANCE != null && buffer.isDirect()) { + if (buffer.isDirect()) { Buffers.clearWritable(buffer); - INSTANCE.clean(buffer); + if (INSTANCE != null) { + INSTANCE.clean(buffer); + } } } catch (final Exception e) { throw new IllegalStateException("Failed to clean direct buffer.", e); diff --git a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java index 8220d2d492e..6cf4af766bb 100644 --- a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java +++ b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java @@ -17,6 +17,7 @@ package org.apache.commons.io.input; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -36,19 +37,22 @@ class ByteBufferCleanerTest { @Test void testCleanEmpty() { - final ByteBuffer buffer = ByteBuffer.allocateDirect(10); + final ByteBuffer buffer = ByteBuffer.allocateDirect(8); // There is no way verify that the buffer has been cleaned up, we are just verifying that // clean() doesn't blow up ByteBufferCleaner.clean(buffer); + verifyCleared(buffer); } @Test void testCleanFull() { - final ByteBuffer buffer = ByteBuffer.allocateDirect(10); - buffer.put(RandomUtils.insecure().randomBytes(10), 0, 10); + final ByteBuffer buffer = ByteBuffer.allocateDirect(8); + buffer.putLong(Long.MAX_VALUE); + verifyUncleared(buffer); // There is no way verify that the buffer has been cleaned up, we are just verifying that // clean() doesn't blow up ByteBufferCleaner.clean(buffer); + verifyCleared(buffer); } @Test @@ -74,4 +78,14 @@ void testUnsupportedByDefaultOnJava23() { assertNull(ByteBufferCleaner.getCleaner()); assertFalse(ByteBufferCleaner.isSupported(), "ByteBufferCleaner does not work on this platform, please investigate and fix"); } + + private void verifyUncleared(final ByteBuffer buffer) { + buffer.flip(); + assertEquals(Long.MAX_VALUE, buffer.getLong()); + buffer.flip(); + } + + private void verifyCleared(final ByteBuffer buffer) { + assertEquals(0, buffer.getLong()); + } } From 66185ad3b072a410fe03824c6137e6e75dc20feb Mon Sep 17 00:00:00 2001 From: maxxedev <5051664+maxxedev@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:09:47 -0700 Subject: [PATCH 4/5] remove unused import --- .../java/org/apache/commons/io/input/ByteBufferCleanerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java index 6cf4af766bb..07f23332a86 100644 --- a/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java +++ b/src/test/java/org/apache/commons/io/input/ByteBufferCleanerTest.java @@ -26,7 +26,6 @@ import java.nio.ByteBuffer; -import org.apache.commons.lang3.RandomUtils; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.EnabledForJreRange; From aa9ae9227ad99e730e860b69862d08c2c19a4425 Mon Sep 17 00:00:00 2001 From: maxxedev <5051664+maxxedev@users.noreply.github.com> Date: Mon, 20 Jul 2026 11:11:35 -0700 Subject: [PATCH 5/5] fix jdk version check --- .../java/org/apache/commons/io/input/ByteBufferCleaner.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java index ea2f75dd849..5bc32c2b926 100644 --- a/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java +++ b/src/main/java/org/apache/commons/io/input/ByteBufferCleaner.java @@ -103,7 +103,7 @@ static void clean(final ByteBuffer buffer) { } static Cleaner getCleaner() { - if (!unsafeMemoryAccessDeprecated()) { + if (unsafeMemoryAccessDeprecated()) { return null; } try { @@ -122,10 +122,10 @@ private static boolean unsafeMemoryAccessDeprecated() { try { version = Integer.parseInt(System.getProperty("java.specification.version")); } catch (final RuntimeException e) { - return true; + return false; } // see https://openjdk.org/jeps/471 - return version < 23; + return version >= 23; } /**