Repository: netty/netty
Issue: Netty on GraalVM with substitutions not fully working as expected

### Expected behavior
The `NetUtilSubstitutions` implement a set InetAddress, Inet4Address and Inet6Address dummy implementation.

### Actual behavior
See logging with exceptions below. I expect no exceptions related to netty.

### Steps to reproduce
Have a running docker engine since it uses the buildpacks from spring.
```
git clone git@github.com:ArjanSchouten/spring-native-netty-reproduce.git
cd spring-native-netty-reproduce
./gradlew bootBuildImage
```
### Minimal yet complete reproducer code (or URL to code)
See steps to reproduce.

### Netty version
4.1.54.Final

### JVM version (e.g. `java -version`)
11.0

### OS version (e.g. `uname -a`)
Darwin computername 19.6.0 Darwin Kernel Version 19.6.0: Mon Aug 31 22:12:52 PDT 2020; root:xnu-6153.141.2~1/RELEASE_X86_64 x86_64

In PR https://github.com/netty/netty/pull/10630 the NetUtilSubstitutions where added. I try to use Netty with https://github.com/spring-projects-experimental/spring-graalvm-native and it fails with:
```
[creator]     22:25:44.836 [ForkJoinPool-2-worker-15] DEBUG io.netty.buffer.AdvancedLeakAwareByteBuf - -Dio.netty.leakDetection.acquireAndReleaseOnly: false
    [creator]     Warning: class initialization of class io.netty.util.internal.logging.Log4JLogger failed with exception java.lang.NoClassDefFoundError: org/apache/log4j/Priority. This class will be initialized at run time because option --allow-incomplete-classpath is used for image building. Use the option --initialize-at-run-time=io.netty.util.internal.logging.Log4JLogger to explicitly request delayed initialization of this class.
    [creator]     22:25:49.486 [ForkJoinPool-2-worker-3] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.noKeySetOptimization: false
    [creator]     22:25:49.486 [ForkJoinPool-2-worker-3] DEBUG io.netty.channel.nio.NioEventLoop - -Dio.netty.selectorAutoRebuildThreshold: 512
    [creator]     22:25:49.605 [ForkJoinPool-2-worker-7] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxCapacityPerThread: 4096
    [creator]     22:25:49.606 [ForkJoinPool-2-worker-7] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.maxSharedCapacityFactor: 2
    [creator]     22:25:49.606 [ForkJoinPool-2-worker-7] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.linkCapacity: 16
    [creator]     22:25:49.606 [ForkJoinPool-2-worker-7] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.ratio: 8
    [creator]     22:25:49.607 [ForkJoinPool-2-worker-7] DEBUG io.netty.util.Recycler - -Dio.netty.recycler.delayedQueue.ratio: 8
    [creator]     22:25:54.431 [ForkJoinPool-2-worker-11] DEBUG io.netty.handler.codec.compression.ZlibCodecFactory - -Dio.netty.noJdkZlibDecoder: false
    [creator]     22:25:54.432 [ForkJoinPool-2-worker-11] DEBUG io.netty.handler.codec.compression.ZlibCodecFactory - -Dio.netty.noJdkZlibEncoder: false
    [creator]     22:25:55.835 [ForkJoinPool-2-worker-9] DEBUG io.netty.handler.ssl.OpenSsl - netty-tcnative not in the classpath; OpenSslEngine will be unavailable.
    [creator]     22:25:57.117 [ForkJoinPool-2-worker-7] DEBUG io.netty.channel.MultithreadEventLoopGroup - -Dio.netty.eventLoopThreads: 16
    [creator]     22:25:58.173 [ForkJoinPool-2-worker-15] DEBUG io.netty.util.internal.PlatformDependent - org.jctools-core.MpscChunkedArrayQueue: available
    [creator]     [/layers/paketo-buildpacks_spring-boot-native-image/native-image/com.example.springnative.Application:169]     analysis:  42,689.86 ms,  2.63 GB
    [creator]     Fatal error:com.oracle.graal.pointsto.util.AnalysisError$ParsingError: Error encountered while parsing io.netty.util.NetUtil.<clinit>()
    [creator]     Parsing context: <no parsing context available>
    [creator]
    [creator]           at com.oracle.graal.pointsto.util.AnalysisError.parsingError(AnalysisError.java:138)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlow.doParse(MethodTypeFlow.java:327)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlow.ensureParsed(MethodTypeFlow.java:302)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlow.addContext(MethodTypeFlow.java:103)
    [creator]           at com.oracle.graal.pointsto.BigBang$1.run(BigBang.java:428)
    [creator]           at com.oracle.graal.pointsto.util.CompletionExecutor.lambda$execute$0(CompletionExecutor.java:173)
    [creator]           at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1426)
    [creator]           at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:290)
    [creator]           at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1020)
    [creator]           at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1656)
    [creator]           at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1594)
    [creator]           at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:183)
    [creator]     Caused by: org.graalvm.compiler.java.BytecodeParser$BytecodeParserError: com.oracle.svm.core.util.VMError$HostedError: Error in @InjectAccessors handling of field io.netty.util.NetUtil.LOCALHOST4, accessors class io.netty.util.NetUtilSubstitutions$NetUtilLocalhost4Accessor: found no method named set or setLOCALHOST4
    [creator]           at parsing io.netty.util.NetUtil.<clinit>(NetUtil.java:139)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.throwParserError(BytecodeParser.java:2580)
    [creator]           at com.oracle.svm.hosted.phases.SharedGraphBuilderPhase$SharedBytecodeParser.throwParserError(SharedGraphBuilderPhase.java:100)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.iterateBytecodesForBlock(BytecodeParser.java:3418)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.processBlock(BytecodeParser.java:3220)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.build(BytecodeParser.java:1090)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.buildRootMethod(BytecodeParser.java:984)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.GraphBuilderPhase$Instance.run(GraphBuilderPhase.java:84)
    [creator]           at com.oracle.svm.hosted.phases.SharedGraphBuilderPhase.run(SharedGraphBuilderPhase.java:74)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.run(Phase.java:49)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.phases.BasePhase.apply(BasePhase.java:214)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.apply(Phase.java:42)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.phases.Phase.apply(Phase.java:38)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.parse(MethodTypeFlowBuilder.java:223)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlowBuilder.apply(MethodTypeFlowBuilder.java:357)
    [creator]           at com.oracle.graal.pointsto.flow.MethodTypeFlow.doParse(MethodTypeFlow.java:313)
    [creator]           ... 10 more
    [creator]     Caused by: com.oracle.svm.core.util.VMError$HostedError: Error in @InjectAccessors handling of field io.netty.util.NetUtil.LOCALHOST4, accessors class io.netty.util.NetUtilSubstitutions$NetUtilLocalhost4Accessor: found no method named set or setLOCALHOST4
    [creator]           at com.oracle.svm.core.util.VMError.shouldNotReachHere(VMError.java:68)
    [creator]           at com.oracle.svm.hosted.phases.InjectedAccessorsPlugin.error(InjectedAccessorsPlugin.java:152)
    [creator]           at com.oracle.svm.hosted.phases.InjectedAccessorsPlugin.handleField(InjectedAccessorsPlugin.java:89)
    [creator]           at com.oracle.svm.hosted.phases.InjectedAccessorsPlugin.handleStoreStaticField(InjectedAccessorsPlugin.java:62)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.genPutStatic(BytecodeParser.java:4970)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.genPutStatic(BytecodeParser.java:4941)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.processBytecode(BytecodeParser.java:5335)
    [creator]           at jdk.internal.vm.compiler/org.graalvm.compiler.java.BytecodeParser.iterateBytecodesForBlock(BytecodeParser.java:3413)
    [creator]           ... 22 more
    [creator]     Error: Image build request failed with exit status 1
```

