From 554b96aec9d3c585fdba853a285f51a80967bfa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lorenzo=20Dematt=C3=A9?= Date: Thu, 29 May 2025 18:52:10 +0200 Subject: [PATCH] [Entitlements] Add missing NIO async network instrumentation (#128582) This PR adds some additional instrumentation to ensure we capture more cases in which we use async network usage via channels and `select` --- .../bridge/EntitlementChecker.java | 30 ++++++++ .../qa/test/DummyImplementations.java | 71 +++++++++++++++++++ .../qa/test/NetworkAccessCheckActions.java | 15 ++++ .../qa/test/NioChannelsActions.java | 49 +++++++++++++ .../DynamicInstrumentation.java | 48 +++++++++---- .../EntitlementInitialization.java | 6 +- .../api/ElasticsearchEntitlementChecker.java | 70 ++++++++++++++++++ .../plugin-metadata/entitlement-policy.yaml | 2 + 8 files changed, 276 insertions(+), 15 deletions(-) diff --git a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java index e8b17d266154..1fd429e6f9fd 100644 --- a/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java +++ b/libs/entitlement/bridge/src/main/java/org/elasticsearch/entitlement/bridge/EntitlementChecker.java @@ -53,6 +53,8 @@ import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; @@ -588,6 +590,16 @@ public interface EntitlementChecker { * (not instrumentable). */ + void check$java_nio_channels_spi_AbstractSelectableChannel$register( + Class callerClass, + SelectableChannel that, + Selector sel, + int ops, + Object att + ); + + void check$java_nio_channels_SelectableChannel$register(Class callerClass, SelectableChannel that, Selector sel, int ops); + // bind void check$java_nio_channels_AsynchronousServerSocketChannel$bind( @@ -611,6 +623,12 @@ public interface EntitlementChecker { void check$sun_nio_ch_ServerSocketChannelImpl$bind(Class callerClass, ServerSocketChannel that, SocketAddress local, int backlog); + void check$java_nio_channels_SocketChannel$$open(Class callerClass); + + void check$java_nio_channels_SocketChannel$$open(Class callerClass, java.net.ProtocolFamily family); + + void check$java_nio_channels_SocketChannel$$open(Class callerClass, SocketAddress remote); + void check$sun_nio_ch_SocketChannelImpl$bind(Class callerClass, SocketChannel that, SocketAddress local); // connect @@ -658,6 +676,18 @@ public interface EntitlementChecker { // provider methods (dynamic) void checkSelectorProviderInheritedChannel(Class callerClass, SelectorProvider that); + void checkSelectorProviderOpenDatagramChannel(Class callerClass, SelectorProvider that); + + void checkSelectorProviderOpenDatagramChannel(Class callerClass, SelectorProvider that, java.net.ProtocolFamily family); + + void checkSelectorProviderOpenServerSocketChannel(Class callerClass, SelectorProvider that); + + void checkSelectorProviderOpenServerSocketChannel(Class callerClass, SelectorProvider that, java.net.ProtocolFamily family); + + void checkSelectorProviderOpenSocketChannel(Class callerClass, SelectorProvider that); + + void checkSelectorProviderOpenSocketChannel(Class callerClass, SelectorProvider that, java.net.ProtocolFamily family); + /// ///////////////// // // Load native libraries diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/DummyImplementations.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/DummyImplementations.java index 6529d6314727..955d48faeed8 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/DummyImplementations.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/DummyImplementations.java @@ -13,6 +13,7 @@ import jdk.nio.Channels; import org.elasticsearch.core.SuppressForbidden; +import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.DatagramPacket; @@ -41,9 +42,12 @@ import java.nio.channels.Pipe; import java.nio.channels.ReadableByteChannel; import java.nio.channels.SeekableByteChannel; import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.WritableByteChannel; +import java.nio.channels.spi.AbstractSelectableChannel; import java.nio.channels.spi.AbstractSelector; import java.nio.channels.spi.AsynchronousChannelProvider; import java.nio.channels.spi.SelectorProvider; @@ -846,4 +850,71 @@ class DummyImplementations { @Override public void implReleaseChannel(SelectableChannel sc) {} } + + static class DummySelectableChannel extends AbstractSelectableChannel { + protected DummySelectableChannel(SelectorProvider provider) { + super(provider); + } + + @Override + protected void implCloseSelectableChannel() throws IOException { + + } + + @Override + protected void implConfigureBlocking(boolean block) throws IOException { + + } + + @Override + public int validOps() { + return SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT; + } + } + + static class DummySelector extends AbstractSelector { + protected DummySelector(SelectorProvider provider) { + super(provider); + } + + @Override + protected void implCloseSelector() throws IOException { + + } + + @Override + protected SelectionKey register(AbstractSelectableChannel ch, int ops, Object att) { + return null; + } + + @Override + public Set keys() { + return Set.of(); + } + + @Override + public Set selectedKeys() { + return Set.of(); + } + + @Override + public int selectNow() throws IOException { + return 0; + } + + @Override + public int select(long timeout) throws IOException { + return 0; + } + + @Override + public int select() throws IOException { + return 0; + } + + @Override + public Selector wakeup() { + return null; + } + } } diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NetworkAccessCheckActions.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NetworkAccessCheckActions.java index c92551488a77..7b99aab0a19e 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NetworkAccessCheckActions.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NetworkAccessCheckActions.java @@ -25,6 +25,7 @@ import java.net.ResponseCache; import java.net.ServerSocket; import java.net.Socket; import java.net.SocketException; +import java.net.StandardProtocolFamily; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; @@ -203,6 +204,20 @@ class NetworkAccessCheckActions { } } + @EntitlementTest(expectedAccess = PLUGINS) + static void socketChannelOpenProtocol() throws IOException { + SocketChannel.open(StandardProtocolFamily.INET).close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void socketChannelOpenAddress() throws IOException { + try { + SocketChannel.open(new InetSocketAddress(InetAddress.getLoopbackAddress(), 0)).close(); + } catch (SocketException ex) { + // Some sort of SocketException is expected, we are trying to connect to port 0 + } + } + @EntitlementTest(expectedAccess = PLUGINS) static void asynchronousSocketChannelBind() throws IOException { try (var socketChannel = AsynchronousSocketChannel.open()) { diff --git a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NioChannelsActions.java b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NioChannelsActions.java index c8271b843f87..db55d82ce4d2 100644 --- a/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NioChannelsActions.java +++ b/libs/entitlement/qa/entitlement-test-plugin/src/main/java/org/elasticsearch/entitlement/qa/test/NioChannelsActions.java @@ -15,8 +15,11 @@ import org.elasticsearch.entitlement.qa.entitled.EntitledActions; import java.io.FileDescriptor; import java.io.IOException; +import java.net.StandardProtocolFamily; import java.nio.channels.AsynchronousFileChannel; import java.nio.channels.FileChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.spi.SelectorProvider; import java.nio.file.StandardOpenOption; import java.util.Set; @@ -85,4 +88,50 @@ class NioChannelsActions { static void channelsReadWriteSelectableChannel() throws IOException { jdk.nio.Channels.readWriteSelectableChannel(new FileDescriptor(), new DummyImplementations.DummySelectableChannelCloser()).close(); } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectableChannelRegisterConnect() throws IOException { + try (var selectableChannel = new DummyImplementations.DummySelectableChannel(SelectorProvider.provider())) { + selectableChannel.configureBlocking(false); + selectableChannel.register(new DummyImplementations.DummySelector(SelectorProvider.provider()), SelectionKey.OP_CONNECT); + } + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectableChannelRegisterAccept() throws IOException { + try (var selectableChannel = new DummyImplementations.DummySelectableChannel(SelectorProvider.provider())) { + selectableChannel.configureBlocking(false); + selectableChannel.register(new DummyImplementations.DummySelector(SelectorProvider.provider()), SelectionKey.OP_ACCEPT); + } + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenSocketChannel() throws IOException { + SelectorProvider.provider().openSocketChannel().close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenDatagramChannel() throws IOException { + SelectorProvider.provider().openDatagramChannel().close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenServerSocketChannel() throws IOException { + SelectorProvider.provider().openServerSocketChannel().close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenSocketChannelWithProtocol() throws IOException { + SelectorProvider.provider().openSocketChannel(StandardProtocolFamily.INET).close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenDatagramChannelWithProtocol() throws IOException { + SelectorProvider.provider().openDatagramChannel(StandardProtocolFamily.INET).close(); + } + + @EntitlementTest(expectedAccess = PLUGINS) + static void selectorProviderOpenServerSocketChannelWithProtocol() throws IOException { + SelectorProvider.provider().openServerSocketChannel(StandardProtocolFamily.INET).close(); + } } diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java index b8d7ea6cdd8c..b7d92d351884 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/DynamicInstrumentation.java @@ -122,20 +122,7 @@ class DynamicInstrumentation { private static Map getMethodsToInstrument(Class checkerInterface) throws ClassNotFoundException, NoSuchMethodException { Map checkMethods = new HashMap<>(INSTRUMENTATION_SERVICE.lookupMethods(checkerInterface)); - Stream.of( - fileSystemProviderChecks(), - fileStoreChecks(), - pathChecks(), - Stream.of( - INSTRUMENTATION_SERVICE.lookupImplementationMethod( - SelectorProvider.class, - "inheritedChannel", - SelectorProvider.provider().getClass(), - EntitlementChecker.class, - "checkSelectorProviderInheritedChannel" - ) - ) - ) + Stream.of(fileSystemProviderChecks(), fileStoreChecks(), pathChecks(), selectorProviderChecks()) .flatMap(Function.identity()) .forEach(instrumentation -> checkMethods.put(instrumentation.targetMethod(), instrumentation.checkMethod())); @@ -257,6 +244,39 @@ class DynamicInstrumentation { }); } + private static Stream selectorProviderChecks() { + var selectorProviderClass = SelectorProvider.provider().getClass(); + + var instrumentation = new InstrumentationInfoFactory() { + @Override + public InstrumentationService.InstrumentationInfo of(String methodName, Class... parameterTypes) + throws ClassNotFoundException, NoSuchMethodException { + return INSTRUMENTATION_SERVICE.lookupImplementationMethod( + SelectorProvider.class, + methodName, + selectorProviderClass, + EntitlementChecker.class, + "checkSelectorProvider" + Character.toUpperCase(methodName.charAt(0)) + methodName.substring(1), + parameterTypes + ); + } + }; + + try { + return Stream.of( + instrumentation.of("inheritedChannel"), + instrumentation.of("openDatagramChannel"), + instrumentation.of("openDatagramChannel", java.net.ProtocolFamily.class), + instrumentation.of("openServerSocketChannel"), + instrumentation.of("openServerSocketChannel", java.net.ProtocolFamily.class), + instrumentation.of("openSocketChannel"), + instrumentation.of("openSocketChannel", java.net.ProtocolFamily.class) + ); + } catch (NoSuchMethodException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + private static Class[] findClassesToRetransform(Class[] loadedClasses, Set classesToTransform) { List> retransform = new ArrayList<>(); for (Class loadedClass : loadedClasses) { diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java index 3a1345ba63c7..704bae6d046e 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/initialization/EntitlementInitialization.java @@ -85,7 +85,11 @@ public class EntitlementInitialization { * transformed and undergo verification. In order to avoid circularity errors as much as possible, we force a partial order. */ private static void ensureClassesSensitiveToVerificationAreInitialized() { - var classesToInitialize = Set.of("sun.net.www.protocol.http.HttpURLConnection"); + var classesToInitialize = Set.of( + "sun.net.www.protocol.http.HttpURLConnection", + "sun.nio.ch.DatagramChannelImpl", + "sun.nio.ch.ServerSocketChannelImpl" + ); for (String className : classesToInitialize) { try { Class.forName(className); diff --git a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java index 78c54dc438e9..d7aae1b9c652 100644 --- a/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java +++ b/libs/entitlement/src/main/java/org/elasticsearch/entitlement/runtime/api/ElasticsearchEntitlementChecker.java @@ -40,6 +40,7 @@ import java.net.InetSocketAddress; import java.net.JarURLConnection; import java.net.MulticastSocket; import java.net.NetworkInterface; +import java.net.ProtocolFamily; import java.net.Proxy; import java.net.ProxySelector; import java.net.ResponseCache; @@ -59,6 +60,9 @@ import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; @@ -1146,6 +1150,27 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker { } } + @Override + public void check$java_nio_channels_spi_AbstractSelectableChannel$register( + Class callerClass, + SelectableChannel that, + Selector sel, + int ops, + Object att + ) { + check$java_nio_channels_SelectableChannel$register(callerClass, that, sel, ops); + } + + @Override + public void check$java_nio_channels_SelectableChannel$register(Class callerClass, SelectableChannel that, Selector sel, int ops) { + if ((ops & SelectionKey.OP_CONNECT) != 0) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + if ((ops & SelectionKey.OP_ACCEPT) != 0) { + policyChecker.checkInboundNetworkAccess(callerClass); + } + } + @Override public void check$java_nio_channels_AsynchronousServerSocketChannel$bind( Class callerClass, @@ -1194,6 +1219,21 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker { policyChecker.checkInboundNetworkAccess(callerClass); } + @Override + public void check$java_nio_channels_SocketChannel$$open(Class callerClass) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + + @Override + public void check$java_nio_channels_SocketChannel$$open(Class callerClass, ProtocolFamily family) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + + @Override + public void check$java_nio_channels_SocketChannel$$open(Class callerClass, SocketAddress remote) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + @Override public void check$sun_nio_ch_SocketChannelImpl$bind(Class callerClass, SocketChannel that, SocketAddress local) { policyChecker.checkOutboundNetworkAccess(callerClass); @@ -1283,6 +1323,36 @@ public class ElasticsearchEntitlementChecker implements EntitlementChecker { policyChecker.checkChangeNetworkHandling(callerClass); } + @Override + public void checkSelectorProviderOpenDatagramChannel(Class callerClass, SelectorProvider that) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + + @Override + public void checkSelectorProviderOpenDatagramChannel(Class callerClass, SelectorProvider that, ProtocolFamily family) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + + @Override + public void checkSelectorProviderOpenServerSocketChannel(Class callerClass, SelectorProvider that) { + policyChecker.checkInboundNetworkAccess(callerClass); + } + + @Override + public void checkSelectorProviderOpenServerSocketChannel(Class callerClass, SelectorProvider that, ProtocolFamily family) { + policyChecker.checkInboundNetworkAccess(callerClass); + } + + @Override + public void checkSelectorProviderOpenSocketChannel(Class callerClass, SelectorProvider that) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + + @Override + public void checkSelectorProviderOpenSocketChannel(Class callerClass, SelectorProvider that, ProtocolFamily family) { + policyChecker.checkOutboundNetworkAccess(callerClass); + } + @Override public void check$java_lang_Runtime$load(Class callerClass, Runtime that, String filename) { policyChecker.checkFileRead(callerClass, Path.of(filename)); diff --git a/modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml b/modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml index 5e0cbfaabe34..bbc4c252b7df 100644 --- a/modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml +++ b/modules/repository-azure/src/main/plugin-metadata/entitlement-policy.yaml @@ -8,6 +8,8 @@ io.netty.common: mode: "read" - path: "/proc/sys/net/core/somaxconn" mode: read +io.netty.transport: + - outbound_network com.azure.identity: - outbound_network - files: