You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by vy...@apache.org on 2024/09/17 19:13:15 UTC

(logging-log4j2) branch 2.x updated: Google OSS-Fuzz integration (#2949)

This is an automated email from the ASF dual-hosted git repository.

vy pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new caa0f1dee5 Google OSS-Fuzz integration (#2949)
caa0f1dee5 is described below

commit caa0f1dee5fb337fb09f56b5b670ee899eb95766
Author: Volkan Yazıcı <vo...@yazi.ci>
AuthorDate: Tue Sep 17 21:13:10 2024 +0200

    Google OSS-Fuzz integration (#2949)
    
    Co-authored-by: Piotr P. Karwasz <pi...@karwasz.org>
---
 .mvn/wrapper/maven-wrapper.properties              |   4 +-
 FUZZING.adoc                                       | 106 +++++
 log4j-core-fuzz-test/pom.xml                       |  51 +++
 .../log4j/core/fuzz/PatternLayoutFuzzer.java       |  43 ++
 log4j-core-its/pom.xml                             |  10 +-
 log4j-fuzz-test/.log4j-plugin-processing-activator |   1 +
 log4j-fuzz-test/pom.xml                            |  77 ++++
 .../logging/log4j/fuzz/EncodingAppender.java       |  62 +++
 .../org/apache/logging/log4j/fuzz/FuzzingUtil.java | 194 +++++++++
 .../logging/log4j/fuzz/JsonEncodingAppender.java   |  66 +++
 .../org/apache/logging/log4j/fuzz/JsonUtil.java    |  47 +++
 .../apache/logging/log4j/fuzz/package-info.java    |  20 +
 .../.log4j-plugin-processing-activator             |   1 +
 log4j-layout-template-json-fuzz-test/pom.xml       |  67 +++
 .../json/fuzz/JsonTemplateLayoutCodecFuzzer.java   |  82 ++++
 .../json/fuzz/JsonTemplateLayoutFuzzer.java        |  42 ++
 log4j-layout-template-json-test/pom.xml            |   3 +-
 .../layout/template/json/util/JsonWriterTest.java  |  96 -----
 .../layout/template/json/util/JsonWriter.java      |   7 +-
 log4j-osgi-test/pom.xml                            |   8 +-
 log4j-parent/pom.xml                               |  76 +++-
 log4j-perf-test/README.adoc                        |  30 ++
 log4j-perf-test/pom.xml                            |  58 +--
 log4j-slf4j2-impl-fuzz-test/pom.xml                |  56 +++
 .../logging/slf4j/fuzz/Slf4jLoggerFacade.java      |  65 +++
 ...jToLog4jBridgeWithJsonTemplateLayoutFuzzer.java |  38 ++
 .../Slf4jToLog4jBridgeWithPatternLayoutFuzzer.java |  38 ++
 ...4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.xml |  36 ++
 .../Slf4jToLog4jBridgeWithPatternLayoutFuzzer.xml  |  36 ++
 mvnw                                               | 447 +++++++++------------
 mvnw.cmd                                           | 303 ++++++--------
 oss-fuzz-build.sh                                  | 217 ++++++++++
 pom.xml                                            |  10 +-
 src/changelog/.2.x.x/2949_add_fuzzing.xml          |   8 +
 .../.2.x.x/fix_JsonWriter_surrogate_handling.xml   |   7 +
 .../antora/modules/ROOT/pages/development.adoc     |   7 +-
 36 files changed, 1813 insertions(+), 606 deletions(-)

diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties
index cc0f6f262b..c76af40b2f 100644
--- a/.mvn/wrapper/maven-wrapper.properties
+++ b/.mvn/wrapper/maven-wrapper.properties
@@ -15,8 +15,6 @@
 # specific language governing permissions and limitations
 # under the License.
 distributionSha256Sum=8351955a9acf2f83c136c4eee0f6db894ab6265fdbe0a94b32a380307dbaa3e1
-distributionType=script
+distributionType=only-script
 distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.8/apache-maven-3.9.8-bin.zip
-wrapperSha256Sum=3d8f20ce6103913be8b52aef6d994e0c54705fb527324ceb9b835b338739c7a8
-wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar
 wrapperVersion=3.3.2
diff --git a/FUZZING.adoc b/FUZZING.adoc
new file mode 100644
index 0000000000..838a89419a
--- /dev/null
+++ b/FUZZING.adoc
@@ -0,0 +1,106 @@
+////
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF 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
+
+         http://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.
+////
+
+Log4j contains fuzz tests implemented using https://github.com/CodeIntelligenceTesting/jazzer[Jazzer]footnote:[
+We are aware that https://github.com/google/oss-fuzz/discussions/12195[Jazzer is discontinued].
+Yet it is still the only mature fuzzing framework in Java and https://google.github.io/oss-fuzz/getting-started/new-project-guide/jvm-lang/#jazzer[the recommended library by OSS-Fuzz].].
+These tests are located in `-fuzz-test` prefixed modules; `log4j-core-fuzz-test`, `log4j-layout-template-json-fuzz-test`, etc.
+
+[#oss-fuzz]
+== Google OSS-Fuzz
+
+https://github.com/google/oss-fuzz[OSS-Fuzz] is a Google service that continuously runs fuzz tests of critical F/OSS projects on a beefy cluster and reports its findings (bugs, vulnerabilities, etc.) privately to project maintainers.
+Log4j provides OSS-Fuzz integration with following helpers:
+
+- https://github.com/google/oss-fuzz/tree/master/projects/log4j2/Dockerfile[Dockerfile] to create a container image for running tests
+- link:oss-fuzz-build.sh[`oss-fuzz-build.sh`] to generate fuzz test runner scripts along with all necessary dependencies
+
+[#faq]
+== F.A.Q.
+
+Below we will try to answer some frequently asked questions.
+
+[#running]
+=== How can I run fuzz tests locally?
+
+. Clone the OSS-Fuzz repository:
++
+[source,bash]
+----
+git clone --depth 1 https://github.com/google/oss-fuzz google-oss-fuzz && cd $_
+----
+
+. Build the container image:
++
+[source,bash]
+----
+python infra/helper.py build_image log4j2
+----
+
+. Run the container image to build the Log4j project and generate runner scripts along with dependencies:
++
+[source,bash]
+----
+python infra/helper.py build_fuzzers \
+  --sanitizer address --engine libfuzzer --architecture x86_64 \
+  log4j2
+----
+
+. List generated runner scripts:
++
+[source,bash]
+----
+ls -al build/out/log4j2
+----
+
+. Check one of the generated runner scripts:
++
+[source,bash]
+----
+python infra/helper.py check_build \
+  --sanitizer address --engine libfuzzer --architecture x86_64 \
+  log4j2 log4j-core-fuzz-test-PatternLayoutFuzzer
+----
+
+. Execute one of the generated runner scripts:
++
+[source,bash]
+----
+python infra/helper.py run_fuzzer \
+  --sanitizer address --engine libfuzzer --architecture x86_64 \
+  log4j2 log4j-core-fuzz-test-PatternLayoutFuzzer
+----
+
+[#view]
+=== How can I view fuzzing failures detected by OSS-Fuzz?
+
+The system running fuzzers registered to OSS-Fuzz is called *ClusterFuzz*, which provides https://oss-fuzz.com/[a web interface] for maintainers to monitor the fuzzing results.
+Tests outputs and <<#reproduce,reproduction>> inputs for failed tests are stored in https://console.cloud.google.com/storage/browser/log4j2-logs.clusterfuzz-external.appspot.com[a Google Cloud Storage bucket].
+Access to both the web interface and the bucket is restricted, and only allowed to https://github.com/google/oss-fuzz/blob/master/projects/log4j2/project.yaml[those configured for the project].
+
+[#reproduce]
+=== How can I reproduce fuzzing failures detected by OSS-Fuzz?
+
+Download the associated `.testcase` file from https://console.cloud.google.com/storage/browser/log4j2-logs.clusterfuzz-external.appspot.com[the Google Cloud Storage bucket], and run the following command:
+
+[source,bash]
+----
+python infra/helper.py reproduce \
+  log4j2 <FUZZ-TARGET-NAME> <TESTCASE-FILE-PATH>
+----
+
+Refer to https://google.github.io/oss-fuzz/advanced-topics/reproducing/[the related OSS-Fuzz documentation] for details.
diff --git a/log4j-core-fuzz-test/pom.xml b/log4j-core-fuzz-test/pom.xml
new file mode 100644
index 0000000000..85bb659a78
--- /dev/null
+++ b/log4j-core-fuzz-test/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>${revision}</version>
+    <relativePath>../log4j-parent</relativePath>
+  </parent>
+
+  <artifactId>log4j-core-fuzz-test</artifactId>
+
+  <name>Apache Log4j Core fuzz tests</name>
+
+  <properties>
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
+    <maven.deploy.skip>true</maven.deploy.skip>
+    <maven.install.skip>true</maven.install.skip>
+    <maven.test.skip>true</maven.test.skip>
+    <sign.skip>true</sign.skip>
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-fuzz-test</artifactId>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/log4j-core-fuzz-test/src/main/java/org/apache/logging/log4j/core/fuzz/PatternLayoutFuzzer.java b/log4j-core-fuzz-test/src/main/java/org/apache/logging/log4j/core/fuzz/PatternLayoutFuzzer.java
new file mode 100644
index 0000000000..a241991126
--- /dev/null
+++ b/log4j-core-fuzz-test/src/main/java/org/apache/logging/log4j/core/fuzz/PatternLayoutFuzzer.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.core.fuzz;
+
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.createLoggerContext;
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.fuzzLogger;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.fuzz.EncodingAppender;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.Log4jLoggerFacade;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.LoggerFacade;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+
+public final class PatternLayoutFuzzer {
+
+    public static void fuzzerTestOneInput(final FuzzedDataProvider dataProvider) {
+        final String loggerContextName = PatternLayoutFuzzer.class.getSimpleName() + "LoggerContext";
+        try (final LoggerContext loggerContext =
+                createLoggerContext(loggerContextName, EncodingAppender.PLUGIN_NAME, configBuilder -> configBuilder
+                        .newLayout("PatternLayout")
+                        // Enforce using a single message-based converter, i.e., `MessagePatternConverter`
+                        .addAttribute("pattern", "%m"))) {
+            final ExtendedLogger logger = loggerContext.getLogger(PatternLayoutFuzzer.class);
+            final LoggerFacade loggerFacade = new Log4jLoggerFacade(logger);
+            fuzzLogger(loggerFacade, dataProvider);
+        }
+    }
+}
diff --git a/log4j-core-its/pom.xml b/log4j-core-its/pom.xml
index fe1efb53a3..49f770b883 100644
--- a/log4j-core-its/pom.xml
+++ b/log4j-core-its/pom.xml
@@ -16,19 +16,24 @@
   ~ 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 http://maven.apache.org/maven-v4_0_0.xsd">
+
   <modelVersion>4.0.0</modelVersion>
+
   <parent>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j</artifactId>
     <version>${revision}</version>
     <relativePath>../log4j-parent</relativePath>
   </parent>
+
   <artifactId>log4j-core-its</artifactId>
-  <packaging>jar</packaging>
+
   <name>Apache Log4j Core Integration Tests</name>
-  <description>Integration Tests for the Apache Log4j Implementation</description>
+
   <properties>
+
     <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
     <maven.deploy.skip>true</maven.deploy.skip>
     <maven.install.skip>true</maven.install.skip>
     <sign.skip>true</sign.skip>
@@ -36,6 +41,7 @@
 
     <!-- Dependency versions -->
     <slf4j2.version>2.0.16</slf4j2.version>
+
   </properties>
 
   <dependencyManagement>
diff --git a/log4j-fuzz-test/.log4j-plugin-processing-activator b/log4j-fuzz-test/.log4j-plugin-processing-activator
new file mode 100644
index 0000000000..ba133f3696
--- /dev/null
+++ b/log4j-fuzz-test/.log4j-plugin-processing-activator
@@ -0,0 +1 @@
+This file is here to activate the `plugin-processing` Maven profile.
diff --git a/log4j-fuzz-test/pom.xml b/log4j-fuzz-test/pom.xml
new file mode 100644
index 0000000000..ba7cca9ce0
--- /dev/null
+++ b/log4j-fuzz-test/pom.xml
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>${revision}</version>
+    <relativePath>../log4j-parent</relativePath>
+  </parent>
+
+  <artifactId>log4j-fuzz-test</artifactId>
+
+  <name>Apache Log4j fuzz tests</name>
+
+  <properties>
+
+    <log4j.docgen.skip>true</log4j.docgen.skip>
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+    <maven.deploy.skip>true</maven.deploy.skip>
+    <maven.test.skip>true</maven.test.skip>
+    <sign.skip>true</sign.skip>
+
+    <!-- dependency versions -->
+    <json.version>20240303</json.version>
+
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.jspecify</groupId>
+      <artifactId>jspecify</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.assertj</groupId>
+      <artifactId>assertj-core</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.code-intelligence</groupId>
+      <artifactId>jazzer</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>${json.version}</version>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/EncodingAppender.java b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/EncodingAppender.java
new file mode 100644
index 0000000000..9a86d96bb2
--- /dev/null
+++ b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/EncodingAppender.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.fuzz;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.Serializable;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * Appender encoding incoming log events using the provided layout.
+ * It is intended for appender-agnostic fuzzing.
+ */
+@Plugin(name = EncodingAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
+public final class EncodingAppender extends AbstractAppender {
+
+    public static final String PLUGIN_NAME = "EncodingAppender";
+
+    private EncodingAppender(final String name, final Layout<? extends Serializable> layout) {
+        super(name, null, layout, true, null);
+        // Guard `PLUGIN_NAME` against copy-paste mistakes
+        assertThat(PLUGIN_NAME).isEqualTo(getClass().getSimpleName());
+    }
+
+    @PluginFactory
+    public static EncodingAppender createAppender(
+            final @PluginAttribute("name") String name, final @PluginElement("layout") Layout<?> layout) {
+        return new EncodingAppender(name, layout);
+    }
+
+    @Override
+    public void append(final LogEvent event) {
+        try {
+            getLayout().toByteArray(event);
+        } catch (final Exception ignored) {
+            // We are inspecting unexpected access.
+            // Hence, event encoding failures are not of interest.
+        }
+    }
+}
diff --git a/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/FuzzingUtil.java b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/FuzzingUtil.java
new file mode 100644
index 0000000000..a529e0e7ac
--- /dev/null
+++ b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/FuzzingUtil.java
@@ -0,0 +1,194 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.fuzz;
+
+import static java.util.Objects.requireNonNull;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.builder.api.AppenderComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilder;
+import org.apache.logging.log4j.core.config.builder.api.ConfigurationBuilderFactory;
+import org.apache.logging.log4j.core.config.builder.api.LayoutComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.api.RootLoggerComponentBuilder;
+import org.apache.logging.log4j.core.config.builder.impl.BuiltConfiguration;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+import org.apache.logging.log4j.util.Strings;
+import org.jspecify.annotations.Nullable;
+
+public final class FuzzingUtil {
+
+    private static final int MAX_STRING_LENGTH = 30;
+
+    public static LoggerContext createLoggerContext(
+            final String loggerContextName,
+            final String appenderPluginName,
+            final Function<ConfigurationBuilder<?>, LayoutComponentBuilder> layoutSupplier) {
+
+        // Create the configuration builder
+        final ConfigurationBuilder<BuiltConfiguration> configBuilder =
+                ConfigurationBuilderFactory.newConfigurationBuilder();
+
+        // Create the appender configuration
+        final String appenderName = "FUZZING_APPENDER";
+        final LayoutComponentBuilder layoutComponentBuilder = layoutSupplier.apply(configBuilder);
+        final AppenderComponentBuilder appenderComponentBuilder =
+                configBuilder.newAppender(appenderName, appenderPluginName).add(layoutComponentBuilder);
+
+        // Create the root logger configuration
+        final RootLoggerComponentBuilder loggerComponentBuilder =
+                configBuilder.newRootLogger(Level.ALL).add(configBuilder.newAppenderRef(appenderName));
+
+        // Create the conclusive configuration (uninitialized!)
+        final Configuration config = configBuilder
+                .setStatusLevel(Level.OFF)
+                .add(appenderComponentBuilder)
+                .add(loggerComponentBuilder)
+                .build(false);
+
+        // Create the logger context
+        final LoggerContext loggerContext = new LoggerContext(loggerContextName);
+        loggerContext.start(config);
+        return loggerContext;
+    }
+
+    /**
+     * Contract modelling an API-agnostic logger.
+     * This allows {@link #fuzzLogger(LoggerFacade, FuzzedDataProvider)} to accept all logger implementations; of Log4j API, of SLF4J, etc.
+     */
+    public interface LoggerFacade {
+
+        void log(String message);
+
+        void log(String message, Throwable throwable);
+
+        void log(String message, Object[] parameters);
+    }
+
+    public static final class Log4jLoggerFacade implements LoggerFacade {
+
+        private static final String FQCN = Log4jLoggerFacade.class.getName();
+
+        private final ExtendedLogger logger;
+
+        public Log4jLoggerFacade(final ExtendedLogger logger) {
+            this.logger = requireNonNull(logger, "logger");
+        }
+
+        @Override
+        public void log(final String message) {
+            logger.logIfEnabled(FQCN, Level.ERROR, null, message);
+        }
+
+        @Override
+        public void log(final String message, final Throwable throwable) {
+            logger.logIfEnabled(FQCN, Level.ERROR, null, message, throwable);
+        }
+
+        @Override
+        public void log(final String message, final Object[] parameters) {
+            logger.logIfEnabled(FQCN, Level.ERROR, null, message, parameters);
+        }
+    }
+
+    public static void fuzzLogger(final LoggerFacade logger, final FuzzedDataProvider dataProvider) {
+        requireNonNull(logger, "logger");
+        requireNonNull(dataProvider, "dataProvider");
+        final boolean parameterized = dataProvider.consumeBoolean();
+        if (parameterized) {
+            fuzzLoggerWithParams(logger, dataProvider);
+        } else {
+            fuzzLoggerWithoutParams(logger, dataProvider);
+        }
+    }
+
+    private static void fuzzLoggerWithoutParams(final LoggerFacade logger, final FuzzedDataProvider dataProvider) {
+        final String message = dataProvider.consumeString(MAX_STRING_LENGTH);
+        final Throwable throwable = createThrowableLogParam(dataProvider);
+        if (throwable != null) {
+            logger.log(message, throwable);
+        } else {
+            logger.log(message);
+        }
+    }
+
+    @Nullable
+    private static Throwable createThrowableLogParam(final FuzzedDataProvider dataProvider) {
+        final boolean enabled = dataProvider.consumeBoolean();
+        if (!enabled) {
+            return null;
+        }
+        final String errorMessage = dataProvider.consumeString(MAX_STRING_LENGTH);
+        return new Exception(errorMessage);
+    }
+
+    private static void fuzzLoggerWithParams(final LoggerFacade logger, final FuzzedDataProvider dataProvider) {
+        final int paramCount = dataProvider.consumeInt(1, 3);
+        final String message = Strings.repeat("{}", paramCount);
+        final Throwable throwable = createThrowableLogParam(dataProvider);
+        final Object[] params = IntStream.range(0, paramCount + (throwable != null ? 1 : 0))
+                .mapToObj(ignored -> createLogParam(dataProvider, true))
+                .toArray(Object[]::new);
+        if (throwable != null) {
+            params[paramCount] = throwable;
+            logger.log(message, params);
+        } else {
+            logger.log(message, params);
+        }
+    }
+
+    private static Object createLogParam(final FuzzedDataProvider dataProvider, final boolean nestingAllowed) {
+        final boolean nested = nestingAllowed && dataProvider.consumeBoolean();
+        final int paramTypeCode = dataProvider.consumeInt(0, nested ? 9 : 8);
+        switch (paramTypeCode) {
+            case 0:
+                return dataProvider.consumeByte();
+            case 1:
+                return dataProvider.consumeBoolean();
+            case 2:
+                return dataProvider.consumeChar();
+            case 3:
+                return dataProvider.consumeShort();
+            case 4:
+                return dataProvider.consumeInt();
+            case 5:
+                return dataProvider.consumeLong();
+            case 6:
+                return dataProvider.consumeFloat();
+            case 7:
+                return dataProvider.consumeDouble();
+            case 8:
+                return dataProvider.consumeString(MAX_STRING_LENGTH);
+            default:
+                return createNestedLogParam(dataProvider);
+        }
+    }
+
+    private static Object createNestedLogParam(final FuzzedDataProvider dataProvider) {
+        final int nestLength = dataProvider.consumeInt(0, 3);
+        final Stream<Object> nestItems =
+                IntStream.range(0, nestLength).mapToObj(ignored -> createLogParam(dataProvider, false));
+        final boolean arrayTyped = dataProvider.consumeBoolean();
+        return arrayTyped ? nestItems.toArray(Object[]::new) : nestItems.collect(Collectors.toList());
+    }
+}
diff --git a/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonEncodingAppender.java b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonEncodingAppender.java
new file mode 100644
index 0000000000..5962f62f0c
--- /dev/null
+++ b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonEncodingAppender.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.fuzz;
+
+import static org.apache.logging.log4j.fuzz.JsonUtil.assertValidJson;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.Serializable;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Core;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginElement;
+import org.apache.logging.log4j.core.config.plugins.PluginFactory;
+
+/**
+ * Appender encoding incoming log events using the provided layout and validating its output's JSON conformance.
+ * It is intended for appender-agnostic fuzzing of JSON-producing layouts.
+ */
+@Plugin(name = JsonEncodingAppender.PLUGIN_NAME, category = Core.CATEGORY_NAME, elementType = Appender.ELEMENT_TYPE)
+public final class JsonEncodingAppender extends AbstractAppender {
+
+    public static final String PLUGIN_NAME = "JsonEncodingAppender";
+
+    private JsonEncodingAppender(final String name, final Layout<? extends Serializable> layout) {
+        super(name, null, layout, true, null);
+        // Guard `PLUGIN_NAME` against copy-paste mistakes
+        assertThat(PLUGIN_NAME).isEqualTo(getClass().getSimpleName());
+    }
+
+    @PluginFactory
+    public static JsonEncodingAppender createAppender(
+            final @PluginAttribute("name") String name, final @PluginElement("layout") Layout<?> layout) {
+        return new JsonEncodingAppender(name, layout);
+    }
+
+    @Override
+    public void append(final LogEvent event) {
+        final byte[] jsonBytes;
+        try {
+            jsonBytes = getLayout().toByteArray(event);
+        } catch (final Exception ignored) {
+            // We are inspecting unexpected access.
+            // Hence, event encoding failures are not of interest.
+            return;
+        }
+        assertValidJson(jsonBytes);
+    }
+}
diff --git a/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonUtil.java b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonUtil.java
new file mode 100644
index 0000000000..daee120f05
--- /dev/null
+++ b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/JsonUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.fuzz;
+
+import com.code_intelligence.jazzer.api.FuzzerSecurityIssueCritical;
+import java.nio.charset.StandardCharsets;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+
+public final class JsonUtil {
+
+    public static void assertValidJson(final String json) {
+        final byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
+        assertValidJson(jsonBytes);
+    }
+
+    public static void assertValidJson(final byte[] jsonBytes) {
+        // We deliberately use an external library instead of using `JsonReader` from Log4j.
+        // This is to fuzz Log4j components in isolation.
+        // We are not using Jackson or Gson, since they have token limitations due to performance reasons.
+        // That is, for instance, Jackson supports numbers up to 1000 digits, which the fuzzer quickly bumps into.
+        try {
+            // `JSONObject` requires the input to be a JSON object, hence we are ensuring it to be so:
+            final String json = "{\"x\":" + new String(jsonBytes, StandardCharsets.UTF_8) + "}";
+            final JSONTokener jsonTokener = new JSONTokener(json);
+            new JSONObject(jsonTokener);
+        } catch (final Exception error) {
+            throw new FuzzerSecurityIssueCritical("malformed JSON output", error);
+        }
+    }
+
+    private JsonUtil() {}
+}
diff --git a/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/package-info.java b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/package-info.java
new file mode 100644
index 0000000000..e18c54c359
--- /dev/null
+++ b/log4j-fuzz-test/src/main/java/org/apache/logging/log4j/fuzz/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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.
+ */
+@NullMarked
+package org.apache.logging.log4j.fuzz;
+
+import org.jspecify.annotations.NullMarked;
diff --git a/log4j-layout-template-json-fuzz-test/.log4j-plugin-processing-activator b/log4j-layout-template-json-fuzz-test/.log4j-plugin-processing-activator
new file mode 100644
index 0000000000..ba133f3696
--- /dev/null
+++ b/log4j-layout-template-json-fuzz-test/.log4j-plugin-processing-activator
@@ -0,0 +1 @@
+This file is here to activate the `plugin-processing` Maven profile.
diff --git a/log4j-layout-template-json-fuzz-test/pom.xml b/log4j-layout-template-json-fuzz-test/pom.xml
new file mode 100644
index 0000000000..df7720b3db
--- /dev/null
+++ b/log4j-layout-template-json-fuzz-test/pom.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>${revision}</version>
+    <relativePath>../log4j-parent</relativePath>
+  </parent>
+
+  <artifactId>log4j-layout-template-json-fuzz-test</artifactId>
+
+  <name>Apache Log4j JSON Template Layout fuzz tests</name>
+
+  <properties>
+
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
+    <maven.deploy.skip>true</maven.deploy.skip>
+    <maven.install.skip>true</maven.install.skip>
+    <maven.test.skip>true</maven.test.skip>
+    <sign.skip>true</sign.skip>
+
+    <!-- dependency versions -->
+    <json.version>20240303</json.version>
+
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-layout-template-json</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-fuzz-test</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.json</groupId>
+      <artifactId>json</artifactId>
+      <version>${json.version}</version>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutCodecFuzzer.java b/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutCodecFuzzer.java
new file mode 100644
index 0000000000..1fe836481e
--- /dev/null
+++ b/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutCodecFuzzer.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.layout.template.json.fuzz;
+
+import static org.apache.logging.log4j.fuzz.JsonUtil.assertValidJson;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.apache.logging.log4j.layout.template.json.JsonTemplateLayoutDefaults;
+import org.apache.logging.log4j.layout.template.json.util.JsonReader;
+import org.apache.logging.log4j.layout.template.json.util.JsonWriter;
+
+// FUZZER-SETUP-SCRIPT cp ${outputDir}/json.dict ${scriptPath}.dict
+// FUZZER-SETUP-SCRIPT cp ${outputDir}/json_seed_corpus.zip ${scriptPath}_seed_corpus.zip
+
+public final class JsonTemplateLayoutCodecFuzzer {
+
+    // Choosing a reasonable JSON document length sufficing following needs:
+    //
+    // 1. Long enough to give fuzzer room to try breaking stuff
+    //
+    // 2. Short enough to avoid stack overflows, which are interpreted as fuzzer failures, though they are not.
+    //    Consider a JSON document containing more than 1024 nested objects.
+    //    This triggers a stack overflow, which is expected, hence nothing to signal.
+    private static final int MAX_STRING_LENGTH = 1024;
+
+    public static void fuzzerTestOneInput(final FuzzedDataProvider dataProvider) {
+        final boolean encodeFirst = dataProvider.consumeBoolean();
+        if (encodeFirst) {
+            encodeThenDecode(dataProvider);
+        } else {
+            decodeThenEncode(dataProvider);
+        }
+    }
+
+    private static void encodeThenDecode(final FuzzedDataProvider dataProvider) {
+        final String expectedString = dataProvider.consumeString(MAX_STRING_LENGTH);
+        final String json = encodeJson(expectedString);
+        final Object actualString = JsonReader.read(json);
+        assertThat(actualString).isEqualTo(expectedString);
+    }
+
+    private static void decodeThenEncode(final FuzzedDataProvider dataProvider) {
+        final Object object = decodeJson(dataProvider);
+        final String json = encodeJson(object);
+        assertValidJson(json);
+    }
+
+    private static Object decodeJson(final FuzzedDataProvider dataProvider) {
+        final String expectedJson = dataProvider.consumeString(MAX_STRING_LENGTH);
+        try {
+            return JsonReader.read(expectedJson);
+        } catch (final Exception ignored) {
+            // We are inspecting unexpected access.
+            // Hence, event decoding failures are not of interest.
+            return null;
+        }
+    }
+
+    private static String encodeJson(final Object object) {
+        final JsonWriter jsonWriter = JsonWriter.newBuilder()
+                .setMaxStringLength(MAX_STRING_LENGTH)
+                .setTruncatedStringSuffix(JsonTemplateLayoutDefaults.getTruncatedStringSuffix())
+                .build();
+        jsonWriter.writeValue(object);
+        return jsonWriter.getStringBuilder().toString();
+    }
+}
diff --git a/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutFuzzer.java b/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutFuzzer.java
new file mode 100644
index 0000000000..f44866ca5b
--- /dev/null
+++ b/log4j-layout-template-json-fuzz-test/src/main/java/org/apache/logging/log4j/layout/template/json/fuzz/JsonTemplateLayoutFuzzer.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.log4j.layout.template.json.fuzz;
+
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.createLoggerContext;
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.fuzzLogger;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.apache.logging.log4j.core.LoggerContext;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.Log4jLoggerFacade;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.LoggerFacade;
+import org.apache.logging.log4j.fuzz.JsonEncodingAppender;
+import org.apache.logging.log4j.spi.ExtendedLogger;
+
+public final class JsonTemplateLayoutFuzzer {
+
+    public static void fuzzerTestOneInput(final FuzzedDataProvider dataProvider) {
+        final String loggerContextName = JsonTemplateLayoutFuzzer.class.getSimpleName() + "LoggerContext";
+        try (final LoggerContext loggerContext = createLoggerContext(
+                loggerContextName,
+                JsonEncodingAppender.PLUGIN_NAME,
+                configBuilder -> configBuilder.newLayout("JsonTemplateLayout"))) {
+            final ExtendedLogger logger = loggerContext.getLogger(JsonTemplateLayoutFuzzer.class);
+            final LoggerFacade loggerFacade = new Log4jLoggerFacade(logger);
+            fuzzLogger(loggerFacade, dataProvider);
+        }
+    }
+}
diff --git a/log4j-layout-template-json-test/pom.xml b/log4j-layout-template-json-test/pom.xml
index 51bc8a9660..bba5ab598e 100644
--- a/log4j-layout-template-json-test/pom.xml
+++ b/log4j-layout-template-json-test/pom.xml
@@ -31,9 +31,8 @@
   <name>Apache Log4j JSON Template Layout tests</name>
 
   <properties>
-
-    <log4j.docgen.skip>true</log4j.docgen.skip>
     <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
     <maven.deploy.skip>true</maven.deploy.skip>
     <sign.skip>true</sign.skip>
 
diff --git a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
index bdada9f49a..11607acb2d 100644
--- a/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
+++ b/log4j-layout-template-json-test/src/test/java/org/apache/logging/log4j/layout/template/json/util/JsonWriterTest.java
@@ -49,19 +49,6 @@ class JsonWriterTest {
             .setTruncatedStringSuffix("~")
             .build();
 
-    private static final int SURROGATE_CODE_POINT = 65536;
-
-    private static final char[] SURROGATE_PAIR = new char[2];
-
-    static {
-        // noinspection ResultOfMethodCallIgnored
-        Character.toChars(SURROGATE_CODE_POINT, SURROGATE_PAIR, 0);
-    }
-
-    private static final char HI_SURROGATE = SURROGATE_PAIR[0];
-
-    private static final char LO_SURROGATE = SURROGATE_PAIR[1];
-
     private static synchronized <V> V withLockedWriterReturning(final Function<JsonWriter, V> consumer) {
         synchronized (WRITER) {
             return consumer.apply(WRITER);
@@ -99,16 +86,6 @@ class JsonWriterTest {
         Assertions.assertThat(writer.getStringBuilder().length()).isEqualTo(0);
     }
 
-    @Test
-    void test_surrogate_code_point() {
-        Assertions.assertThat(HI_SURROGATE).matches(Character::isHighSurrogate, "is high surrogate");
-        Assertions.assertThat(LO_SURROGATE).matches(Character::isLowSurrogate, "is low surrogate");
-        Assertions.assertThat(Character.isSurrogatePair(HI_SURROGATE, LO_SURROGATE))
-                .as("is surrogate pair")
-                .isTrue();
-        Assertions.assertThat(SURROGATE_CODE_POINT).matches(Character::isDefined, "is defined");
-    }
-
     @Test
     void test_use_null_Runnable() {
         Assertions.assertThatThrownBy(() -> withLockedWriter(writer -> writer.use(null)))
@@ -436,25 +413,6 @@ class JsonWriterTest {
         });
     }
 
-    @Test
-    void test_writeString_emitter_excessive_string_ending_with_high_surrogate() {
-        withLockedWriter(writer -> {
-            final int maxStringLength = writer.getMaxStringLength();
-            @SuppressWarnings("StringBufferReplaceableByString")
-            final String excessiveString = new StringBuilder()
-                    .append(Strings.repeat("x", maxStringLength - 1))
-                    .append(HI_SURROGATE)
-                    .append(LO_SURROGATE)
-                    .toString();
-            final String expectedJson =
-                    "\"" + Strings.repeat("x", maxStringLength - 1) + writer.getTruncatedStringSuffix() + '"';
-            final BiConsumer<StringBuilder, String> emitter = StringBuilder::append;
-            final String actualJson = writer.use(() -> writer.writeString(emitter, excessiveString));
-            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
-            assertFormattableBufferReset(writer);
-        });
-    }
-
     @Test
     void test_writeString_null_formattable() {
         expectNull(writer -> writer.writeString((StringBuilderFormattable) null));
@@ -482,25 +440,6 @@ class JsonWriterTest {
         });
     }
 
-    @Test
-    void test_writeString_formattable_excessive_string_ending_with_high_surrogate() {
-        withLockedWriter(writer -> {
-            final int maxStringLength = writer.getMaxStringLength();
-            @SuppressWarnings("StringBufferReplaceableByString")
-            final String excessiveString = new StringBuilder()
-                    .append(Strings.repeat("x", maxStringLength - 1))
-                    .append(HI_SURROGATE)
-                    .append(LO_SURROGATE)
-                    .toString();
-            final String expectedJson =
-                    "\"" + Strings.repeat("x", maxStringLength - 1) + writer.getTruncatedStringSuffix() + '"';
-            final String actualJson =
-                    writer.use(() -> writer.writeString(stringBuilder -> stringBuilder.append(excessiveString)));
-            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
-            assertFormattableBufferReset(writer);
-        });
-    }
-
     private static void assertFormattableBufferReset(final JsonWriter writer) {
         final StringBuilder formattableBuffer = getFormattableBuffer(writer);
         Assertions.assertThat(formattableBuffer.capacity()).isEqualTo(writer.getMaxStringLength());
@@ -552,23 +491,6 @@ class JsonWriterTest {
         });
     }
 
-    @Test
-    void test_writeString_excessive_seq_ending_with_high_surrogate() {
-        withLockedWriter(writer -> {
-            final int maxStringLength = writer.getMaxStringLength();
-            @SuppressWarnings("StringBufferReplaceableByString")
-            final CharSequence seq = new StringBuilder()
-                    .append(Strings.repeat("x", maxStringLength - 1))
-                    .append(HI_SURROGATE)
-                    .append(LO_SURROGATE)
-                    .toString();
-            final String expectedJson =
-                    "\"" + Strings.repeat("x", maxStringLength - 1) + writer.getTruncatedStringSuffix() + '"';
-            final String actualJson = writer.use(() -> writer.writeString(seq));
-            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
-        });
-    }
-
     @Test
     void test_writeString_seq() throws IOException {
         final char[] surrogates = new char[2];
@@ -617,24 +539,6 @@ class JsonWriterTest {
         });
     }
 
-    @Test
-    void test_writerString_excessive_buffer_ending_with_high_surrogate() {
-        withLockedWriter(writer -> {
-            final int maxStringLength = writer.getMaxStringLength();
-            @SuppressWarnings("StringBufferReplaceableByString")
-            final char[] buffer = new StringBuilder()
-                    .append(Strings.repeat("x", maxStringLength - 1))
-                    .append(HI_SURROGATE)
-                    .append(LO_SURROGATE)
-                    .toString()
-                    .toCharArray();
-            final String expectedJson =
-                    "\"" + Strings.repeat("x", maxStringLength - 1) + writer.getTruncatedStringSuffix() + '"';
-            final String actualJson = writer.use(() -> writer.writeString(buffer));
-            Assertions.assertThat(actualJson).isEqualTo(expectedJson);
-        });
-    }
-
     @Test
     void test_writeString_buffer() throws IOException {
         final char[] buffer = new char[2];
diff --git a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
index 4316d731fd..749eb1519e 100644
--- a/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
+++ b/log4j-layout-template-json/src/main/java/org/apache/logging/log4j/layout/template/json/util/JsonWriter.java
@@ -579,9 +579,7 @@ public final class JsonWriter implements AutoCloseable, Cloneable {
      * Quote text contents using JSON standard quoting.
      */
     private void quoteString(final CharSequence seq, final int offset, final int length) {
-        final int surrogateCorrection =
-                length > 0 && Character.isHighSurrogate(seq.charAt(offset + length - 1)) ? -1 : 0;
-        final int limit = offset + length + surrogateCorrection;
+        final int limit = offset + length;
         int i = offset;
         outer:
         while (i < limit) {
@@ -643,8 +641,7 @@ public final class JsonWriter implements AutoCloseable, Cloneable {
      * Quote text contents using JSON standard quoting.
      */
     private void quoteString(final char[] buffer, final int offset, final int length) {
-        final int surrogateCorrection = length > 0 && Character.isHighSurrogate(buffer[offset + length - 1]) ? -1 : 0;
-        final int limit = offset + length + surrogateCorrection;
+        final int limit = offset + length;
         int i = offset;
         outer:
         while (i < limit) {
diff --git a/log4j-osgi-test/pom.xml b/log4j-osgi-test/pom.xml
index 60930eb317..a4f3d91179 100644
--- a/log4j-osgi-test/pom.xml
+++ b/log4j-osgi-test/pom.xml
@@ -32,15 +32,17 @@
   <description>The Apache Log4j OSGi tests</description>
 
   <properties>
-    <log4j.docgen.skip>true</log4j.docgen.skip>
-    <module.name>org.apache.logging.log4j.osgi</module.name>
+
     <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
     <maven.deploy.skip>true</maven.deploy.skip>
     <maven.install.skip>true</maven.install.skip>
     <sign.skip>true</sign.skip>
-
     <spotbugs.skip>true</spotbugs.skip>
+
+    <!-- dependency versions -->
     <spifly.version>1.3.7</spifly.version>
+
   </properties>
 
   <dependencies>
diff --git a/log4j-parent/pom.xml b/log4j-parent/pom.xml
index 304231465a..78c5d4fc15 100644
--- a/log4j-parent/pom.xml
+++ b/log4j-parent/pom.xml
@@ -103,6 +103,7 @@
     <javax-servlet-jsp.version>2.3.3</javax-servlet-jsp.version>
     <jansi.version>2.4.1</jansi.version>
     <java-allocation-instrumenter.version>3.3.4</java-allocation-instrumenter.version>
+    <jazzer.version>0.22.1</jazzer.version>
     <jconsole.version>1.7.0</jconsole.version>
     <jctools.version>4.0.5</jctools.version>
     <jeromq.version>0.6.0</jeromq.version>
@@ -116,6 +117,7 @@
     <kafka.version>3.8.0</kafka.version>
     <lightcouch.version>0.2.0</lightcouch.version>
     <log4j.version>1.2.17</log4j.version>
+    <log4j-transform.version>0.1.0</log4j-transform.version>
     <log4j2-cachefile-transformer.version>2.15.0</log4j2-cachefile-transformer.version>
     <log4j2-ecs-layout.version>1.6.0</log4j2-ecs-layout.version>
     <logback.version>1.3.14</logback.version>
@@ -158,11 +160,11 @@
   <dependencyManagement>
     <dependencies>
 
-      <!-- BEGIN: Modules that are not published (and hence missing in the BOM) -->
+      <!-- BEGIN: Modules that are consumed only internally -->
 
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
-        <artifactId>log4j-osgi-test</artifactId>
+        <artifactId>log4j-fuzz-test</artifactId>
         <version>${project.version}</version>
       </dependency>
 
@@ -172,7 +174,7 @@
         <version>${project.version}</version>
       </dependency>
 
-      <!-- END: Modules that are not published (and hence missing in the BOM) -->
+      <!-- END: Modules that are consumed only internally -->
 
       <dependency>
         <groupId>org.codehaus.groovy</groupId>
@@ -244,6 +246,13 @@
         <type>zip</type>
       </dependency>
 
+      <!-- Dummy dependency to trick `dependabot` into updating the `log4j-transform.version` property: -->
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
+        <version>${log4j-transform.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.activemq</groupId>
         <artifactId>activemq-broker</artifactId>
@@ -268,6 +277,12 @@
         <version>${awaitility.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>com.code-intelligence</groupId>
+        <artifactId>jazzer</artifactId>
+        <version>${jazzer.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache-extras.beanshell</groupId>
         <artifactId>bsh</artifactId>
@@ -894,6 +909,61 @@
           <version>${maven-taglib-plugin.version}</version>
         </plugin>
 
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-shade-plugin</artifactId>
+          <dependencies>
+            <dependency>
+              <groupId>org.apache.logging.log4j</groupId>
+              <artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
+              <version>${log4j-transform.version}</version>
+            </dependency>
+          </dependencies>
+          <executions>
+            <execution>
+              <id>generate-uber-jar</id>
+              <goals>
+                <goal>shade</goal>
+              </goals>
+              <phase>package</phase>
+              <configuration>
+                <createDependencyReducedPom>false</createDependencyReducedPom>
+                <!-- Minimization causes `@Plugin`-annotated classes to be omitted, hence disabling it: -->
+                <minimizeJar>false</minimizeJar>
+                <finalName>${project.artifactId}-${project.version}-uber</finalName>
+                <transformers>
+                  <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
+                  <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheNoticeResourceTransformer" />
+                  <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                    <manifestEntries>
+                      <Multi-Release>true</Multi-Release>
+                    </manifestEntries>
+                  </transformer>
+                  <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
+                  <transformer implementation="org.apache.logging.log4j.maven.plugins.shade.transformer.Log4j2PluginCacheFileTransformer" />
+                </transformers>
+                <filters>
+                  <filter>
+                    <!--
+                      Shading signed JARs will fail without this.
+                      http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
+                    -->
+                    <artifact>*:*</artifact>
+                    <excludes>
+                      <exclude>META-INF/*.SF</exclude>
+                      <exclude>META-INF/*.DSA</exclude>
+                      <exclude>META-INF/*.RSA</exclude>
+                      <exclude>META-INF/MANIFEST.MF</exclude>
+                      <exclude>module-info.class</exclude>
+                      <exclude>META-INF/versions/9/module-info.class</exclude>
+                    </excludes>
+                  </filter>
+                </filters>
+              </configuration>
+            </execution>
+          </executions>
+        </plugin>
+
       </plugins>
     </pluginManagement>
 
diff --git a/log4j-perf-test/README.adoc b/log4j-perf-test/README.adoc
new file mode 100644
index 0000000000..ab7951082c
--- /dev/null
+++ b/log4j-perf-test/README.adoc
@@ -0,0 +1,30 @@
+////
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF 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
+
+         http://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.
+////
+
+This module contains JMH-based performance tests for Log4j.
+You can use the generated fat JAR to run the tests as follows:
+
+[source,bash]
+----
+java -jar target/log4j-perf-test-*-uber.jar <JMH-ARGS>
+----
+
+To list the available JMH arguments run:
+[source,bash]
+----
+java -jar target/log4j-perf-test-*-uber.jar -h
+----
diff --git a/log4j-perf-test/pom.xml b/log4j-perf-test/pom.xml
index 76a38c5d99..8b47178c9d 100644
--- a/log4j-perf-test/pom.xml
+++ b/log4j-perf-test/pom.xml
@@ -16,7 +16,9 @@
   ~ 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
   <modelVersion>4.0.0</modelVersion>
+
   <parent>
     <groupId>org.apache.logging.log4j</groupId>
     <artifactId>log4j</artifactId>
@@ -30,22 +32,21 @@
   <description>The Apache Log4j development-time performance tests</description>
 
   <properties>
-    <uberjar.name>benchmarks</uberjar.name>
-    <log4j.docgen.skip>true</log4j.docgen.skip>
+
     <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
     <maven.test.skip>true</maven.test.skip>
     <maven.deploy.skip>true</maven.deploy.skip>
     <maven.install.skip>true</maven.install.skip>
     <sign.skip>true</sign.skip>
+    <spotbugs.skip>true</spotbugs.skip>
 
-    <module.name>org.apache.logging.log4j.perf</module.name>
     <maven.compiler.release>9</maven.compiler.release>
     <surefire.jdkToolchain>[9, )</surefire.jdkToolchain>
-    <spotbugs.skip>true</spotbugs.skip>
 
     <!-- Dependency versions -->
-    <log4j-transform.version>0.1.0</log4j-transform.version>
     <slf4j2.version>2.0.16</slf4j2.version>
+
   </properties>
 
   <dependencyManagement>
@@ -191,56 +192,13 @@
         </configuration>
       </plugin>
 
+      <!-- Activate the inherited `generate-uber-jar` configuration -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-shade-plugin</artifactId>
-        <dependencies>
-          <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-transform-maven-shade-plugin-extensions</artifactId>
-            <version>${log4j-transform.version}</version>
-          </dependency>
-        </dependencies>
-        <executions>
-          <execution>
-            <goals>
-              <goal>shade</goal>
-            </goals>
-            <phase>package</phase>
-            <configuration>
-              <createDependencyReducedPom>false</createDependencyReducedPom>
-              <minimizeJar>false</minimizeJar>
-              <finalName>${uberjar.name}</finalName>
-              <transformers>
-                <transformer implementation="org.apache.maven.plugins.shade.resource.ApacheLicenseResourceTransformer" />
-                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
-                  <mainClass>org.openjdk.jmh.Main</mainClass>
-                  <manifestEntries>
-                    <Multi-Release>true</Multi-Release>
-                  </manifestEntries>
-                </transformer>
-                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer" />
-                <transformer implementation="org.apache.logging.log4j.maven.plugins.shade.transformer.Log4j2PluginCacheFileTransformer" />
-              </transformers>
-              <filters>
-                <filter>
-                  <!--
-                    Shading signed JARs will fail without this.
-                    http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
-                  -->
-                  <artifact>*:*</artifact>
-                  <excludes>
-                    <exclude>META-INF/*.SF</exclude>
-                    <exclude>META-INF/*.DSA</exclude>
-                    <exclude>META-INF/*.RSA</exclude>
-                  </excludes>
-                </filter>
-              </filters>
-            </configuration>
-          </execution>
-        </executions>
       </plugin>
 
     </plugins>
   </build>
+
 </project>
diff --git a/log4j-slf4j2-impl-fuzz-test/pom.xml b/log4j-slf4j2-impl-fuzz-test/pom.xml
new file mode 100644
index 0000000000..6dc3fd1020
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/pom.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>${revision}</version>
+    <relativePath>../log4j-parent</relativePath>
+  </parent>
+
+  <artifactId>log4j-slf4j2-impl-fuzz-test</artifactId>
+
+  <name>SLF4J 2 Provider for Log4j API fuzz tests</name>
+
+  <properties>
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+    <log4j.docgen.skip>true</log4j.docgen.skip>
+    <maven.deploy.skip>true</maven.deploy.skip>
+    <maven.install.skip>true</maven.install.skip>
+    <maven.test.skip>true</maven.test.skip>
+    <sign.skip>true</sign.skip>
+  </properties>
+
+  <dependencies>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-slf4j2-impl</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-fuzz-test</artifactId>
+    </dependency>
+
+  </dependencies>
+
+</project>
diff --git a/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jLoggerFacade.java b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jLoggerFacade.java
new file mode 100644
index 0000000000..5622f0343c
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jLoggerFacade.java
@@ -0,0 +1,65 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.slf4j.fuzz;
+
+import static java.util.Objects.requireNonNull;
+
+import org.apache.logging.log4j.fuzz.FuzzingUtil.LoggerFacade;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.spi.CallerBoundaryAware;
+import org.slf4j.spi.LoggingEventBuilder;
+
+public final class Slf4jLoggerFacade implements LoggerFacade {
+
+    private static final String FQCN = Slf4jLoggerFacade.class.getName();
+
+    private final Logger logger;
+
+    private Slf4jLoggerFacade(final Logger logger) {
+        this.logger = logger;
+    }
+
+    static Slf4jLoggerFacade ofClass(final Class<?> clazz) {
+        requireNonNull(clazz, "clazz");
+        final Logger logger = LoggerFactory.getLogger(clazz);
+        return new Slf4jLoggerFacade(logger);
+    }
+
+    private LoggingEventBuilder atError() {
+        final LoggingEventBuilder builder = logger.atError();
+        if (builder instanceof CallerBoundaryAware) {
+            ((CallerBoundaryAware) builder).setCallerBoundary(FQCN);
+        }
+        return builder;
+    }
+
+    @Override
+    public void log(final String message) {
+        atError().log(message);
+    }
+
+    @Override
+    public void log(final String message, final Throwable throwable) {
+        atError().setCause(throwable).log(message);
+    }
+
+    @Override
+    public void log(final String message, final Object[] parameters) {
+        atError().log(message, parameters);
+    }
+}
diff --git a/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.java b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.java
new file mode 100644
index 0000000000..0ee07f0572
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.slf4j.fuzz;
+
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.fuzzLogger;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.LoggerFacade;
+
+public final class Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer {
+
+    static {
+        final String configurationFile = Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.class.getSimpleName() + ".xml";
+        System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, configurationFile);
+    }
+
+    private static final LoggerFacade LOGGER =
+            Slf4jLoggerFacade.ofClass(Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.class);
+
+    public static void fuzzerTestOneInput(final FuzzedDataProvider dataProvider) {
+        fuzzLogger(LOGGER, dataProvider);
+    }
+}
diff --git a/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.java b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.java
new file mode 100644
index 0000000000..059d0ac9a8
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/src/main/java/org/apache/logging/slf4j/fuzz/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF 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
+ *
+ *      http://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 org.apache.logging.slf4j.fuzz;
+
+import static org.apache.logging.log4j.fuzz.FuzzingUtil.fuzzLogger;
+
+import com.code_intelligence.jazzer.api.FuzzedDataProvider;
+import org.apache.logging.log4j.core.config.ConfigurationFactory;
+import org.apache.logging.log4j.fuzz.FuzzingUtil.LoggerFacade;
+
+public final class Slf4jToLog4jBridgeWithPatternLayoutFuzzer {
+
+    static {
+        final String configurationFile = Slf4jToLog4jBridgeWithPatternLayoutFuzzer.class.getSimpleName() + ".xml";
+        System.setProperty(ConfigurationFactory.CONFIGURATION_FILE_PROPERTY, configurationFile);
+    }
+
+    private static final LoggerFacade LOGGER =
+            Slf4jLoggerFacade.ofClass(Slf4jToLog4jBridgeWithPatternLayoutFuzzer.class);
+
+    public static void fuzzerTestOneInput(final FuzzedDataProvider dataProvider) {
+        fuzzLogger(LOGGER, dataProvider);
+    }
+}
diff --git a/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.xml b/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.xml
new file mode 100644
index 0000000000..a60edb4b16
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithJsonTemplateLayoutFuzzer.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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.
+  -->
+<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xmlns="https://logging.apache.org/xml/ns"
+               xsi:schemaLocation="
+                       https://logging.apache.org/xml/ns
+                       https://logging.apache.org/xml/ns/log4j-config-2.xsd">
+
+  <Appenders>
+    <JsonEncodingAppender name="FUZZING_APPENDER">
+      <JsonTemplateLayout/>
+    </JsonEncodingAppender>
+  </Appenders>
+
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="FUZZING_APPENDER"/>
+    </Root>
+  </Loggers>
+
+</Configuration>
diff --git a/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.xml b/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.xml
new file mode 100644
index 0000000000..5f3ea8d49f
--- /dev/null
+++ b/log4j-slf4j2-impl-fuzz-test/src/main/resources/Slf4jToLog4jBridgeWithPatternLayoutFuzzer.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Licensed to the Apache Software Foundation (ASF) under one or more
+  ~ contributor license agreements.  See the NOTICE file distributed with
+  ~ this work for additional information regarding copyright ownership.
+  ~ The ASF 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
+  ~
+  ~      http://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.
+  -->
+<Configuration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+               xmlns="https://logging.apache.org/xml/ns"
+               xsi:schemaLocation="
+                       https://logging.apache.org/xml/ns
+                       https://logging.apache.org/xml/ns/log4j-config-2.xsd">
+
+  <Appenders>
+    <EncodingAppender name="FUZZING_APPENDER">
+      <PatternLayout pattern="%m%n"/>
+    </EncodingAppender>
+  </Appenders>
+
+  <Loggers>
+    <Root level="ALL">
+      <AppenderRef ref="FUZZING_APPENDER"/>
+    </Root>
+  </Loggers>
+
+</Configuration>
diff --git a/mvnw b/mvnw
index 5e9618cac2..19529ddf8c 100755
--- a/mvnw
+++ b/mvnw
@@ -21,312 +21,239 @@
 # ----------------------------------------------------------------------------
 # Apache Maven Wrapper startup batch script, version 3.3.2
 #
-# Required ENV vars:
-# ------------------
-#   JAVA_HOME - location of a JDK home dir
-#
 # Optional ENV vars
 # -----------------
-#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
-#     e.g. to debug Maven itself, use
-#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
-#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+#   JAVA_HOME - location of a JDK home dir, required when download maven via java source
+#   MVNW_REPOURL - repo url base for downloading maven distribution
+#   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+#   MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
 # ----------------------------------------------------------------------------
 
-if [ -z "$MAVEN_SKIP_RC" ]; then
-
-  if [ -f /usr/local/etc/mavenrc ]; then
-    . /usr/local/etc/mavenrc
-  fi
-
-  if [ -f /etc/mavenrc ]; then
-    . /etc/mavenrc
-  fi
+set -euf
+[ "${MVNW_VERBOSE-}" != debug ] || set -x
 
-  if [ -f "$HOME/.mavenrc" ]; then
-    . "$HOME/.mavenrc"
-  fi
-
-fi
-
-# OS specific support.  $var _must_ be set to either true or false.
-cygwin=false
-darwin=false
-mingw=false
+# OS specific support.
+native_path() { printf %s\\n "$1"; }
 case "$(uname)" in
-CYGWIN*) cygwin=true ;;
-MINGW*) mingw=true ;;
-Darwin*)
-  darwin=true
-  # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
-  # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
-  if [ -z "$JAVA_HOME" ]; then
-    if [ -x "/usr/libexec/java_home" ]; then
-      JAVA_HOME="$(/usr/libexec/java_home)"
-      export JAVA_HOME
-    else
-      JAVA_HOME="/Library/Java/Home"
-      export JAVA_HOME
-    fi
-  fi
+CYGWIN* | MINGW*)
+  [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
+  native_path() { cygpath --path --windows "$1"; }
   ;;
 esac
 
-if [ -z "$JAVA_HOME" ]; then
-  if [ -r /etc/gentoo-release ]; then
-    JAVA_HOME=$(java-config --jre-home)
-  fi
-fi
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched
-if $cygwin; then
-  [ -n "$JAVA_HOME" ] \
-    && JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
-  [ -n "$CLASSPATH" ] \
-    && CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
-fi
-
-# For Mingw, ensure paths are in UNIX format before anything is touched
-if $mingw; then
-  [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] \
-    && JAVA_HOME="$(
-      cd "$JAVA_HOME" || (
-        echo "cannot cd into $JAVA_HOME." >&2
-        exit 1
-      )
-      pwd
-    )"
-fi
-
-if [ -z "$JAVA_HOME" ]; then
-  javaExecutable="$(which javac)"
-  if [ -n "$javaExecutable" ] && ! [ "$(expr "$javaExecutable" : '\([^ ]*\)')" = "no" ]; then
-    # readlink(1) is not available as standard on Solaris 10.
-    readLink=$(which readlink)
-    if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
-      if $darwin; then
-        javaHome="$(dirname "$javaExecutable")"
-        javaExecutable="$(cd "$javaHome" && pwd -P)/javac"
-      else
-        javaExecutable="$(readlink -f "$javaExecutable")"
-      fi
-      javaHome="$(dirname "$javaExecutable")"
-      javaHome=$(expr "$javaHome" : '\(.*\)/bin')
-      JAVA_HOME="$javaHome"
-      export JAVA_HOME
-    fi
-  fi
-fi
-
-if [ -z "$JAVACMD" ]; then
-  if [ -n "$JAVA_HOME" ]; then
+# set JAVACMD and JAVACCMD
+set_java_home() {
+  # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
+  if [ -n "${JAVA_HOME-}" ]; then
     if [ -x "$JAVA_HOME/jre/sh/java" ]; then
       # IBM's JDK on AIX uses strange locations for the executables
       JAVACMD="$JAVA_HOME/jre/sh/java"
+      JAVACCMD="$JAVA_HOME/jre/sh/javac"
     else
       JAVACMD="$JAVA_HOME/bin/java"
+      JAVACCMD="$JAVA_HOME/bin/javac"
+
+      if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
+        echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
+        echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
+        return 1
+      fi
     fi
   else
     JAVACMD="$(
-      \unset -f command 2>/dev/null
-      \command -v java
-    )"
-  fi
-fi
-
-if [ ! -x "$JAVACMD" ]; then
-  echo "Error: JAVA_HOME is not defined correctly." >&2
-  echo "  We cannot execute $JAVACMD" >&2
-  exit 1
-fi
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v java
+    )" || :
+    JAVACCMD="$(
+      'set' +e
+      'unset' -f command 2>/dev/null
+      'command' -v javac
+    )" || :
 
-if [ -z "$JAVA_HOME" ]; then
-  echo "Warning: JAVA_HOME environment variable is not set." >&2
-fi
-
-# traverses directory structure from process work directory to filesystem root
-# first directory with .mvn subdirectory is considered project base directory
-find_maven_basedir() {
-  if [ -z "$1" ]; then
-    echo "Path not specified to find_maven_basedir" >&2
-    return 1
+    if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
+      echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
+      return 1
+    fi
   fi
+}
 
-  basedir="$1"
-  wdir="$1"
-  while [ "$wdir" != '/' ]; do
-    if [ -d "$wdir"/.mvn ]; then
-      basedir=$wdir
-      break
-    fi
-    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
-    if [ -d "${wdir}" ]; then
-      wdir=$(
-        cd "$wdir/.." || exit 1
-        pwd
-      )
-    fi
-    # end of workaround
+# hash string like Java String::hashCode
+hash_string() {
+  str="${1:-}" h=0
+  while [ -n "$str" ]; do
+    char="${str%"${str#?}"}"
+    h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
+    str="${str#?}"
   done
-  printf '%s' "$(
-    cd "$basedir" || exit 1
-    pwd
-  )"
+  printf %x\\n $h
 }
 
-# concatenates all lines of a file
-concat_lines() {
-  if [ -f "$1" ]; then
-    # Remove \r in case we run on Windows within Git Bash
-    # and check out the repository with auto CRLF management
-    # enabled. Otherwise, we may read lines that are delimited with
-    # \r\n and produce $'-Xarg\r' rather than -Xarg due to word
-    # splitting rules.
-    tr -s '\r\n' ' ' <"$1"
-  fi
+verbose() { :; }
+[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
+
+die() {
+  printf %s\\n "$1" >&2
+  exit 1
 }
 
-log() {
-  if [ "$MVNW_VERBOSE" = true ]; then
-    printf '%s\n' "$1"
-  fi
+trim() {
+  # MWRAPPER-139:
+  #   Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
+  #   Needed for removing poorly interpreted newline sequences when running in more
+  #   exotic environments such as mingw bash on Windows.
+  printf "%s" "${1}" | tr -d '[:space:]'
 }
 
-BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
-if [ -z "$BASE_DIR" ]; then
-  exit 1
+# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
+while IFS="=" read -r key value; do
+  case "${key-}" in
+  distributionUrl) distributionUrl=$(trim "${value-}") ;;
+  distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
+  esac
+done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
+
+case "${distributionUrl##*/}" in
+maven-mvnd-*bin.*)
+  MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
+  case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
+  *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
+  :Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
+  :Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
+  :Linux*x86_64*) distributionPlatform=linux-amd64 ;;
+  *)
+    echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
+    distributionPlatform=linux-amd64
+    ;;
+  esac
+  distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
+  ;;
+maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
+*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
+esac
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
+distributionUrlName="${distributionUrl##*/}"
+distributionUrlNameMain="${distributionUrlName%.*}"
+distributionUrlNameMain="${distributionUrlNameMain%-bin}"
+MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
+MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
+
+exec_maven() {
+  unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
+  exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
+}
+
+if [ -d "$MAVEN_HOME" ]; then
+  verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  exec_maven "$@"
 fi
 
-MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
-export MAVEN_PROJECTBASEDIR
-log "$MAVEN_PROJECTBASEDIR"
+case "${distributionUrl-}" in
+*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
+*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
+esac
 
-##########################################################################################
-# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
-# This allows using the maven wrapper in projects that prohibit checking in binary data.
-##########################################################################################
-wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
-if [ -r "$wrapperJarPath" ]; then
-  log "Found $wrapperJarPath"
+# prepare tmp dir
+if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
+  clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
+  trap clean HUP INT TERM EXIT
 else
-  log "Couldn't find $wrapperJarPath, downloading it ..."
+  die "cannot create temp dir"
+fi
 
-  if [ -n "$MVNW_REPOURL" ]; then
-    wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
-  else
-    wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
-  fi
-  while IFS="=" read -r key value; do
-    # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
-    safeValue=$(echo "$value" | tr -d '\r')
-    case "$key" in wrapperUrl)
-      wrapperUrl="$safeValue"
-      break
-      ;;
-    esac
-  done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
-  log "Downloading from: $wrapperUrl"
+mkdir -p -- "${MAVEN_HOME%/*}"
 
-  if $cygwin; then
-    wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
-  fi
+# Download and Install Apache Maven
+verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+verbose "Downloading from: $distributionUrl"
+verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
 
-  if command -v wget >/dev/null; then
-    log "Found wget ... using wget"
-    [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
-    if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-      wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
-    else
-      wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
-    fi
-  elif command -v curl >/dev/null; then
-    log "Found curl ... using curl"
-    [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
-    if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
-      curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
-    else
-      curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
-    fi
-  else
-    log "Falling back to using Java to download"
-    javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
-    javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
-    # For Cygwin, switch paths to Windows format before running javac
-    if $cygwin; then
-      javaSource=$(cygpath --path --windows "$javaSource")
-      javaClass=$(cygpath --path --windows "$javaClass")
-    fi
-    if [ -e "$javaSource" ]; then
-      if [ ! -e "$javaClass" ]; then
-        log " - Compiling MavenWrapperDownloader.java ..."
-        ("$JAVA_HOME/bin/javac" "$javaSource")
-      fi
-      if [ -e "$javaClass" ]; then
-        log " - Running MavenWrapperDownloader.java ..."
-        ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
-      fi
-    fi
-  fi
+# select .zip or .tar.gz
+if ! command -v unzip >/dev/null; then
+  distributionUrl="${distributionUrl%.zip}.tar.gz"
+  distributionUrlName="${distributionUrl##*/}"
 fi
-##########################################################################################
-# End of extension
-##########################################################################################
 
-# If specified, validate the SHA-256 sum of the Maven wrapper jar file
-wrapperSha256Sum=""
-while IFS="=" read -r key value; do
-  case "$key" in wrapperSha256Sum)
-    wrapperSha256Sum=$value
-    break
-    ;;
-  esac
-done <"$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
-if [ -n "$wrapperSha256Sum" ]; then
-  wrapperSha256Result=false
-  if command -v sha256sum >/dev/null; then
-    if echo "$wrapperSha256Sum  $wrapperJarPath" | sha256sum -c >/dev/null 2>&1; then
-      wrapperSha256Result=true
+# verbose opt
+__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
+[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
+
+# normalize http auth
+case "${MVNW_PASSWORD:+has-password}" in
+'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
+esac
+
+if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
+  verbose "Found wget ... using wget"
+  wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
+elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
+  verbose "Found curl ... using curl"
+  curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
+elif set_java_home; then
+  verbose "Falling back to use Java to download"
+  javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
+  targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
+  cat >"$javaSource" <<-END
+	public class Downloader extends java.net.Authenticator
+	{
+	  protected java.net.PasswordAuthentication getPasswordAuthentication()
+	  {
+	    return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
+	  }
+	  public static void main( String[] args ) throws Exception
+	  {
+	    setDefault( new Downloader() );
+	    java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
+	  }
+	}
+	END
+  # For Cygwin/MinGW, switch paths to Windows format before running javac and java
+  verbose " - Compiling Downloader.java ..."
+  "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
+  verbose " - Running Downloader.java ..."
+  "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
+fi
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+if [ -n "${distributionSha256Sum-}" ]; then
+  distributionSha256Result=false
+  if [ "$MVN_CMD" = mvnd.sh ]; then
+    echo "Checksum validation is not supported for maven-mvnd." >&2
+    echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
+    exit 1
+  elif command -v sha256sum >/dev/null; then
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
+      distributionSha256Result=true
     fi
   elif command -v shasum >/dev/null; then
-    if echo "$wrapperSha256Sum  $wrapperJarPath" | shasum -a 256 -c >/dev/null 2>&1; then
-      wrapperSha256Result=true
+    if echo "$distributionSha256Sum  $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
+      distributionSha256Result=true
     fi
   else
     echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
-    echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." >&2
+    echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
     exit 1
   fi
-  if [ $wrapperSha256Result = false ]; then
-    echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
-    echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
-    echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
+  if [ $distributionSha256Result = false ]; then
+    echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
+    echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
     exit 1
   fi
 fi
 
-MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin; then
-  [ -n "$JAVA_HOME" ] \
-    && JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
-  [ -n "$CLASSPATH" ] \
-    && CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
-  [ -n "$MAVEN_PROJECTBASEDIR" ] \
-    && MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
+# unzip and move
+if command -v unzip >/dev/null; then
+  unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
+else
+  tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
 fi
+printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
+mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
 
-# Provide a "standardized" way to retrieve the CLI args that will
-# work with both Windows and non-Windows executions.
-MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
-export MAVEN_CMD_LINE_ARGS
-
-WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-
-# shellcheck disable=SC2086 # safe args
-exec "$JAVACMD" \
-  $MAVEN_OPTS \
-  $MAVEN_DEBUG_OPTS \
-  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
-  "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
-  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
+clean || :
+exec_maven "$@"
diff --git a/mvnw.cmd b/mvnw.cmd
index 4136715f08..249bdf3822 100644
--- a/mvnw.cmd
+++ b/mvnw.cmd
@@ -1,3 +1,4 @@
+<# : batch portion
 @REM ----------------------------------------------------------------------------
 @REM Licensed to the Apache Software Foundation (ASF) under one
 @REM or more contributor license agreements.  See the NOTICE file
@@ -20,187 +21,129 @@
 @REM ----------------------------------------------------------------------------
 @REM Apache Maven Wrapper startup batch script, version 3.3.2
 @REM
-@REM Required ENV vars:
-@REM JAVA_HOME - location of a JDK home dir
-@REM
 @REM Optional ENV vars
-@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
-@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
-@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
-@REM     e.g. to debug Maven itself, use
-@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
-@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM   MVNW_REPOURL - repo url base for downloading maven distribution
+@REM   MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
+@REM   MVNW_VERBOSE - true: enable verbose log; others: silence the output
 @REM ----------------------------------------------------------------------------
 
-@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
-@echo off
-@REM set title of command window
-title %0
-@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
-@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
-
-@REM set %HOME% to equivalent of $HOME
-if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
-
-@REM Execute a user defined script before this one
-if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
-@REM check for pre script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
-if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
-:skipRcPre
-
-@setlocal
-
-set ERROR_CODE=0
-
-@REM To isolate internal variables from possible post scripts, we use another setlocal
-@setlocal
-
-@REM ==== START VALIDATION ====
-if not "%JAVA_HOME%" == "" goto OkJHome
-
-echo. >&2
-echo Error: JAVA_HOME not found in your environment. >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo. >&2
-goto error
-
-:OkJHome
-if exist "%JAVA_HOME%\bin\java.exe" goto init
-
-echo. >&2
-echo Error: JAVA_HOME is set to an invalid directory. >&2
-echo JAVA_HOME = "%JAVA_HOME%" >&2
-echo Please set the JAVA_HOME variable in your environment to match the >&2
-echo location of your Java installation. >&2
-echo. >&2
-goto error
-
-@REM ==== END VALIDATION ====
-
-:init
-
-@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
-@REM Fallback to current working directory if not found.
-
-set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
-IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
-
-set EXEC_DIR=%CD%
-set WDIR=%EXEC_DIR%
-:findBaseDir
-IF EXIST "%WDIR%"\.mvn goto baseDirFound
-cd ..
-IF "%WDIR%"=="%CD%" goto baseDirNotFound
-set WDIR=%CD%
-goto findBaseDir
-
-:baseDirFound
-set MAVEN_PROJECTBASEDIR=%WDIR%
-cd "%EXEC_DIR%"
-goto endDetectBaseDir
-
-:baseDirNotFound
-set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
-cd "%EXEC_DIR%"
-
-:endDetectBaseDir
-
-IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
-
-@setlocal EnableExtensions EnableDelayedExpansion
-for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
-@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
-
-:endReadAdditionalConfig
-
-SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
-set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
-set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
-
-set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
-
-FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
-    IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
-)
-
-@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
-@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
-if exist %WRAPPER_JAR% (
-    if "%MVNW_VERBOSE%" == "true" (
-        echo Found %WRAPPER_JAR%
-    )
-) else (
-    if not "%MVNW_REPOURL%" == "" (
-        SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar"
-    )
-    if "%MVNW_VERBOSE%" == "true" (
-        echo Couldn't find %WRAPPER_JAR%, downloading it ...
-        echo Downloading from: %WRAPPER_URL%
-    )
-
-    powershell -Command "&{"^
-		"$webclient = new-object System.Net.WebClient;"^
-		"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
-		"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
-		"}"^
-		"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
-		"}"
-    if "%MVNW_VERBOSE%" == "true" (
-        echo Finished downloading %WRAPPER_JAR%
-    )
-)
-@REM End of extension
-
-@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
-SET WRAPPER_SHA_256_SUM=""
-FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
-    IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
+@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
+@SET __MVNW_CMD__=
+@SET __MVNW_ERROR__=
+@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
+@SET PSModulePath=
+@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
+  IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
 )
-IF NOT %WRAPPER_SHA_256_SUM%=="" (
-    powershell -Command "&{"^
-       "Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash;"^
-       "$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
-       "If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
-       "  Write-Error 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
-       "  Write-Error 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
-       "  Write-Error 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
-       "  exit 1;"^
-       "}"^
-       "}"
-    if ERRORLEVEL 1 goto error
-)
-
-@REM Provide a "standardized" way to retrieve the CLI args that will
-@REM work with both Windows and non-Windows executions.
-set MAVEN_CMD_LINE_ARGS=%*
-
-%MAVEN_JAVA_EXE% ^
-  %JVM_CONFIG_MAVEN_PROPS% ^
-  %MAVEN_OPTS% ^
-  %MAVEN_DEBUG_OPTS% ^
-  -classpath %WRAPPER_JAR% ^
-  "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
-  %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
-if ERRORLEVEL 1 goto error
-goto end
-
-:error
-set ERROR_CODE=1
-
-:end
-@endlocal & set ERROR_CODE=%ERROR_CODE%
-
-if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
-@REM check for post script, once with legacy .bat ending and once with .cmd ending
-if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
-if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
-:skipRcPost
-
-@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
-if "%MAVEN_BATCH_PAUSE%"=="on" pause
-
-if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
-
-cmd /C exit /B %ERROR_CODE%
+@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
+@SET __MVNW_PSMODULEP_SAVE=
+@SET __MVNW_ARG0_NAME__=
+@SET MVNW_USERNAME=
+@SET MVNW_PASSWORD=
+@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
+@echo Cannot start maven from wrapper >&2 && exit /b 1
+@GOTO :EOF
+: end batch / begin powershell #>
+
+$ErrorActionPreference = "Stop"
+if ($env:MVNW_VERBOSE -eq "true") {
+  $VerbosePreference = "Continue"
+}
+
+# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
+$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
+if (!$distributionUrl) {
+  Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
+}
+
+switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
+  "maven-mvnd-*" {
+    $USE_MVND = $true
+    $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
+    $MVN_CMD = "mvnd.cmd"
+    break
+  }
+  default {
+    $USE_MVND = $false
+    $MVN_CMD = $script -replace '^mvnw','mvn'
+    break
+  }
+}
+
+# apply MVNW_REPOURL and calculate MAVEN_HOME
+# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
+if ($env:MVNW_REPOURL) {
+  $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
+  $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
+}
+$distributionUrlName = $distributionUrl -replace '^.*/',''
+$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
+$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
+if ($env:MAVEN_USER_HOME) {
+  $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
+}
+$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
+$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
+
+if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
+  Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
+  Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
+  exit $?
+}
+
+if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
+  Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
+}
+
+# prepare tmp dir
+$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
+$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
+$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
+trap {
+  if ($TMP_DOWNLOAD_DIR.Exists) {
+    try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+    catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+  }
+}
+
+New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
+
+# Download and Install Apache Maven
+Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
+Write-Verbose "Downloading from: $distributionUrl"
+Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
+
+$webclient = New-Object System.Net.WebClient
+if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
+  $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
+}
+[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
+
+# If specified, validate the SHA-256 sum of the Maven distribution zip file
+$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
+if ($distributionSha256Sum) {
+  if ($USE_MVND) {
+    Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
+  }
+  Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
+  if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
+    Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
+  }
+}
+
+# unzip and move
+Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
+Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
+try {
+  Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
+} catch {
+  if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
+    Write-Error "fail to move MAVEN_HOME"
+  }
+} finally {
+  try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
+  catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
+}
+
+Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
diff --git a/oss-fuzz-build.sh b/oss-fuzz-build.sh
new file mode 100755
index 0000000000..98f0650695
--- /dev/null
+++ b/oss-fuzz-build.sh
@@ -0,0 +1,217 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF 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
+#
+#      http://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.
+
+# Enable strict mode
+set -euo pipefail
+IFS=$'\n\t'
+
+# Read command line arguments
+if [[ "$#" -ne 1 || -z "${JVM_LD_LIBRARY_PATH:-}" ]]; then
+  cat >&2 <<EOF
+Generates fuzzer runner scripts to be employed by Google OSS-Fuzz.
+See "FUZZING.adoc" for details.
+
+Usage: $0 <outputDir>
+
+  outputDir
+
+    The output directory to dump runner scripts and their dependencies.
+
+Environment variables:
+
+  BUILD (optional)
+
+    If "false", Maven installation will be skipped.
+
+  FORCE_DOWNLOAD (optional)
+
+    If "true", certain externally downloaded files (corpus, dictionary, etc.)
+    will be downloaded anyway and override existing ones.
+
+  JVM_LD_LIBRARY_PATH (required)
+
+    Directory containing the Java shared libraries, e.g.,
+    "/usr/lib/jvm/zulu17/lib/server".
+EOF
+  exit 1
+fi
+outputDir=$(readlink -f "$1")
+
+# Ensure output directory exists
+mkdir -p "$outputDir"
+
+# Switch to the script directory
+cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
+
+# Record the Git commit ID
+commitId=$(git rev-parse HEAD)
+
+# To contain all Maven dependencies under `$outputDir`, explicitly provide the Maven local repository.
+# `.`-prefixed files are not copied by OSS-Fuzz, don't use them!
+export MAVEN_OPTS="-Dmaven.repo.local=$outputDir/m2"
+
+# Make Maven executions scripting friendly
+export MAVEN_ARGS="--batch-mode --no-transfer-progress --errors"
+
+# Determine the project version
+projectVersion=$(./mvnw \
+  --quiet -DforceStdout=true \
+  -Dexpression=project.version \
+  help:evaluate \
+  | tail -n 1)
+
+# Build the project
+[ "${BUILD:-}" != "false" ] && ./mvnw \
+  -Dmaven.test.skip=true \
+  -Dxml.skip \
+  -Dcyclonedx.skip \
+  -Dbnd.baseline.skip \
+  -Drat.skip \
+  -Dspotless.skip \
+  -Dspotbugs.skip \
+  -Denforcer.skip \
+  install
+
+# Download the JSON dictionary
+jsonDictPath="$outputDir/json.dict"
+[[ ! -f "$jsonDictPath" || "${FORCE_DOWNLOAD:-}" = "true" ]] && \
+  wget --quiet https://raw.githubusercontent.com/google/fuzzing/master/dictionaries/json.dict -O "$jsonDictPath"
+
+# Download the JSON seed corpus
+jsonSeedCorpusPath="$outputDir/json_seed_corpus.zip"
+if [[ ! -f "$jsonSeedCorpusPath" || "${FORCE_DOWNLOAD:-}" = "true" ]]; then
+  git clone --quiet --depth 1 https://github.com/dvyukov/go-fuzz-corpus
+  zip -q -j "$jsonSeedCorpusPath" go-fuzz-corpus/json/corpus/*
+  rm -rf go-fuzz-corpus
+fi
+
+# Build and fuzz environments are different: https://google.github.io/oss-fuzz/further-reading/fuzzer-environment/
+# Package the Java runtime along with the fuzzers.
+javaHomeFileName="jvm"
+cp -rL "$JAVA_HOME" "$outputDir/$javaHomeFileName"
+
+# Iterate over fuzzers
+for module in *-fuzz-test; do
+
+  # Copy the built module artifact to the output folder
+  cp "$PWD/$module/target/$module-$projectVersion.jar" "$outputDir/$module-$projectVersion.jar"
+
+  # Determine the Java class path
+  ./mvnw dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt -pl $module
+  classPath="$(sed "s|$outputDir/||g" /tmp/cp.txt):$module-$projectVersion.jar"
+  rm /tmp/cp.txt
+
+  for fuzzer in $(find "$module" -name "*Fuzzer.java"); do
+
+    # Create the runner script
+    fqcn=$(echo "$fuzzer" | sed -r 's|^'$module'/src/main/java/(.+)\.java|\1|' | tr / .)
+    className="${fqcn##*.}"
+    scriptPath="$outputDir/$module-$className"
+    cat >"$scriptPath" <<EOF
+#!/bin/bash
+#
+# This file is auto-generated by https://github.com/apache/logging-log4j2/tree/2.x/oss-fuzz-build.sh
+#
+
+# OSS-Fuzz detects fuzzers by checking the presence of the magical "LLVMFuzzerTestOneInput" word, hence this line.
+
+# Enable strict mode
+set -euo pipefail
+IFS=\$'\n\t'
+
+# Report the build
+echo "INFO: Produced using the commit ID: $commitId"
+
+# Switch to the script directory
+cd -- "\$(dirname -- "\${BASH_SOURCE[0]}")"
+
+# Determine JVM args
+if [[ "\$@" =~ (^| )-runs=[0-9]+(\$| ) ]]; then
+  jvmArgs="-Xmx1900m:-Xss900k"
+else
+  jvmArgs="-Xmx2048m:-Xss1024k"
+fi
+
+# Verify the classpath
+classPath="$classPath"
+echo "\$classPath" | sed 's/:/\n/g' | while read classPathFilePath; do
+  if [[ ! -f "\$classPathFilePath" ]]; then
+    echo "ERROR: Could not find class path file: \"\$classPathFilePath\""
+    echo "ERROR: Complete class path: \"\$classPath\""
+    echo "DEBUG: \"ls\" output"
+    ls -al | sed "s/^/DEBUG: /g"
+    classPathFileName=\$(basename "\$classPathFilePath")
+    echo "DEBUG: \"find\" output"
+    find / -name "\$classPathFileName" 2>/dev/null | sed "s/^/DEBUG: /g"
+    exit 1
+  fi
+done
+
+# Employ the Java runtime
+export JAVA_HOME="\$PWD/$javaHomeFileName"
+export JVM_LD_LIBRARY_PATH="\$JAVA_HOME/lib/server"
+export PATH="\$JAVA_HOME/bin:\$PATH"
+java -version
+
+# Dump some debugging aid
+dump_debug() {
+  echo "DEBUG: Dumping system memory information..."
+  free -m
+  echo "DEBUG: Dumping environment variables..."
+  export
+  for filePath in . "\${CRASH_STACKTRACES_DIR:-}" "\${CRASH_STACKTRACES_DIR:-}" "\${DATA_BUNDLES_DIR:-}" "\${FUZZ_INPUTS:-}"; do
+    if [ -e "\$filePath" ]; then
+      echo "DEBUG: Listing \"\$filePath\"..."
+      ls -al "\$filePath"
+    fi
+  done
+}
+dump_debug
+
+# Create a temporary file for capturing the command output
+cmdOutputFile=\$(mktemp "$module-$className-XXX.out")
+trap 'rm -f -- "'\$cmdOutputFile'"' EXIT
+
+# Run the fuzzer
+LD_LIBRARY_PATH="\$JVM_LD_LIBRARY_PATH":. \\
+jazzer_driver \\
+  --agent_path=jazzer_agent_deploy.jar \\
+  --cp="\$classPath" \\
+  --target_class="$fqcn" \\
+  --jvm_args="\$jvmArgs" \\
+  \$@ \\
+  2>&1 | tee "\$cmdOutputFile" || {
+    retCode=\$?;
+    if grep -q "A fatal error has been detected by the Java Runtime Environment" "\$cmdOutputFile"; then
+      echo "WARN: Detected JRE crash; it doesn't qualify as a fuzzing failure."
+      echo "WARN: Cleaning up crash reports and exiting with success..."
+      dump_debug
+      exit 0
+    else
+      exit \$retCode
+    fi
+  }
+EOF
+    chmod +x "$scriptPath"
+
+    # Execute fuzzer setup scripts
+    { grep "^// FUZZER-SETUP-SCRIPT " "$fuzzer" || :; } | while read _ _ setupScript; do
+      eval $setupScript
+    done
+
+  done
+
+done
diff --git a/pom.xml b/pom.xml
index e778a2e640..5b6219e14d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -230,7 +230,7 @@
     <!-- the parent POM must come first: -->
     <module>log4j-parent</module>
 
-    <!-- Next unpublished modules in alphabetical order: -->
+    <!-- Next unpublished, but internally and *implicitly* (e.g., as a plugin dependency) consumed modules in alphabetical order: -->
     <module>log4j-api-java9</module>
     <module>log4j-core-java9</module>
 
@@ -242,10 +242,12 @@
     <module>log4j-appserver</module>
     <module>log4j-cassandra</module>
     <module>log4j-core</module>
+    <module>log4j-core-fuzz-test</module>
     <module>log4j-core-its</module>
     <module>log4j-core-test</module>
     <module>log4j-couchdb</module>
     <module>log4j-docker</module>
+    <module>log4j-fuzz-test</module>
     <module>log4j-iostreams</module>
     <module>log4j-jakarta-smtp</module>
     <module>log4j-jakarta-web</module>
@@ -255,6 +257,7 @@
     <module>log4j-jdbc-dbcp2</module>
     <module>log4j-jul</module>
     <module>log4j-layout-template-json</module>
+    <module>log4j-layout-template-json-fuzz-test</module>
     <module>log4j-layout-template-json-test</module>
     <module>log4j-mongodb</module>
     <module>log4j-mongodb4</module>
@@ -262,6 +265,7 @@
     <module>log4j-perf-test</module>
     <module>log4j-slf4j-impl</module>
     <module>log4j-slf4j2-impl</module>
+    <module>log4j-slf4j2-impl-fuzz-test</module>
     <module>log4j-spring-boot</module>
     <module>log4j-spring-cloud-config-client</module>
     <module>log4j-taglib</module>
@@ -372,6 +376,10 @@
   <dependencyManagement>
     <dependencies>
 
+      <!-- List of managed dependencies (in alphabetical order) that are intended for public consumption.
+           Modules that are not used anywhere (e.g., `log4j-core-its`, `log4j-osgi-test`) should not be placed here.
+           Modules that are used only internally (e.g., `log4j-layout-template-json-test`) should be added to `dependencyManagement > dependencies` in `log4j-parent/pom.xml`. -->
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-1.2-api</artifactId>
diff --git a/src/changelog/.2.x.x/2949_add_fuzzing.xml b/src/changelog/.2.x.x/2949_add_fuzzing.xml
new file mode 100644
index 0000000000..5ca555b861
--- /dev/null
+++ b/src/changelog/.2.x.x/2949_add_fuzzing.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="added">
+  <issue id="2949" link="https://github.com/apache/logging-log4j2/pull/2949"/>
+  <description format="asciidoc">Add Google OSS-Fuzz integration</description>
+</entry>
diff --git a/src/changelog/.2.x.x/fix_JsonWriter_surrogate_handling.xml b/src/changelog/.2.x.x/fix_JsonWriter_surrogate_handling.xml
new file mode 100644
index 0000000000..d3610aaec4
--- /dev/null
+++ b/src/changelog/.2.x.x/fix_JsonWriter_surrogate_handling.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <description format="asciidoc">Avoid surrogate character handling in `JsonWriter`</description>
+</entry>
diff --git a/src/site/antora/modules/ROOT/pages/development.adoc b/src/site/antora/modules/ROOT/pages/development.adoc
index f9efa8959d..edc861ede6 100644
--- a/src/site/antora/modules/ROOT/pages/development.adoc
+++ b/src/site/antora/modules/ROOT/pages/development.adoc
@@ -61,10 +61,15 @@ See {logging-services-url}/logging-parent/usage.html#website[the Logging Parent
 Branch triggering the CI logic to start {logging-services-url}/logging-parent/release-instructions-project.html[the release process]
 
 [#building]
-== How can I build Log4j from sources?
+== How can I build the project?
 
 See {project-github-url}/BUILDING.adoc[the build instructions].
 
+[#fuzzing]
+== How can I run fuzz tests?
+
+See {project-github-url}/FUZZING.adoc[the fuzzing instructions].
+
 [#contributor]
 == I am **not** a committer. How shall I submit a patch?