I tried to add a substitute myself but it looks like there can only be one substitute. It looks like the author of the stackoverflow answer posted the PR: https://stackoverflow.com/questions/63328298/how-do-you-debug-a-no-instances-of-are-allowed-in-the-image-heap-when-buil (which is great!!)

I think that a set implementation like the stackoverflow answer from @rpuch like in part 4 of the answer will solve my issue?! @rpuch can you validate that my assumption is correct?

_edit: according to the documentation: https://javadoc.io/doc/org.graalvm.nativeimage/svm/latest/com/oracle/svm/core/annotate/InjectAccessors.html_

> Inject accessors methods for the field denoted using a Alias annotation. All loads and stores to the original field are redirected to accessor methods located in the class provided in the InjectAccessors.value() property. The class must implement the marker interface InjectAccessors. The accessor methods are static methods in that class, named either get / set or getFoo / setFoo for a field name foo. Depending on the kind of accessor (get / set for a static / non-static field), the accessor must have 0, 1, or 2 parameters. If the field is non-static, the first method parameter is the accessed object. The type of the parameter must be the class that declared the field. The null check on the object is performed before the accessor is called, in the same way as the null check for a regular field access. For get-accessors, the return type of the method must be the type of the field. For set-accessors, the last method parameter must be the type of the field and denotes the value stored to the field. **If no set-accessor is provided, stores to the field lead to a fatal error during image generation**. If no get-accessor is provided, loads of the field lead to a fatal error during image generation. The injected accessors must not access the original field. Since all field accesses use the accessors, that would lead to a recursive call of the accessors. Instead, data must be stored in either a new static field, or an injected instance field.

According to the documentation, is the setLOCALHOST4 accessed illegally or do we have to implement the static set method as an empty method?

(I believe this will be a blocker for https://github.com/spring-projects-experimental/spring-graalvm-native/issues/184 as well)

Issue comments:
Hi @ArjanSchouten.

I tried to build your project and reproduced the problem. (Stangely enough, the build downloads GraalVM 20.2.0 on each run, aren't you aware of a way to make it cache the downloaded file? The download is pretty slow).

To be honest, I'm not an expert in GraalVM (nor its Native Builder feature). Anyway, it seems strange that any field store could be possible because `NetUtil.LOCALHOST4` field is final. Also, the build of `netty-testsuite-native-image-client` does not trigger this problem, even if the field is referenced directly from the main class.

When I checked out the current 4.1 branch of Netty (the substitutions are already there), added the 3 dummy setters, built the patched Netty version and changed `netty-common` version to the newly-built version, the error you demonstrate has gone, but the build still failed with the following error:

```
    [creator]     [/layers/paketo-buildpacks_spring-boot-native-image/native-image/com.example.springnative.Application:153]     analysis:  39,857.64 ms,  3.71 GB
    [creator]     Error: Classes that should be initialized at run time got initialized during image building:
    [creator]      io.netty.channel.socket.InternetProtocolFamily the class was requested to be initialized at run time (from feature org.springframework.graalvm.support.InitializationHandler.lambda$registerInitializationDescriptor$4). To see why io.netty.channel.socket.InternetProtocolFamily got initialized use -H:+TraceClassInitialization
    [creator]     
    [creator]     Error: Use -H:+ReportExceptionStackTraces to print stacktrace of underlying exception
    [creator]     Error: Image build request failed with exit status 1
    [creator]     unable to invoke layer creator
    [creator]     unable to contribute native-image layer
    [creator]     error running build
    [creator]     exit status 1
    [creator]     ERROR: failed to build: exit status 1

> Task :bootBuildImage FAILED
```

Also, when I downgrade `netty-common` version to the previous release `4.1.53` (which does not contain the substitutions) with

```
    implementation 'io.netty:netty-common:4.1.53.Final'
```

then the build fails with the same message as it does with a patched `4.1.54`.

It is probable that the suggested addition of setters could help fix the version with substitutions, but it would be great to get rid of all other build errors (like the highlighted problem with `InternetProtocolFamily`) because for now such a fix looks like a blind shot. I still don't understand why a setter should be needed for a final field.
#10630 was actually authored to make the changes described in the stackoverflow post obsolete. The solutions differ n details, because access to Netty code allowed to avoid using set-and-store strategy and just use setter. So, ideally, having #10630 included in Netty, one doesn't need any modifications suggested in the stackoverflow post.
BTW, why do you believe that https://github.com/spring-projects-experimental/spring-graalvm-native/issues/184 has any relation to this one?
Ok, I think I see what causes the problem.

The 3 fields (LOCALHOST4, LOCALHOST6, LOCALHOST) are only written in `NetUtil` static initialization block.

When `NetUtil` gets initialized at build time, the initialization block does not get initialized at runtime, so the initialization block does not run (at runtime), and the field is only accessed with reads. Getters on substitutions are enough.

But when `NetUtil` gets initialized at run-time, the initialization block gets executed and tries to write to the field. There is a substitution, so a setter is looked up, and there it does not exist. BOOM.

This is embarassing, but with #10630 I broke Netty for the cases when `NetUtil` is initialized at run-time.

I'm preparing a fix, thank you for letting me know @ArjanSchouten 
@ArjanSchouten could you please check Netty built from #10799 to see whether it fixes the problem for you?
> This is embarassing, but with #10630 I broke Netty for the cases when NetUtil is initialized at run-time.

FYI this change broke native Netty support in Spring Boot 2.4.0. I will adapt our support accordingly.

> @ArjanSchouten could you please check Netty built from #10799 to see whether it fixes the problem for you?

@rpuch Thanks for working on a fix, but looking at the changes in #10630, I understand the problem you are trying to solve but maybe we should take a step back on why those changes are needed.

@normanmaurer Just to give more context, as you know GraalVM native started its life with build time initialization by default, but since is clearly moving towards runtime initialization by default to favor compatibility, avoid to have 2 distinct behaviors of Java to support in the ecosystem, and because build time init is really required in just a few places where it is critical for performances or footprint:
 - [GraalVM native 19.0.0](https://www.graalvm.org/docs/release-notes/19_0/#1900) switched to runtime init by default
 - [GraalVM native 20.2.0](https://www.graalvm.org/release-notes/20_2/) moved most of the JDK to runtime init
 - I think GraalVM and Project Leyden teams are trying to limit the differences of behavior in order to avoid to have 2 different behaviors of Java, in order to avoid a maintainability nightmare well illustrated by those changes
 - I think @vjovanov from GraalVM team is about to provide a PR to restore by default runtime init by default in Netty.

In a nustshell, my proposal is to:
 - Rollback #10630 changes
 - Merge @vjovanov upcoming PR in order to restore defaults consistent with GraalVM one, with more consistency between JVM and native
 - Evolve Netty native support with a goal of limiting substitutions, favor consistency about JVM and native flavors, make Netty consistent with other libraries that are by default initialized at runtime in GraalVM native
 - Use build time init if needed on the few classes where there are needed to perform build time code removal or when critical for performances
 - Document with @rpuch and others interested how to use Netty with build time init and sync with them in order to allow them to adapt to those changes
The GraalVM Native Image team is preparing a PR that will revert build-time initialization for Netty. By aggregating all the issues and request that we get due to this feature, it is clear to us that the default setting should be run time (the same as GraalVM): the maintenance overhead far outweighs the startup performance benefits. 

Projects that want to keep build-time initialization for `netty` can always take the existing config and use it in their projects, or make a separate repo. Of course, all issues related to build-time initialization from GraalVM shall be redirected to those project or that repo. 
Thanks all. It's working fine since I use the new version of spring-graalvm-native.
I believe spring graalvm native implemented some workarounds. Something still needs to be fixed I guess.
Things are either tracked by @vjovanov or @sdeleuze and https://github.com/netty/netty/pull/10799 is also still open. If you feel that this is the issue that keeps track of the things that still need to be done to get rid of the workarounds I can reopen the issue (or someone else can do).

Pull request review comments:

Patch that resolved the issue:
diff --git a/common/src/main/java/io/netty/util/NetUtilSubstitutions.java b/common/src/main/java/io/netty/util/NetUtilSubstitutions.java
index 9d9740392ca..7894037e4e9 100644
--- a/common/src/main/java/io/netty/util/NetUtilSubstitutions.java
+++ b/common/src/main/java/io/netty/util/NetUtilSubstitutions.java
@@ -45,6 +45,10 @@ static Inet4Address get() {
             // using https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
             return NetUtilLocalhost4LazyHolder.LOCALHOST4;
         }
+
+        static void set(Inet4Address ignored) {
+            // a no-op setter to avoid exceptions when NetUtil is initialized at run-time
+        }
     }
 
     private static final class NetUtilLocalhost4LazyHolder {
@@ -56,6 +60,10 @@ static Inet6Address get() {
             // using https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
             return NetUtilLocalhost6LazyHolder.LOCALHOST6;
         }
+
+        static void set(Inet6Address ignored) {
+            // a no-op setter to avoid exceptions when NetUtil is initialized at run-time
+        }
     }
 
     private static final class NetUtilLocalhost6LazyHolder {
@@ -67,6 +75,10 @@ static InetAddress get() {
             // using https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom
             return NetUtilLocalhostLazyHolder.LOCALHOST;
         }
+
+        static void set(InetAddress ignored) {
+            // a no-op setter to avoid exceptions when NetUtil is initialized at run-time
+        }
     }
 
     private static final class NetUtilLocalhostLazyHolder {
diff --git a/pom.xml b/pom.xml
index 55154158cb5..feb2d7a8bde 100644
--- a/pom.xml
+++ b/pom.xml
@@ -444,6 +444,7 @@
     <module>testsuite-shading</module>
     <module>testsuite-native-image</module>
     <module>testsuite-native-image-client</module>
+    <module>testsuite-native-image-client-runtime-init</module>
     <module>transport-blockhound-tests</module>
     <module>microbench</module>
     <module>bom</module>


Test patch:
diff --git a/testsuite-native-image-client-runtime-init/pom.xml b/testsuite-native-image-client-runtime-init/pom.xml
new file mode 100644
index 00000000000..eb22b7aa33d
--- /dev/null
+++ b/testsuite-native-image-client-runtime-init/pom.xml
@@ -0,0 +1,100 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Copyright 2020 The Netty Project
+  ~
+  ~ The Netty Project licenses this file to you under the Apache License,
+  ~ version 2.0 (the "License"); you may not use this file except in compliance
+  ~ with the License. You may obtain a copy of the License at:
+  ~
+  ~   https://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+  ~ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+  ~ License for the specific language governing permissions and limitations
+  ~ under the License.
+  -->
+<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 https://maven.apache.org/maven-v4_0_0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>io.netty</groupId>
+    <artifactId>netty-parent</artifactId>
+    <version>4.1.55.Final-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>netty-testsuite-native-image-client-runtime-init</artifactId>
+  <packaging>jar</packaging>
+
+  <name>Netty/Testsuite/NativeImage/ClientRuntimeInit</name>
+
+  <properties>
+    <skipJapicmp>true</skipJapicmp>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>${project.groupId}</groupId>
+      <artifactId>netty-common</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>skipTests</id>
+      <activation>
+        <property>
+          <name>skipTests</name>
+        </property>
+      </activation>
+      <properties>
+        <skipNativeImageTestsuite>true</skipNativeImageTestsuite>
+      </properties>
+    </profile>
+  </profiles>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>com.oracle.substratevm</groupId>
+        <artifactId>native-image-maven-plugin</artifactId>
+        <version>${graalvm.version}</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>native-image</goal>
+            </goals>
+            <phase>package</phase>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>${skipNativeImageTestsuite}</skip>
+          <imageName>${project.artifactId}</imageName>
+          <mainClass>io.netty.testsuite.svm.client.NativeClientWithNettyInitAtRuntime</mainClass>
+          <buildArgs>--report-unsupported-elements-at-runtime --allow-incomplete-classpath --no-fallback --initialize-at-run-time=io.netty.util.NetUtil</buildArgs>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>exec-maven-plugin</artifactId>
+        <version>1.6.0</version>
+        <executions>
+          <!-- This will do a whitesmoke test: if the substitutions are missing the binary will fail to run -->
+          <!-- If the metadata is missing the build above will fail -->
+          <execution>
+            <id>verify-native-image</id>
+            <phase>verify</phase>
+            <goals>
+              <goal>exec</goal>
+            </goals>
+          </execution>
+        </executions>
+        <configuration>
+          <skip>${skipNativeImageTestsuite}</skip>
+          <executable>${project.build.directory}/${project.artifactId}</executable>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/NativeClientWithNettyInitAtRuntime.java b/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/NativeClientWithNettyInitAtRuntime.java
new file mode 100644
index 00000000000..6c4aa22ba80
--- /dev/null
+++ b/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/NativeClientWithNettyInitAtRuntime.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+package io.netty.testsuite.svm.client;
+
+import io.netty.util.NetUtil;
+
+/**
+ * A client that triggers runtime initialization of NetUtil when
+ * built to a native image.
+ */
+public final class NativeClientWithNettyInitAtRuntime {
+    /**
+     * Main entry point (not instantiable)
+     */
+    private NativeClientWithNettyInitAtRuntime() {
+    }
+
+    public static void main(String[] args) {
+        System.out.println(NetUtil.LOCALHOST4);
+        System.out.println(NetUtil.LOCALHOST6);
+        System.out.println(NetUtil.LOCALHOST);
+    }
+}
diff --git a/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/package-info.java b/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/package-info.java
new file mode 100644
index 00000000000..c5d8a74b31e
--- /dev/null
+++ b/testsuite-native-image-client-runtime-init/src/main/java/io/netty/testsuite/svm/client/package-info.java
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2020 The Netty Project
+ *
+ * The Netty Project licenses this file to you under the Apache License,
+ * version 2.0 (the "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at:
+ *
+ *   https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * A client that triggers runtime initialization of NetUtil when
+ * built to a native image.
+ */
+package io.netty.testsuite.svm.client;
diff --git a/testsuite-native-image-client/src/main/java/io/netty/testsuite/svm/client/package-info.java b/testsuite-native-image-client/src/main/java/io/netty/testsuite/svm/client/package-info.java
index 0426d41985c..7b575e68359 100644
--- a/testsuite-native-image-client/src/main/java/io/netty/testsuite/svm/client/package-info.java
+++ b/testsuite-native-image-client/src/main/java/io/netty/testsuite/svm/client/package-info.java
@@ -15,6 +15,6 @@
  */
 
 /**
- * A hello world server that should be compiled to native.
+ * A client that uses netty-dns and gets compiled to a native image.
  */
 package io.netty.testsuite.svm.client;