From 26caf847100bca95a613fe4e64a33ed5a8a32d26 Mon Sep 17 00:00:00 2001 From: Quan Anh Mai Date: Wed, 15 Jul 2026 15:22:52 +0000 Subject: [PATCH 01/17] 8387328: C2: A Phi must not have a narrower Type than its inputs Reviewed-by: thartmann Backport-of: 2b05a136cb80221a4252719510f817e268da5d5a --- src/hotspot/share/opto/parse.hpp | 1 + src/hotspot/share/opto/parse1.cpp | 31 ++- .../jtreg/compiler/parsing/TestNarrowPhi.java | 196 ++++++++++++++++++ 3 files changed, 225 insertions(+), 3 deletions(-) create mode 100644 test/hotspot/jtreg/compiler/parsing/TestNarrowPhi.java diff --git a/src/hotspot/share/opto/parse.hpp b/src/hotspot/share/opto/parse.hpp index 5118019fc313..426720b5bba7 100644 --- a/src/hotspot/share/opto/parse.hpp +++ b/src/hotspot/share/opto/parse.hpp @@ -480,6 +480,7 @@ class Parse : public GraphKit { // Helper: Merge the current mapping into the given basic block void merge_common(Block* target, int pnum); // Helper functions for merging individual cells. + Node* maybe_narrow_phi_input(Node* ctrl, Node* n, const Type* phi_type); PhiNode *ensure_phi( int idx, bool nocreate = false); PhiNode *ensure_memory_phi(int idx, bool nocreate = false); // Helper to merge the current memory state into the given basic block diff --git a/src/hotspot/share/opto/parse1.cpp b/src/hotspot/share/opto/parse1.cpp index 6a400631bffb..2d74866e5708 100644 --- a/src/hotspot/share/opto/parse1.cpp +++ b/src/hotspot/share/opto/parse1.cpp @@ -1883,7 +1883,8 @@ void Parse::merge_common(Parse::Block* target, int pnum) { if (phi != nullptr) { assert(n != top() || r->in(pnum) == top(), "live value must not be garbage"); assert(phi->region() == r, ""); - phi->set_req(pnum, n); // Then add 'n' to the merge + phi->set_req(pnum, maybe_narrow_phi_input(r->in(pnum), n, _gvn.type(phi))); + if (pnum == PhiNode::Input) { // Last merge for this Phi. // So far, Phis have had a reasonable type from ciTypeFlow. @@ -2060,6 +2061,21 @@ int Parse::Block::add_new_path() { return pnum; } +// The verifier ensures that the ciType of phi is not narrower than its inputs. However, since +// TypeOopPtr::make_from_klass may be aggressive if it finds that the ciType has only a single +// concrete subtype, and concurrent class loading/unloading may change this property during the +// compilation process, it may be the case that the Type of phi is narrower than its inputs. In +// those cases, we need to insert a CheckCastPP, otherwise several PhiNode idealization may be +// unsound, as we may replace a Phi which has a narrower Type with one of its input which has a +// wider Type. +Node* Parse::maybe_narrow_phi_input(Node* ctrl, Node* n, const Type* phi_type) { + if (phi_type->isa_oopptr() != nullptr && !_gvn.type(n)->higher_equal(phi_type)) { + n = new CheckCastPPNode(ctrl, n, phi_type, ConstraintCastNode::DependencyType::NonFloatingNarrowing); + n = _gvn.transform(n); + } + return n; +} + //------------------------------ensure_phi------------------------------------- // Turn the idx'th entry of the current map into a Phi PhiNode *Parse::ensure_phi(int idx, bool nocreate) { @@ -2108,9 +2124,18 @@ PhiNode *Parse::ensure_phi(int idx, bool nocreate) { return nullptr; } - PhiNode* phi = PhiNode::make(region, o, t); + PhiNode* phi = new PhiNode(region, t); gvn().set_type(phi, t); - if (C->do_escape_analysis()) record_for_igvn(phi); + for (uint i = 1; i < phi->req(); i++) { + Node* ctrl = region->in(i); + if (ctrl != nullptr) { + phi->init_req(i, maybe_narrow_phi_input(ctrl, o, t)); + } + } + + if (C->do_escape_analysis()) { + record_for_igvn(phi); + } map->set_req(idx, phi); return phi; } diff --git a/test/hotspot/jtreg/compiler/parsing/TestNarrowPhi.java b/test/hotspot/jtreg/compiler/parsing/TestNarrowPhi.java new file mode 100644 index 000000000000..879234a62cfb --- /dev/null +++ b/test/hotspot/jtreg/compiler/parsing/TestNarrowPhi.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package compiler.parsing; + +import java.io.IOException; +import java.util.Objects; +import jdk.test.lib.Asserts; +import jdk.test.whitebox.WhiteBox; +import jdk.test.lib.process.ProcessTools; + +/* + * @test + * @bug 8387328 + * @summary A Phi having a narrower Type than its inputs may result in incorrect scheduling + * @library /test/lib + * @requires vm.compiler2.enabled + * @modules java.base/jdk.internal.misc + * @build jdk.test.whitebox.WhiteBox + * @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox + * @run main/othervm -Xbootclasspath/a:. -XX:+UnlockDiagnosticVMOptions -XX:+WhiteBoxAPI + * ${test.main.class} + */ +public class TestNarrowPhi { + private static final WhiteBox WHITE_BOX = WhiteBox.getWhiteBox(); + private static volatile Throwable failure; + + private static abstract class P { + int u; + + private static P allocate() { + return new C(); + } + } + + private static class C extends P { + int v; + } + + public static void main(String[] args) throws IOException, InterruptedException, NoSuchMethodException { + if (args.length == 0) { + spawnTestProcesses(); + } else { + int idx = Integer.parseInt(args[0]); + runTest(idx); + } + } + + private static void spawnTestProcesses() throws IOException, InterruptedException { + String testClassName = TestNarrowPhi.class.getName(); + // Since we cannot reliably coordinate the compiler thread and the thread that load the + // child class, randomly delaying one of them + for (int i = 0; i <= 10; i++) { + var builder = ProcessTools.createTestJavaProcessBuilder( + "-Xbootclasspath/a:.", + "-Xbatch", + "-XX:-TieredCompilation", + "-XX:+UnlockDiagnosticVMOptions", + "-XX:+WhiteBoxAPI", + "-XX:CompileOnly=" + testClassName + "::test*", + "-XX:CompileCommand=inline," + testClassName + "::inline*", + "-XX:CompileCommand=dontinline," + testClassName + "::nonInline", + "-XX:CompileCommand=delayinline," + testClassName + "::inlineTestHelper", + testClassName, + Integer.toString(i)); + builder.redirectOutput(ProcessBuilder.Redirect.INHERIT); + builder.redirectError(ProcessBuilder.Redirect.INHERIT); + var process = builder.start(); + process.waitFor(); + Asserts.assertEQ(0, process.exitValue()); + } + } + + private static void runTest(int idx) throws InterruptedException, NoSuchMethodException { + var testMethod = TestNarrowPhi.class.getDeclaredMethod("testMethod", boolean.class, P.class, P.class, P.class); + var _ = Objects.class; + Thread loader = new Thread(() -> { + try { + if (idx < 5) { + Thread.sleep((5 - idx) * 10L); + } + var _ = C.class; + } catch (Exception e) { + failure = e; + } + }); + loader.start(); + + if (idx > 5) { + Thread.sleep((idx - 5) * 10L); + } + if (!WHITE_BOX.enqueueMethodForCompilation(testMethod, 4)) { + throw new RuntimeException("Could not enqueue the test method for C2 compilation"); + } + while (WHITE_BOX.isMethodQueuedForCompilation(testMethod)) { + Thread.yield(); + } + P p = P.allocate(); + Asserts.assertEQ(0, testMethod(true, p, p, p)); + loader.join(); + if (failure != null) { + throw new RuntimeException(failure); + } + } + + private static int testMethod(boolean b, P p1, P p2, P p3) { + // Arbitrarily delay the parser between generating the Type for P1 and for the loop Phi + // below + inline0(); + // This method is late-inlined, which increases the chance that C has been loaded then + return inlineTestHelper(b, p1, p2, p3); + } + + private static int inlineTestHelper(boolean b, P p1, P p2, P p3) { + // Random access that can be used as an implicit null-check, so that the load below can + // float freely + p1.u = 0; + P p = p1; + for (int i = 0; i < 1; i++) { + if (i % 2 != 0) { + p = p2; + } + } + + C cp = (C) Objects.requireNonNull(p); + C cp3 = (C) Objects.requireNonNull(p3); + int res = cp.v; + cp3.v = 1; + if (b) { + cp3.v = 2; + return res; + } else { + return nonInline(); + } + } + + private static int nonInline() { + return 0; + } + + private static void inline0() { + inline1(); + inline1(); + inline1(); + inline1(); + } + + private static void inline1() { + inline2(); + inline2(); + inline2(); + inline2(); + } + + private static void inline2() { + inline3(); + inline3(); + inline3(); + inline3(); + } + + private static void inline3() { + inline4(); + inline4(); + inline4(); + inline4(); + } + + private static void inline4() { + inline5(); + inline5(); + inline5(); + inline5(); + } + + private static void inline5() {} +} From cf9a5e12f4350c7a572332cb771d7d94b2f58098 Mon Sep 17 00:00:00 2001 From: Erik Gahlin Date: Thu, 16 Jul 2026 07:11:06 +0000 Subject: [PATCH 02/17] 8385957: JFR: Sensitive command-line arguments still in environment variable values Reviewed-by: mgronlun Backport-of: 723826295aa50a88cbb702128da79a91e6d87c73 --- src/hotspot/share/jfr/dcmd/jfrDcmds.cpp | 5 +- .../share/jfr/periodic/jfrRedactedEvents.cpp | 176 ++++++++++++++---- .../share/jfr/periodic/jfrRedactedEvents.hpp | 4 + src/java.base/share/man/java.md | 8 +- test/jdk/jdk/jfr/startupargs/TestRedact.java | 90 +++++++-- 5 files changed, 228 insertions(+), 55 deletions(-) diff --git a/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp b/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp index a41515edfbbf..58d6c029bc16 100644 --- a/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp +++ b/src/hotspot/share/jfr/dcmd/jfrDcmds.cpp @@ -486,8 +486,9 @@ void JfrConfigureFlightRecorderDCmd::print_help(outputStream* out, bool startup) out->print_cr(" The option redact-argument is best-effort and applies only to"); out->print_cr(" command-line arguments in the jdk.JVMInformation event and to"); out->print_cr(" the java.command system property in the jdk.InitialSystemProperty"); - out->print_cr(" event. Other events, such as jdk.ProcessStart (child processes),"); - out->print_cr(" are not redacted."); + out->print_cr(" event, and to matching command-line argument text in the values"); + out->print_cr(" of jdk.InitialEnvironmentVariable events. Other events, such as"); + out->print_cr(" jdk.ProcessStart (child processes), are not redacted."); out->print_cr(""); out->print_cr(" If the redact-argument option is not specified, the following"); out->print_cr(" filters are used by default:"); diff --git a/src/hotspot/share/jfr/periodic/jfrRedactedEvents.cpp b/src/hotspot/share/jfr/periodic/jfrRedactedEvents.cpp index 331c28cffa20..652320c99046 100644 --- a/src/hotspot/share/jfr/periodic/jfrRedactedEvents.cpp +++ b/src/hotspot/share/jfr/periodic/jfrRedactedEvents.cpp @@ -29,6 +29,7 @@ #include "logging/logMessage.hpp" #include "runtime/arguments.hpp" #include "runtime/flags/jvmFlag.hpp" +#include "runtime/javaThread.hpp" #include "runtime/os.hpp" #include "runtime/vm_version.hpp" #include "services/diagnosticArgument.hpp" @@ -46,17 +47,20 @@ using StringFlag = JfrRedactedEvents::StringFlag; using StringKeyValueArray = GrowableArray*; static const char REDACTED[] = "[REDACTED]"; +static const char REDACTED_MARKER = (char)0xFF; static const char DELIMITER[] = " "; +static const char REDACT_ARGUMENT[] = "redact-argument"; static const char REDACT_ARGUMENT_EQUAL[] = "redact-argument="; static const size_t REDACTED_LENGTH = sizeof(REDACTED) -1; static const size_t DELIMITER_LENGTH = sizeof(DELIMITER) -1; -static const size_t REDACT_ARGUMENT_EQUAL_LENGTH = sizeof(REDACT_ARGUMENT_EQUAL) -1; +static const size_t REDACT_ARGUMENT_LENGTH = sizeof(REDACT_ARGUMENT) -1; String* JfrRedactedEvents::_redacted_java_command_line = nullptr; String* JfrRedactedEvents::_redacted_jvm_command_line = nullptr; String* JfrRedactedEvents::_redacted_flags_command_line = nullptr; String* JfrRedactedEvents::_redacted_flight_recorder_options = nullptr; +String* JfrRedactedEvents::_redacted_flight_recorder_options_with_marker = nullptr; StringKeyValueArray JfrRedactedEvents::_initial_environment_variables = nullptr; StringKeyValueArray JfrRedactedEvents::_initial_system_properties = nullptr; @@ -71,6 +75,10 @@ bool JfrRedactedEvents::_initialized = false; bool JfrRedactedEvents::set_argument_filter(const char* filters) { assert (_argument_filters == nullptr, "invariant"); assert (filters != nullptr, "invariant"); + if (strcmp(filters, "*") != 0 && strcmp(filters, "none") != 0) { + _redacted_arguments = new StringArray(); + _redacted_arguments->add(filters); + } _argument_filters = new StringArray(); return append_filters(_argument_filters, true, filters); } @@ -139,9 +147,8 @@ bool JfrRedactedEvents::append_filters(StringArray* target, bool argument, const } if (filters[0] == '\0') { LogMessage(jfr, redact) msg; - msg.warning("Default redaction filters are replaced. Specify:"); - msg.warning("-XX:FlightRecorderOptions:%s=none to disable filters without a warning.", option_name); - return true; + msg.error("Specify -XX:FlightRecorderOptions:%s=none to disable filters completely.", option_name); + return false; } if (strcmp(filters, "none") == 0) { return true; @@ -187,6 +194,67 @@ char* JfrRedactedEvents::new_redacted_text() { return result; } +void JfrRedactedEvents::redact(String* scratch_string, const char* target, const String* redaction) { + if (strchr(redaction->text(), REDACTED_MARKER)) { + return; + } + const char* position = target; + while (true) { + const char* sensitive = strstr(position, redaction->text()); + if (sensitive == nullptr) { + return; + } + size_t index = (size_t)(sensitive - target); + for (size_t i = 0; i < redaction->length(); i++) { + scratch_string->set(index + i, REDACTED_MARKER); + } + position = sensitive + 1; + } +} + +String* JfrRedactedEvents::redact_environment_variable_value(const char* value) { + if (strchr(value, REDACTED_MARKER)) { + return new String(REDACTED); + } + bool changed = false; + String* input = new String(value); + if (_redacted_flight_recorder_options_with_marker != nullptr) { + size_t length = strlen(FlightRecorderOptions); + while (const char* start = strstr(input->text(), FlightRecorderOptions)) { + changed = true; + const char* end = start + length; + stringStream s; + s.write(input->text(), start - input->text()); + s.write(_redacted_flight_recorder_options_with_marker->text(), _redacted_flight_recorder_options_with_marker->length()); + s.write(end, strlen(end)); + String* result = new String(s.base()); + delete input; + input = result; + } + } + String* scratch_string = new String(input->text()); + for (int i = 0; i < _redacted_arguments->length(); i++) { + redact(scratch_string, input->text(), _redacted_arguments->at(i)); + } + stringStream result; + bool inside_redaction = false; + for (size_t i = 0; i < scratch_string->length(); i++) { + if (scratch_string->at(i) == REDACTED_MARKER) { + changed = true; + if (!inside_redaction) { + result.print(REDACTED); + } + inside_redaction = true; + } else { + result.put(scratch_string->at(i)); + inside_redaction = false; + } + } + delete scratch_string; + delete input; + return changed ? new String(result.base()) : nullptr; +} + bool JfrRedactedEvents::emit_initial_environment_variables(bool log) { if (_initial_environment_variables == nullptr) { ensure_initialized(); @@ -207,6 +275,16 @@ bool JfrRedactedEvents::emit_initial_environment_variables(bool log) { if (log) { log_debug(jfr, redact)("Redacted initial environment variable named '%s'", key->text()); } + } else { + String* redacted_value = redact_environment_variable_value(value); + if (redacted_value != nullptr) { + if (log) { + log_debug(jfr, redact)("Redacted argument in initial environment variable value named '%s'", key->text()); + } + _initial_environment_variables->append(new StringKeyValue(key, redacted_value->text())); + delete redacted_value; + continue; + } } _initial_environment_variables->append(new StringKeyValue(key, value)); } @@ -262,6 +340,9 @@ bool JfrRedactedEvents::match_flag(const char* flag_name, const char* arg) { if (flag_name == nullptr || arg == nullptr) { return false; } + if (strncmp(arg, "-XX:", 4) == 0) { + arg += 4; + } while (*flag_name) { if (*arg != *flag_name) { return false; @@ -346,6 +427,34 @@ void JfrRedactedEvents::emit_jvm_information(bool log) { } } +// Method assumes that FlightRecorderOptions has been successfully parsed during startup +String* JfrRedactedEvents::redact_flight_recorder_options(const char* option, bool marker) { + JavaThread* THREAD = JavaThread::current(); + const size_t length = strlen(option); + DCmdArgIter iterator(option, length, ','); + while (iterator.next(THREAD)) { + if (strncmp(iterator.key_addr(), REDACT_ARGUMENT, REDACT_ARGUMENT_LENGTH) == 0) { + const char* start = iterator.value_addr(); + const char* end = start + iterator.value_length(); + stringStream result; + result.write(option, start - option); + if (marker) { + result.put(REDACTED_MARKER); + } else { + result.write(REDACTED, REDACTED_LENGTH); + } + result.write(end, option + length - end); + return new String(result.base()); + } + } + if (HAS_PENDING_EXCEPTION) { + DEBUG_ONLY(ShouldNotReachHere();) + CLEAR_PENDING_EXCEPTION; + return new String(REDACTED); + } + return nullptr; +} + void JfrRedactedEvents::ensure_initialized() { if (_initialized) { return; @@ -359,31 +468,12 @@ void JfrRedactedEvents::ensure_initialized() { add_default_filters(_argument_filters, true); } if (FlightRecorderOptions != nullptr) { - if (strstr(FlightRecorderOptions, REDACT_ARGUMENT_EQUAL) != nullptr) { - DCmdIter iterator(FlightRecorderOptions, ','); - stringStream result; - size_t pos = 0; - while(iterator.has_next()) { - CmdLine line = iterator.next(); - const char* start = line.cmd_addr(); - if (strncmp(start, REDACT_ARGUMENT_EQUAL, REDACT_ARGUMENT_EQUAL_LENGTH) == 0) { - result.print(REDACT_ARGUMENT_EQUAL); - result.print(REDACTED); - // Preserve ',' if there are more tokens - pos = iterator.has_next() ? iterator.cursor() - 1 : iterator.cursor(); - } - while (pos < iterator.cursor()) { - result.write(FlightRecorderOptions + pos, 1); - pos++; - } - } - _redacted_flight_recorder_options = new String(result.base()); - } else { - _redacted_flight_recorder_options = new String(FlightRecorderOptions); - } + _redacted_flight_recorder_options = redact_flight_recorder_options(FlightRecorderOptions, false); + _redacted_flight_recorder_options_with_marker = redact_flight_recorder_options(FlightRecorderOptions, true); + } + if (_redacted_arguments == nullptr) { + _redacted_arguments = new StringArray(); } - - _redacted_arguments = new StringArray(); StringArray* java_args = make_java_args_array(); _redacted_java_command_line = redact_command_line(java_args); @@ -396,7 +486,6 @@ void JfrRedactedEvents::ensure_initialized() { StringArray* flags_args = make_jvm_args_array(Arguments::jvm_flags_array(), Arguments::num_jvm_flags()); _redacted_flags_command_line = redact_command_line(flags_args); delete flags_args; - _initialized = true; } @@ -422,8 +511,8 @@ String* JfrRedactedEvents::redact_command_line(StringArray* arguments) { for (int j = arg_index; j < next_index; j++) { result->add(REDACTED); const char* arg = arguments->at(j)->text(); - if (arg != nullptr && strncmp(arg, "-XX:", 4) == 0) { - _redacted_arguments->add(arg + 4); + if (arg != nullptr) { + _redacted_arguments->add(arg); } } arg_index = next_index; @@ -504,15 +593,23 @@ StringArray* JfrRedactedEvents::make_jvm_args_array(char** jvm_args_array, int a return nullptr; } StringArray* result = new StringArray(array_length); - for(int i = 0; i < array_length; i++) { + for (int i = 0; i < array_length; i++) { char* argument = jvm_args_array[i]; - if (_redacted_flight_recorder_options != nullptr && - strncmp(argument, "-XX:FlightRecorderOptions", 25) == 0) { - const char* text = _redacted_flight_recorder_options->text(); - size_t length = _redacted_flight_recorder_options->length(); - // Length must be at least 26 or the JVM will not start. - result->add(new String(argument, 26, text, length)); - continue; + if (strncmp(argument, "-XX:FlightRecorderOptions", 25) == 0) { + size_t length = strlen(argument); + if (length > 25 && + _redacted_flight_recorder_options != nullptr && + strcmp(argument + 26, FlightRecorderOptions) == 0) { + const char* text = _redacted_flight_recorder_options->text(); + // Length must be at least 26 or the JVM will not start. + result->add(new String(argument, 26, text, _redacted_flight_recorder_options->length())); + continue; + } + if (strstr(argument, REDACT_ARGUMENT_EQUAL) != nullptr) { + _redacted_arguments->add(argument); + result->add("-XX:FlightRecorderOptions:[REDACTED]"); + continue; + } } if (strncmp(argument, "-D", 2) == 0) { const char* key_start = argument + 2; @@ -523,6 +620,7 @@ StringArray* JfrRedactedEvents::make_jvm_args_array(char** jvm_args_array, int a bool redact = match_key(_key_filters, key_tmp->text()); delete key_tmp; if (redact) { + _redacted_arguments->add(argument); size_t unsensitive_length = (size_t)(eq - argument) + 1; result->add(new String(argument, unsensitive_length, REDACTED, REDACTED_LENGTH)); continue; diff --git a/src/hotspot/share/jfr/periodic/jfrRedactedEvents.hpp b/src/hotspot/share/jfr/periodic/jfrRedactedEvents.hpp index dc972190b6ce..c3d6fd32cbc1 100644 --- a/src/hotspot/share/jfr/periodic/jfrRedactedEvents.hpp +++ b/src/hotspot/share/jfr/periodic/jfrRedactedEvents.hpp @@ -195,6 +195,7 @@ class JfrRedactedEvents: public AllStatic { static String* _redacted_jvm_command_line; static String* _redacted_flags_command_line; static String* _redacted_flight_recorder_options; + static String* _redacted_flight_recorder_options_with_marker; static GrowableArray* _initial_system_properties; static GrowableArray* _initial_environment_variables; static GrowableArray* _string_flags; @@ -217,7 +218,10 @@ class JfrRedactedEvents: public AllStatic { static int match_arguments(StringArray* filter_array, StringArray* arguments, int arg_index); static bool match_key(StringArray* array, const char* text); static bool read_file(StringArray* target, const char* filename); + static void redact(String* scratch_string, const char* target, const String* redaction); + static String* redact_flight_recorder_options(const char* option, bool marker); static String* redact_command_line(StringArray* arguments); + static String* redact_environment_variable_value(const char* value); static StringArray* split(const char* text, char separator); }; diff --git a/src/java.base/share/man/java.md b/src/java.base/share/man/java.md index 624428a001b2..0a8b23d57b6d 100644 --- a/src/java.base/share/man/java.md +++ b/src/java.base/share/man/java.md @@ -1215,9 +1215,11 @@ These `java` options control the runtime behavior of the Java HotSpot VM. be replaced with `[REDACTED]`. The option `redact-argument` is best-effort and applies only to command-line arguments in the `jdk.JVMInformation` event and to the `java.command` system property in the - `jdk.InitialSystemProperty` event. Other events, such as `jdk.ProcessStart` - (child processes), are not redacted. Use `-XX:FlightRecorderOptions:help` - to see the default filters used by the `redact-argument` option. + `jdk.InitialSystemProperty` event, and to matching command-line argument + text in the values of `jdk.InitialEnvironmentVariable` events. Other + events, such as `jdk.ProcessStart` (child processes), are not redacted. + Use `-XX:FlightRecorderOptions:help` to see the default filters used by + the `redact-argument` option. `redact-key=`key-filter : Replace the value of environment variables and system properties diff --git a/test/jdk/jdk/jfr/startupargs/TestRedact.java b/test/jdk/jdk/jfr/startupargs/TestRedact.java index 2d96408a3f5c..f3f3a9e5fa6b 100644 --- a/test/jdk/jdk/jfr/startupargs/TestRedact.java +++ b/test/jdk/jdk/jfr/startupargs/TestRedact.java @@ -42,6 +42,7 @@ import jdk.jfr.consumer.RecordingFile; import jdk.test.lib.Asserts; import jdk.test.lib.jfr.CommonHelper; +import jdk.test.lib.Platform; import jdk.test.lib.process.OutputAnalyzer; import jdk.test.lib.process.ProcessTools; @@ -169,6 +170,7 @@ public static void main(String... args) throws Exception { testRedactKey(); testRedactArgument(); testRedactMultiple(); + testOptionVariable(); testWildcards(); testDefaults(); testRedactFile(); @@ -315,15 +317,6 @@ private static void testRedactFile() throws Exception { private static void testEmpty() throws Exception { var environment = Map.of("API_TOKEN", "Zebra1"); var properties = Map.of("API_KEY", "Zebra2"); - Execution e1 = run(environment, properties, - "-XX:FlightRecorderOptions:redact-key=,redact-argument=", "Zebra3" - ); - e1.output().shouldContain("Default redaction filters are replaced."); - e1.output().shouldContain("redact-key=none to disable filters without a warning"); - e1.output().shouldContain("redact-argument=none to disable filters without a warning"); - e1.assertUnredacted("Zebra1"); - e1.assertUnredacted("Zebra2"); - e1.assertUnredacted("Zebra3"); Execution e2 = run(environment, properties, "-XX:FlightRecorderOptions:redact-argument=none,redact-key=none", "Zebra3" @@ -377,6 +370,12 @@ private static void testRedactArgument() throws Exception { e.assertRedactedArgument("N4711"); e.assertRedactedArgument("Smith:abc123"); e.assertUnredacted("Banana"); + + String option = Platform.isWindows() ? + "-XX:FlightRecorderOptions:redact-argument='Foo,bar'" : + "-XX:FlightRecorderOptions:redact-argument=\"Foo,bar\""; + Execution e2 = run(option,"Foo,bar"); + e2.assertRedactedArgument("Foo,bar"); } private static void testRedactMultiple() throws Exception { @@ -390,6 +389,69 @@ private static void testRedactMultiple() throws Exception { e.assertRedactedArgument("Quz"); } + private static void testOptionVariable() throws Exception { + // Simulate shell expansion with the three options: + // SYSTEM_PROPS, JVM_OPTIONS and PROGRAM_OPTIONS + String systemProperty = "-Dsecret=apple"; + String jvmOption = "-XX:FlightRecorderOptions:stackdepth=32,redact-argument=+Aracuan"; + String programOption = "Aracuan"; + Execution e1 = run( + Map.of("SYSTEM_PROPS", systemProperty, + "JVM_OPTIONS", jvmOption, + "PROGRAM_OPTIONS", programOption), + Map.of("secret","apple"), + List.of(systemProperty, jvmOption), + programOption + ); + e1.assertRedactedKey("SYSTEM_PROPS"); + String redactedJVMOption = e1.environment.get("JVM_OPTIONS"); + if (!redactedJVMOption.equals("-XX:FlightRecorderOptions:stackdepth=32,redact-argument=[REDACTED]")) { + throw new Exception("Expected partial redaction for environment variable with -XX:FlightRecorderOptions:redact-argument="); + } + e1.assertRedactedKey("PROGRAM_OPTIONS"); + e1.assertRedactedKey("secret"); + e1.assertRedactedArgument("Aracuan"); + + Execution e2 = run( + Map.of("PROGRAM_OPTIONS", "BLUE RED GREEN GREDELINE"), + Map.of(), + "-XX:FlightRecorderOptions:redact-argument=+*red*", + "BLUE", "RED", "GREEN", "GREDELINE" + ); + String programOptions = e2.environment().get("PROGRAM_OPTIONS"); + if (!programOptions.equals("BLUE [REDACTED] GREEN [REDACTED]")) { + e2.print(); + throw new Exception("Missing redaction inside option variable"); + } + + Execution e3 = run( + Map.of("PROGRAM_OPTIONS", "ZEBRA FISH ZEBRACCOON FISH RACCOON"), + Map.of(), + "-XX:FlightRecorderOptions:redact-argument=+zebra;raccoon", + "ZEBRA", "FISH", "ZEBRACCOON", "FISH", "RACCOON" + ); + programOptions = e3.environment().get("PROGRAM_OPTIONS"); + if (!programOptions.equals("[REDACTED] FISH [REDACTED] FISH [REDACTED]")) { + e3.print(); + throw new Exception("Incorrect redaction when option arguments overlap"); + } + + String option1 = "-XX:FlightRecorderOptions:redact-argument=Zebra,gibberish=,,,"; + String option2 = "-XX:FlightRecorderOptions:redact-argument=Tiger"; + Execution e4 = run( + Map.of("MY_JVM_OPTIONS", option1 + " " + option2), + Map.of(), + List.of(option1, option2), + "TIGER" + ); + e4.assertRedactedArgument("TIGER"); + String redacted = e4.environment().get("MY_JVM_OPTIONS"); + if (!redacted.equals("[REDACTED] -XX:FlightRecorderOptions:redact-argument=[REDACTED]")) { + e4.print(); + throw new Exception("Incorrect redaction with multiple options in environment variables"); + } + } + private static void testRedactKey() throws Exception { Execution e = run( Map.of("cart", "wheel", "banana", "split", "rose", "bud"), @@ -407,14 +469,20 @@ private static Execution run(String options, String... args) throws Exception { return run(Map.of(), Map.of(), options, args); } - private static Execution run(Map environment, Map properties, String options, String... args) throws Exception { + private static Execution run(Map environment, Map properties, String option, String... args) throws Exception { + return run(environment, properties, List.of(option), args); + } + + private static Execution run(Map environment, Map properties, List options, String... args) throws Exception { List arguments = new ArrayList<>(); Path file = Path.of("file.jfr"); for (var entry : properties.entrySet()) { arguments.add("-D" + entry.getKey() + "=" + entry.getValue()); } arguments.add("-XX:StartFlightRecording:filename=" + file.toAbsolutePath().toString()); - arguments.add(options); + for (String option : options) { + arguments.add(option); + } arguments.add("jdk.jfr.startupargs.Application"); arguments.addAll(Arrays.asList(args)); From 9abdb0861b6914d8987ad7234738f3db8ff72c2b Mon Sep 17 00:00:00 2001 From: Artur Barashev Date: Thu, 8 Jan 2026 19:08:20 +0000 Subject: [PATCH 03/17] 8373275: Improve DTLS handshaking Reviewed-by: rhalade, pkumaraswamy, ahgross, jnibedita, jnimeh, mullan --- .../sun/security/ssl/HelloCookieManager.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/java.base/share/classes/sun/security/ssl/HelloCookieManager.java b/src/java.base/share/classes/sun/security/ssl/HelloCookieManager.java index b3155f5170a8..4268b9779bd8 100644 --- a/src/java.base/share/classes/sun/security/ssl/HelloCookieManager.java +++ b/src/java.base/share/classes/sun/security/ssl/HelloCookieManager.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -26,6 +26,7 @@ package sun.security.ssl; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -121,6 +122,7 @@ abstract boolean isCookieValid(ServerHandshakeContext context, private static final class D10HelloCookieManager extends HelloCookieManager { + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; final SecureRandom secureRandom; private int cookieVersion; // allow to wrap, version + sequence private final byte[] cookieSecret; @@ -170,6 +172,7 @@ byte[] createCookie(ServerHandshakeContext context, } byte[] helloBytes = clientHello.getHelloCookieBytes(); md.update(helloBytes); + md.update(getHostPortBytes(context)); byte[] cookie = md.digest(secret); // 32 bytes cookie[0] = (byte)((version >> 24) & 0xFF); @@ -205,11 +208,30 @@ boolean isCookieValid(ServerHandshakeContext context, } byte[] helloBytes = clientHello.getHelloCookieBytes(); md.update(helloBytes); + md.update(getHostPortBytes(context)); byte[] target = md.digest(secret); // 32 bytes target[0] = cookie[0]; return MessageDigest.isEqual(target, cookie); } + + /** + * Returns host and port bytes if those are set. + * Using ASCII unit separator character to separate host and port so we + * can differentiate between otherwise identical host and port string + * concatenations, for example host 172.0.0.1 with port 25 and host + * 172.0.0.12 with port 5. + */ + private static byte[] getHostPortBytes(ServerHandshakeContext context) { + final String host = context.conContext.transport.getPeerHost(); + final int port = context.conContext.transport.getPeerPort(); + final String hostStr = host != null ? host : ""; + final String portStr = port > -1 ? Integer.toString(port) : ""; + return hostStr.isEmpty() && portStr.isEmpty() ? + EMPTY_BYTE_ARRAY : + (hostStr + '\u001F' + portStr).getBytes( + StandardCharsets.UTF_8); + } } private static final From 4229dd0123d778f4de94d25b64a65ed99fe5a834 Mon Sep 17 00:00:00 2001 From: Bradford Wetmore Date: Wed, 14 Jan 2026 20:54:56 +0000 Subject: [PATCH 04/17] 8368041: Enhance TLS certificate handling Reviewed-by: jnimeh, abarashev, hchao, djelinski, ksreenath, ahgross, rhalade --- .../share/classes/sun/security/ssl/Alert.java | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) diff --git a/src/java.base/share/classes/sun/security/ssl/Alert.java b/src/java.base/share/classes/sun/security/ssl/Alert.java index e9588a09b3d8..c081ec5f747e 100644 --- a/src/java.base/share/classes/sun/security/ssl/Alert.java +++ b/src/java.base/share/classes/sun/security/ssl/Alert.java @@ -181,11 +181,13 @@ private static final class AlertMessage { AlertMessage(TransportContext context, ByteBuffer m) throws IOException { - // From RFC 8446 "Implementations - // MUST NOT send Handshake and Alert records that have a zero-length - // TLSInnerPlaintext.content; if such a message is received, the - // receiving implementation MUST terminate the connection with an - // "unexpected_message" alert." + + // From RFC 8446: TLSv1.3 + // + // Implementations MUST NOT send Handshake and Alert records that + // have a zero-length TLSInnerPlaintext.content; if such a message + // is received, the receiving implementation MUST terminate the + // connection with an "unexpected_message" alert. if (m.remaining() == 0) { throw context.fatal(Alert.UNEXPECTED_MESSAGE, "Alert fragments must not be zero length."); @@ -264,27 +266,39 @@ public void consume(ConnectionContext context, } else if ((level == Level.WARNING) && (alert != null)) { // Terminate the connection if an alert with a level of warning // is received during handshaking, except the no_certificate - // warning. - if (alert.handshakeOnly && (tc.handshakeContext != null)) { - // It's OK to get a no_certificate alert from a client of - // which we requested client authentication. However, - // if we required it, then this is not acceptable. - if (tc.sslConfig.isClientMode || - alert != Alert.NO_CERTIFICATE || - (tc.sslConfig.clientAuthType != + // warning for SSLv3. + HandshakeContext hc = tc.handshakeContext; + if (alert.handshakeOnly && (hc != null)) { + // In SSLv3, it's OK to get a no_certificate alert from a + // client where we requested (want) client authentication. + // If we required it (need), this is not acceptable + // and must fail. + // + // no_certificate alerts are not acceptable in TLSv1.*. + // + if (!tc.sslConfig.isClientMode && + (hc.negotiatedProtocol == ProtocolVersion.SSL30) && + (alert == Alert.NO_CERTIFICATE) && + (tc.sslConfig.clientAuthType == ClientAuthType.CLIENT_AUTH_REQUESTED)) { - throw tc.fatal(Alert.HANDSHAKE_FAILURE, - "received handshake warning: " + alert.description); - } else { - // Otherwise, ignore the warning but remove the - // Certificate and CertificateVerify handshake - // consumer so the state machine doesn't expect it. - tc.handshakeContext.handshakeConsumers.remove( - SSLHandshake.CERTIFICATE.id); - tc.handshakeContext.handshakeConsumers.remove( + + // We'll ignore the warning and remove the Certificate, + // CompressedCertificate and CertificateVerify handshake + // consumers so the state machine isn't expecting them. + if (hc.handshakeConsumers.remove( + SSLHandshake.CERTIFICATE.id) != null) { + hc.handshakeConsumers.remove( SSLHandshake.COMPRESSED_CERTIFICATE.id); - tc.handshakeContext.handshakeConsumers.remove( + hc.handshakeConsumers.remove( SSLHandshake.CERTIFICATE_VERIFY.id); + } else { + throw tc.fatal(Alert.HANDSHAKE_FAILURE, + "NO_CERTIFICATE alert received when certs" + + " were not expected or already received"); + } + } else { + throw tc.fatal(Alert.HANDSHAKE_FAILURE, + "Received handshake warning: " + alert.description); } } // Otherwise, ignore the warning } else { // fatal or unknown From f5f5e1e228133dcbff56073ae7e9509caf72f341 Mon Sep 17 00:00:00 2001 From: Volkan Yazici Date: Mon, 2 Mar 2026 10:00:34 +0000 Subject: [PATCH 05/17] 8377498: Improve HttpServer handling Reviewed-by: dfuchs --- .../sun/net/httpserver/ServerImpl.java | 11 +++++++++- .../simpleserver/FileServerHandler.java | 22 ++++++++++--------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java index 94fe78b9c647..67f6b08a2636 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/ServerImpl.java @@ -768,15 +768,24 @@ public void run() { requestLine, "Bad request line"); return; } + + // Read the request URI String uriStr = requestLine.substring(start, space); + // Reject ambiguous URIs + if (uriStr.startsWith("//")) { + reject(Code.HTTP_BAD_REQUEST, + requestLine, "Bad request URI"); + return; + } URI uri; try { uri = new URI(uriStr); } catch (URISyntaxException e3) { reject(Code.HTTP_BAD_REQUEST, - requestLine, "URISyntaxException thrown"); + requestLine, "Bad request URI"); return; } + start = space+1; String version = requestLine.substring(start); Headers headers = req.headers(); diff --git a/src/jdk.httpserver/share/classes/sun/net/httpserver/simpleserver/FileServerHandler.java b/src/jdk.httpserver/share/classes/sun/net/httpserver/simpleserver/FileServerHandler.java index cbf032e83985..08ea357b7f4b 100644 --- a/src/jdk.httpserver/share/classes/sun/net/httpserver/simpleserver/FileServerHandler.java +++ b/src/jdk.httpserver/share/classes/sun/net/httpserver/simpleserver/FileServerHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -106,16 +106,22 @@ private void handleGET(HttpExchange exchange, Path path) throws IOException { private void handleSupportedMethod(HttpExchange exchange, Path path, boolean writeBody) throws IOException { + boolean requestURIEndsWithSlash = pathEndsWithSlash(exchange); if (Files.isDirectory(path)) { - if (missingSlash(exchange)) { + if (!requestURIEndsWithSlash) { handleMovedPermanently(exchange); return; } - if (indexFile(path) != null) { - serveFile(exchange, indexFile(path), writeBody); + Path indexFile = indexFile(path); + if (indexFile != null) { + serveFile(exchange, indexFile, writeBody); } else { listFiles(exchange, path, writeBody); } + } + // Disallow non-directory paths ending with slash + else if (requestURIEndsWithSlash) { + handleNotFound(exchange); } else { serveFile(exchange, path, writeBody); } @@ -126,10 +132,6 @@ private void handleMovedPermanently(HttpExchange exchange) throws IOException { exchange.sendResponseHeaders(301, RSPBODY_EMPTY); } - private void handleForbidden(HttpExchange exchange) throws IOException { - exchange.sendResponseHeaders(403, RSPBODY_EMPTY); - } - private void handleNotFound(HttpExchange exchange) throws IOException { String fileNotFound = ResourceBundleHelper.getMessage("html.not.found"); var bytes = (openHTML @@ -161,8 +163,8 @@ private String getRedirectURI(URI uri) { return query == null ? redirectPath : redirectPath + "?" + query; } - private static boolean missingSlash(HttpExchange exchange) { - return !exchange.getRequestURI().getPath().endsWith("/"); + private static boolean pathEndsWithSlash(HttpExchange exchange) { + return exchange.getRequestURI().getPath().endsWith("/"); } private static String contextPath(HttpExchange exchange) { From 203517564e1380ce8f80ae21c7196a5f8f0e38fc Mon Sep 17 00:00:00 2001 From: Jayathirth D V Date: Wed, 4 Mar 2026 17:01:12 +0000 Subject: [PATCH 06/17] 8374058: Enhance JPEG handling Reviewed-by: mschoene, rhalade, psadhukhan, prr --- .../share/native/libjavajpeg/imageioJPEG.c | 395 ++++++++---------- .../jpeg/LargeJpegReadWithProgressBench.java | 166 ++++++++ .../plugins/jpeg/LargeJpegReadWriteBench.java | 141 +++++++ 3 files changed, 472 insertions(+), 230 deletions(-) create mode 100644 test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWithProgressBench.java create mode 100644 test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWriteBench.java diff --git a/src/java.desktop/share/native/libjavajpeg/imageioJPEG.c b/src/java.desktop/share/native/libjavajpeg/imageioJPEG.c index a764eb1ae3bb..ac37ad8eab6f 100644 --- a/src/java.desktop/share/native/libjavajpeg/imageioJPEG.c +++ b/src/java.desktop/share/native/libjavajpeg/imageioJPEG.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2000, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -120,6 +120,7 @@ typedef struct streamBufferStruct { size_t bufferLength; // Allocated, nut just used int suspendable; // Set to true to suspend input long remaining_skip; // Used only on input + jboolean isCopy; // GetByteArrayElements copied/pinned the Java array } streamBuffer, *streamBufferPtr; /* @@ -200,7 +201,8 @@ static void destroyStreamBuffer(JNIEnv *env, streamBufferPtr sb) { // Forward reference static void unpinStreamBuffer(JNIEnv *env, streamBufferPtr sb, - const JOCTET *next_byte); + const JOCTET *next_byte, + int streamReleaseMode); /* * Resets the state of a streamBuffer object that has been in use. * The global reference to the stream is released, but the reference @@ -212,15 +214,16 @@ static void resetStreamBuffer(JNIEnv *env, streamBufferPtr sb) { (*env)->DeleteWeakGlobalRef(env, sb->ioRef); sb->ioRef = NULL; } - unpinStreamBuffer(env, sb, NULL); + unpinStreamBuffer(env, sb, NULL, JNI_ABORT); sb->bufferOffset = NO_DATA; sb->suspendable = FALSE; sb->remaining_skip = 0; } /* - * Pins the data buffer associated with this stream. Returns OK on - * success, NOT_OK on failure, as GetPrimitiveArrayCritical may fail. + * Pins/copies the data buffer associated with this stream. Returns OK on + * success, NOT_OK on failure, as GetByteArrayElements + * may fail. */ static int pinStreamBuffer(JNIEnv *env, streamBufferPtr sb, @@ -228,9 +231,9 @@ static int pinStreamBuffer(JNIEnv *env, if (sb->hstreamBuffer != NULL) { assert(sb->buf == NULL); sb->buf = - (JOCTET *)(*env)->GetPrimitiveArrayCritical(env, - sb->hstreamBuffer, - NULL); + (JOCTET *)(*env)->GetByteArrayElements(env, + sb->hstreamBuffer, + &sb->isCopy); if (sb->buf == NULL) { return NOT_OK; } @@ -242,11 +245,12 @@ static int pinStreamBuffer(JNIEnv *env, } /* - * Unpins the data buffer associated with this stream. + * Unpins/releases the data buffer associated with this stream. */ static void unpinStreamBuffer(JNIEnv *env, streamBufferPtr sb, - const JOCTET *next_byte) { + const JOCTET *next_byte, + int streamReleaseMode) { if (sb->buf != NULL) { assert(sb->hstreamBuffer != NULL); if (next_byte == NULL) { @@ -254,11 +258,13 @@ static void unpinStreamBuffer(JNIEnv *env, } else { sb->bufferOffset = next_byte - sb->buf; } - (*env)->ReleasePrimitiveArrayCritical(env, - sb->hstreamBuffer, - sb->buf, - 0); - sb->buf = NULL; + (*env)->ReleaseByteArrayElements(env, + sb->hstreamBuffer, + (jbyte *)sb->buf, + streamReleaseMode); + if (streamReleaseMode != JNI_COMMIT) { + sb->buf = NULL; + } } } @@ -276,6 +282,7 @@ static void clearStreamBuffer(streamBufferPtr sb) { typedef struct pixelBufferStruct { jobject hpixelObject; // Usually a DataBuffer bank as a byte array unsigned int byteBufferLength; + jboolean isCopy; // GetByteArrayElements copied/pinned the Java array union pixptr { INT32 *ip; // Pinned buffer pointer, as 32-bit ints unsigned char *bp; // Pinned buffer pointer, as bytes @@ -309,7 +316,7 @@ static int setPixelBuffer(JNIEnv *env, pixelBufferPtr pb, jobject obj) { } // Forward reference -static void unpinPixelBuffer(JNIEnv *env, pixelBufferPtr pb); +static void unpinPixelBuffer(JNIEnv *env, pixelBufferPtr pb, int pixelReleaseMode); /* * Resets a pixel buffer to its initial state. Unpins any pixel buffer, @@ -318,7 +325,7 @@ static void unpinPixelBuffer(JNIEnv *env, pixelBufferPtr pb); */ static void resetPixelBuffer(JNIEnv *env, pixelBufferPtr pb) { if (pb->hpixelObject != NULL) { - unpinPixelBuffer(env, pb); + unpinPixelBuffer(env, pb, JNI_ABORT); (*env)->DeleteGlobalRef(env, pb->hpixelObject); pb->hpixelObject = NULL; pb->byteBufferLength = 0; @@ -326,13 +333,13 @@ static void resetPixelBuffer(JNIEnv *env, pixelBufferPtr pb) { } /* - * Pins the data buffer. Returns OK on success, NOT_OK on failure. + * Pins/copies the data buffer. Returns OK on success, NOT_OK on failure. */ static int pinPixelBuffer(JNIEnv *env, pixelBufferPtr pb) { if (pb->hpixelObject != NULL) { assert(pb->buf.ip == NULL); - pb->buf.bp = (unsigned char *)(*env)->GetPrimitiveArrayCritical - (env, pb->hpixelObject, NULL); + pb->buf.bp = (unsigned char *)(*env)->GetByteArrayElements + (env, pb->hpixelObject, &pb->isCopy); if (pb->buf.bp == NULL) { return NOT_OK; } @@ -341,17 +348,19 @@ static int pinPixelBuffer(JNIEnv *env, pixelBufferPtr pb) { } /* - * Unpins the data buffer. + * Unpins/releases the pixel buffer. */ -static void unpinPixelBuffer(JNIEnv *env, pixelBufferPtr pb) { +static void unpinPixelBuffer(JNIEnv *env, pixelBufferPtr pb, int pixelReleaseMode) { if (pb->buf.ip != NULL) { assert(pb->hpixelObject != NULL); - (*env)->ReleasePrimitiveArrayCritical(env, - pb->hpixelObject, - pb->buf.ip, - 0); - pb->buf.ip = NULL; + (*env)->ReleaseByteArrayElements(env, + pb->hpixelObject, + (jbyte *)pb->buf.ip, + pixelReleaseMode); + if (pixelReleaseMode != JNI_COMMIT) { + pb->buf.ip = NULL; + } } } @@ -468,34 +477,28 @@ static j_common_ptr destroyImageioData(JNIEnv *env, imageIODataPtr data) { /******************** Java array pinning and unpinning *****************/ -/* We use Get/ReleasePrimitiveArrayCritical functions to avoid - * the need to copy array elements for the above two objects. - * - * MAKE SURE TO: - * - * - carefully insert pairs of RELEASE_ARRAYS and GET_ARRAYS around - * callbacks to Java. - * - call RELEASE_ARRAYS before returning to Java. - * - * Otherwise things will go horribly wrong. There may be memory leaks, - * excessive pinning, or even VM crashes! - * - * Note that GetPrimitiveArrayCritical may fail! +/* + * We use Get/ReleaseByteArrayElements functions for access stream + * and pixel information from Java level arrays. + * If we receive reference to copy of Java array make sure you update + * Java array also when the latest information is needed at Java level. + * Also we use specific release modes for performance optimizations. */ /* - * Release (unpin) all the arrays in use during a read. + * Release (unpin) both stream and pixel arrays. */ -static void RELEASE_ARRAYS(JNIEnv *env, imageIODataPtr data, const JOCTET *next_byte) +static void RELEASE_ARRAYS(JNIEnv *env, imageIODataPtr data, const JOCTET *next_byte, + int streamReleaseMode, int pixelReleaseMode) { - unpinStreamBuffer(env, &data->streamBuf, next_byte); + unpinStreamBuffer(env, &data->streamBuf, next_byte, streamReleaseMode); - unpinPixelBuffer(env, &data->pixelBuf); + unpinPixelBuffer(env, &data->pixelBuf, pixelReleaseMode); } /* - * Get (pin) all the arrays in use during a read. + * Get (pin) both stream and pixel arrays. */ static int GET_ARRAYS(JNIEnv *env, imageIODataPtr data, const JOCTET **next_byte) { if (pinStreamBuffer(env, &data->streamBuf, next_byte) == NOT_OK) { @@ -503,7 +506,7 @@ static int GET_ARRAYS(JNIEnv *env, imageIODataPtr data, const JOCTET **next_byte } if (pinPixelBuffer(env, &data->pixelBuf) == NOT_OK) { - RELEASE_ARRAYS(env, data, *next_byte); + RELEASE_ARRAYS(env, data, *next_byte, JNI_ABORT, JNI_ABORT); return NOT_OK; } return OK; @@ -570,26 +573,16 @@ sun_jpeg_output_message (j_common_ptr cinfo) theObject = data->imageIOobj; if (cinfo->is_decompressor) { - struct jpeg_source_mgr *src = ((j_decompress_ptr)cinfo)->src; - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, theObject, JPEGImageReader_warningWithMessageID, string); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit(cinfo); - } } else { - struct jpeg_destination_mgr *dest = ((j_compress_ptr)cinfo)->dest; - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); (*env)->CallVoidMethod(env, theObject, JPEGImageWriter_warningWithMessageID, string); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, - (const JOCTET **)(&dest->next_output_byte))) { - cinfo->err->error_exit(cinfo); - } + } + if ((*env)->ExceptionCheck(env)) { + cinfo->err->error_exit(cinfo); } } @@ -941,7 +934,7 @@ imageio_fill_input_buffer(j_decompress_ptr cinfo) #ifdef DEBUG_IIO_JPEG printf("Filling input buffer, remaining skip is %ld, ", sb->remaining_skip); - printf("Buffer length is %d\n", sb->bufferLength); + printf("Buffer length is %zu\n", sb->bufferLength); #endif /* @@ -956,8 +949,15 @@ imageio_fill_input_buffer(j_decompress_ptr cinfo) /* * Now fill a complete buffer, or as much of one as the stream * will give us if we are near the end. + * + * The native copy of java array is not valid anymore so we just + * release it and get new copy, if we don't have native copy we rely + * on JVM to maintain the pinned handle of java array. */ - RELEASE_ARRAYS(env, data, src->next_input_byte); + jboolean isCopy = sb->isCopy; + if (isCopy) { + unpinStreamBuffer(env, &data->streamBuf, src->next_input_byte, JNI_ABORT); + } GET_IO_REF(input); @@ -969,9 +969,12 @@ imageio_fill_input_buffer(j_decompress_ptr cinfo) if ((ret > 0) && ((unsigned int)ret > sb->bufferLength)) { ret = (int)sb->bufferLength; } - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); + + if ((*env)->ExceptionCheck(env) || + (isCopy && (pinStreamBuffer(env, + &data->streamBuf, + &(src->next_input_byte)) == NOT_OK))) { + cinfo->err->error_exit((j_common_ptr) cinfo); } #ifdef DEBUG_IIO_JPEG @@ -988,12 +991,10 @@ imageio_fill_input_buffer(j_decompress_ptr cinfo) #ifdef DEBUG_IIO_JPEG printf("YO! Early EOI! ret = %d\n", ret); #endif - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, reader, JPEGImageReader_warningOccurredID, READ_NO_EOI); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } @@ -1008,97 +1009,6 @@ imageio_fill_input_buffer(j_decompress_ptr cinfo) return TRUE; } -/* - * With I/O suspension turned on, the JPEG library requires that all - * buffer filling be done at the top application level, using this - * function. Due to the way that backtracking works, this procedure - * saves all of the data that was left in the buffer when suspension - * occurred and read new data only at the end. - */ - -GLOBAL(void) -imageio_fill_suspended_buffer(j_decompress_ptr cinfo) -{ - struct jpeg_source_mgr *src = cinfo->src; - imageIODataPtr data = (imageIODataPtr) cinfo->client_data; - streamBufferPtr sb = &data->streamBuf; - JNIEnv *env = (JNIEnv *)JNU_GetEnv(the_jvm, JNI_VERSION_1_2); - jint ret; - size_t offset, buflen; - jobject input = NULL; - - /* - * The original (jpegdecoder.c) had code here that called - * InputStream.available and just returned if the number of bytes - * available was less than any remaining skip. Presumably this was - * to avoid blocking, although the benefit was unclear, as no more - * decompression can take place until more data is available, so - * the code would block on input a little further along anyway. - * ImageInputStreams don't have an available method, so we'll just - * block in the skip if we have to. - */ - - if (sb->remaining_skip) { - src->skip_input_data(cinfo, 0); - } - - /* Save the data currently in the buffer */ - offset = src->bytes_in_buffer; - if (src->next_input_byte > sb->buf) { - memcpy(sb->buf, src->next_input_byte, offset); - } - - - RELEASE_ARRAYS(env, data, src->next_input_byte); - - GET_IO_REF(input); - - buflen = sb->bufferLength - offset; - if (buflen <= 0) { - if (!GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); - } - RELEASE_ARRAYS(env, data, src->next_input_byte); - return; - } - - ret = (*env)->CallIntMethod(env, input, - JPEGImageReader_readInputDataID, - sb->hstreamBuffer, - offset, buflen); - if ((ret > 0) && ((unsigned int)ret > buflen)) ret = (int)buflen; - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); - } - /* - * If we have reached the end of the stream, then the EOI marker - * is missing. We accept such streams but generate a warning. - * The image is likely to be corrupted, though everything through - * the end of the last complete MCU should be usable. - */ - if (ret <= 0) { - jobject reader = data->imageIOobj; - RELEASE_ARRAYS(env, data, src->next_input_byte); - (*env)->CallVoidMethod(env, reader, - JPEGImageReader_warningOccurredID, - READ_NO_EOI); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); - } - - sb->buf[offset] = (JOCTET) 0xFF; - sb->buf[offset + 1] = (JOCTET) JPEG_EOI; - ret = 2; - } - - src->next_input_byte = sb->buf; - src->bytes_in_buffer = ret + offset; - - return; -} - /* * Skip num_bytes worth of data. The buffer pointer and count are * advanced over num_bytes input bytes, using the input stream @@ -1160,16 +1070,13 @@ imageio_skip_input_data(j_decompress_ptr cinfo, long num_bytes) return; } - RELEASE_ARRAYS(env, data, src->next_input_byte); - GET_IO_REF(input); ret = (*env)->CallLongMethod(env, input, JPEGImageReader_skipInputBytesID, (jlong) num_bytes); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } @@ -1181,15 +1088,12 @@ imageio_skip_input_data(j_decompress_ptr cinfo, long num_bytes) */ if (ret <= 0) { reader = data->imageIOobj; - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, reader, JPEGImageReader_warningOccurredID, READ_NO_EOI); - - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); + if ((*env)->ExceptionCheck(env)) { + cinfo->err->error_exit((j_common_ptr) cinfo); } sb->buf[0] = (JOCTET) 0xFF; sb->buf[1] = (JOCTET) JPEG_EOI; @@ -1215,7 +1119,7 @@ imageio_term_source(j_decompress_ptr cinfo) JNIEnv *env = (JNIEnv *)JNU_GetEnv(the_jvm, JNI_VERSION_1_2); jobject reader = data->imageIOobj; if (src->bytes_in_buffer > 0) { - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, 0); (*env)->CallVoidMethod(env, reader, JPEGImageReader_pushBackID, @@ -1659,7 +1563,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader if (setjmp(jerr->setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error while reading the header. */ - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, JNI_ABORT); if (!(*env)->ExceptionCheck(env)) { char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message) ((struct jpeg_common_struct *) cinfo, @@ -1678,7 +1582,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader (*env)->ExceptionClear(env); JNU_ThrowByName(env, "javax/imageio/IIOException", - "Array pin failed"); + "Get array elements failed"); return retval; } @@ -1701,7 +1605,11 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader printf("just read tables-only image; q table 0 at %p\n", cinfo->quant_tbl_ptrs[0]); #endif - RELEASE_ARRAYS(env, data, src->next_input_byte); + /* + * readImageHeader can be called independently, so + * we release the arrays when we return back. + */ + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, 0); } else { /* * Now adjust the jpeg_color_space variable, which was set in @@ -1802,7 +1710,6 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader /* Leave the output space as CMYK */ } } - RELEASE_ARRAYS(env, data, src->next_input_byte); /* read icc profile data */ profileData = read_icc_profile(env, cinfo); @@ -1819,14 +1726,17 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImageHeader cinfo->out_color_space, cinfo->num_components, profileData); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } if (reset) { jpeg_abort_decompress(cinfo); } - RELEASE_ARRAYS(env, data, src->next_input_byte); + /* + * readImageHeader can be called independently, so + * we release the arrays when we return back. + */ + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, 0); } return retval; @@ -1987,7 +1897,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage if (setjmp(jerr->setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error while reading. */ - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, JNI_ABORT); if (!(*env)->ExceptionCheck(env)) { char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message) ((struct jpeg_common_struct *) cinfo, @@ -2005,7 +1915,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage (*env)->ExceptionClear(env); JNU_ThrowByName(env, "javax/imageio/IIOException", - "Array pin failed"); + "Get array elements failed"); return data->abortFlag; } @@ -2037,7 +1947,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage jpeg_start_decompress(cinfo); if (numBands != cinfo->output_components) { - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, JNI_ABORT); JNU_ThrowByName(env, "javax/imageio/IIOException", "Invalid argument to native readImage"); return data->abortFlag; @@ -2046,7 +1956,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage if (cinfo->output_components <= 0 || cinfo->image_width > (0xffffffffu / (unsigned int)cinfo->output_components)) { - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, JNI_ABORT); JNU_ThrowByName(env, "javax/imageio/IIOException", "Invalid number of output components"); return data->abortFlag; @@ -2055,7 +1965,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage // Allocate a 1-scanline buffer scanLinePtr = (JSAMPROW)malloc(cinfo->image_width*cinfo->output_components); if (scanLinePtr == NULL) { - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, JNI_ABORT); JNU_ThrowByName( env, "java/lang/OutOfMemoryError", "Reading JPEG Stream"); @@ -2070,22 +1980,18 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage // the first interesting pass. jpeg_start_output(cinfo, cinfo->input_scan_number); if (wantUpdates) { - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, this, JPEGImageReader_passStartedID, cinfo->input_scan_number-1); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } } } else if (wantUpdates) { - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, this, JPEGImageReader_passStartedID, 0); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } } @@ -2136,16 +2042,20 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage } } - // And call it back to Java - RELEASE_ARRAYS(env, data, src->next_input_byte); + /* + * Optimisation to just commit the native pixel buffer + * content back to java array without releasing the + * native buffer. + */ + if (pb->isCopy) { + unpinPixelBuffer(env, pb, JNI_COMMIT); + } (*env)->CallVoidMethod(env, this, JPEGImageReader_acceptPixelsID, targetLine++, progressive); - - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } @@ -2175,11 +2085,9 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage done = TRUE; } if (wantUpdates) { - RELEASE_ARRAYS(env, data, src->next_input_byte); (*env)->CallVoidMethod(env, this, JPEGImageReader_passCompleteID); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, &(src->next_input_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } } @@ -2204,13 +2112,16 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageReader_readImage this, JPEGImageReader_skipPastImageID, imageIndex); + if ((*env)->ExceptionCheck(env)) { + cinfo->err->error_exit((j_common_ptr) cinfo); + } } else { jpeg_finish_decompress(cinfo); } free(scanLinePtr); - RELEASE_ARRAYS(env, data, src->next_input_byte); + RELEASE_ARRAYS(env, data, src->next_input_byte, JNI_ABORT, 0); return data->abortFlag; } @@ -2405,8 +2316,16 @@ imageio_empty_output_buffer (j_compress_ptr cinfo) JNIEnv *env = (JNIEnv *)JNU_GetEnv(the_jvm, JNI_VERSION_1_2); jobject output = NULL; - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); - + /* + * Optimization to not delete the native copy of stream buffer, + * but just commit the content back to the Java array. + * In case where we don't have a copy, we rely on JVM to maintain + * the native reference of Java array. + */ + jboolean isCopy = sb->isCopy; + if (isCopy) { + unpinStreamBuffer(env, sb, dest->next_output_byte, JNI_COMMIT); + } GET_IO_REF(output); (*env)->CallVoidMethod(env, @@ -2415,10 +2334,8 @@ imageio_empty_output_buffer (j_compress_ptr cinfo) sb->hstreamBuffer, 0, sb->bufferLength); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, - (const JOCTET **)(&dest->next_output_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); + if ((*env)->ExceptionCheck(env)) { + cinfo->err->error_exit((j_common_ptr) cinfo); } dest->next_output_byte = sb->buf; @@ -2447,7 +2364,16 @@ imageio_term_destination (j_compress_ptr cinfo) if (datacount != 0) { jobject output = NULL; - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + /* + * Optimization to not delete the native copy of stream buffer, + * but just commit the content back to the Java array. + * In case where we don't have a copy, we rely on JVM to maintain + * the native reference of Java array. + */ + jboolean isCopy = sb->isCopy; + if (isCopy) { + unpinStreamBuffer(env, sb, dest->next_output_byte, JNI_COMMIT); + } GET_IO_REF(output); @@ -2457,17 +2383,13 @@ imageio_term_destination (j_compress_ptr cinfo) sb->hstreamBuffer, 0, datacount); - - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, - (const JOCTET **)(&dest->next_output_byte))) { + if ((*env)->ExceptionCheck(env)) { cinfo->err->error_exit((j_common_ptr) cinfo); } } dest->next_output_byte = NULL; dest->free_in_buffer = 0; - } /* @@ -2668,7 +2590,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeTables if (setjmp(jerr->setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error while writing. */ - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte), JNI_ABORT, JNI_ABORT); if (!(*env)->ExceptionCheck(env)) { char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message) ((j_common_ptr) cinfo, @@ -2683,7 +2605,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeTables (*env)->ExceptionClear(env); JNU_ThrowByName(env, "javax/imageio/IIOException", - "Array pin failed"); + "Get array elements failed"); return; } @@ -2703,7 +2625,15 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeTables } jpeg_write_tables(cinfo); // Flushes the buffer for you - RELEASE_ARRAYS(env, data, NULL); + /* + * writeTables can be called independently, so + * we release the arrays when we return back. + * Also the table content in output_buffer is + * already flushed, so no need to commit the + * native copy of stream content back to the + * Java array. + */ + RELEASE_ARRAYS(env, data, NULL, JNI_ABORT, 0); } static void freeArray(UINT8** arr, jint size) { @@ -2766,7 +2696,6 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage UINT8** scale = NULL; boolean success = TRUE; - /* verify the inputs */ if (data == NULL) { @@ -2891,7 +2820,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage if (setjmp(jerr->setjmp_buffer)) { /* If we get here, the JPEG code has signaled an error while writing. */ - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte), JNI_ABORT, JNI_ABORT); if (!(*env)->ExceptionCheck(env)) { char buffer[JMSG_LENGTH_MAX]; (*cinfo->err->format_message) ((j_common_ptr) cinfo, @@ -2973,7 +2902,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage free(scanLinePtr); JNU_ThrowByName(env, "javax/imageio/IIOException", - "Array pin failed"); + "Get array elements failed"); return data->abortFlag; } @@ -3006,7 +2935,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage scanptr = (int *) cinfo->script_space; scanData = (*env)->GetIntArrayElements(env, scanInfo, NULL); if (scanData == NULL) { - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte), JNI_ABORT, JNI_ABORT); freeArray(scale, numBands); free(scanLinePtr); return data->abortFlag; @@ -3034,16 +2963,13 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage if (haveMetadata) { // Flush the buffer imageio_flush_destination(cinfo); - // Call Java to write the metadata - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + // Call Java to write the metadata. (*env)->CallVoidMethod(env, this, JPEGImageWriter_writeMetadataID); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, - (const JOCTET **)(&dest->next_output_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); - } + if ((*env)->ExceptionCheck(env)) { + cinfo->err->error_exit((j_common_ptr) cinfo); + } } targetLine = 0; @@ -3053,20 +2979,29 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage // for each line in destHeight while ((data->abortFlag == JNI_FALSE) && (cinfo->next_scanline < cinfo->image_height)) { - // get the line from Java - RELEASE_ARRAYS(env, data, (const JOCTET *)(dest->next_output_byte)); + /* + * Get a line of pixel data from Java. + * In case where we have native copy of Java pixel array, + * we need to just use JNI_ABORT to exclude any copy operation + * and then get new copy for next scanline. + * + * If we have direct reference to Java array, we rely on + * JVM to maintain the reference appropriately. + */ + jboolean isCopy = pb->isCopy; + if (isCopy) { + unpinPixelBuffer(env, pb, JNI_ABORT); + } (*env)->CallVoidMethod(env, this, JPEGImageWriter_grabPixelsID, targetLine); - if ((*env)->ExceptionCheck(env) - || !GET_ARRAYS(env, data, - (const JOCTET **)(&dest->next_output_byte))) { - cinfo->err->error_exit((j_common_ptr) cinfo); - } + if ((*env)->ExceptionCheck(env) || + (isCopy && (pinPixelBuffer(env, pb) == NOT_OK))) { + cinfo->err->error_exit((j_common_ptr) cinfo); + } // subsample it into our buffer - in = data->pixelBuf.buf.bp; out = scanLinePtr; pixelLimit = in + ((pixelBufferSize > data->pixelBuf.byteBufferLength) ? @@ -3108,7 +3043,7 @@ Java_com_sun_imageio_plugins_jpeg_JPEGImageWriter_writeImage freeArray(scale, numBands); free(scanLinePtr); - RELEASE_ARRAYS(env, data, NULL); + RELEASE_ARRAYS(env, data, NULL, 0, JNI_ABORT); return data->abortFlag; } diff --git a/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWithProgressBench.java b/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWithProgressBench.java new file mode 100644 index 000000000000..70f8020f358c --- /dev/null +++ b/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWithProgressBench.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.javax.imageio.plugins.jpeg; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.event.IIOReadProgressListener; +import javax.imageio.stream.ImageInputStream; + +/** + * Measure time taken to read large jpeg image + * make test TEST="micro:javax.imageio.plugins.jpeg.LargeJpegReadWithProgressBench" + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +@State(Scope.Benchmark) +public class LargeJpegReadWithProgressBench { + + private static final File pwd = new File("."); + private static ImageReader reader; + + @Setup + public void setup() throws IOException { + BufferedImage src = createSource(); + ImageInputStream iis = prepareInput(src); + reader = null; + Iterator it = ImageIO.getImageReadersByFormatName("jpeg"); + if (it.hasNext()) { + reader = (ImageReader)it.next(); + } else { + throw new RuntimeException("Could not find JPEG reader"); + } + reader.setInput(iis); + ImageReadProgressListener listener = new ImageReadProgressListener(); + reader.addIIOReadProgressListener(listener); + } + + @Benchmark + public void readLargeJpegImage(Blackhole bh) throws IOException { + reader.read(0); + } + + private static BufferedImage createSource() { + int width = 2000; + int height = 2000; + int squareSize = 20; + + Color red = Color.RED; + Color green = Color.GREEN; + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (((x / squareSize) + (y / squareSize)) % 2 == 0) { + image.setRGB(x, y, red.getRGB()); + } else { + image.setRGB(x, y, green.getRGB()); + } + } + } + return image; + } + + private static ImageInputStream prepareInput(BufferedImage src) + throws IOException { + File f = File.createTempFile("src_", ".jpeg", pwd); + if (ImageIO.write(src, "jpeg", f)) { + ImageInputStream iis = ImageIO.createImageInputStream(f); + f.deleteOnExit(); + return iis; + } else { + throw new RuntimeException("Unable to write jpeg image"); + } + } +} + +class ImageReadProgressListener implements IIOReadProgressListener { + // This class is a no-op, it is added just to have a progress listener + @Override + public void sequenceStarted(ImageReader source, int minIndex) { + + } + + @Override + public void sequenceComplete(ImageReader source) { + + } + + @Override + public void imageStarted(ImageReader source, int imageIndex) { + + } + + @Override + public void imageProgress(ImageReader source, float percentageDone) { + + } + + @Override + public void imageComplete(ImageReader source) { + + } + + @Override + public void thumbnailStarted(ImageReader source, int imageIndex, int thumbnailIndex) { + + } + + @Override + public void thumbnailProgress(ImageReader source, float percentageDone) { + + } + + @Override + public void thumbnailComplete(ImageReader source) { + + } + + @Override + public void readAborted(ImageReader source) { + + } +} diff --git a/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWriteBench.java b/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWriteBench.java new file mode 100644 index 000000000000..8a84eab4da7e --- /dev/null +++ b/test/micro/org/openjdk/bench/javax/imageio/plugins/jpeg/LargeJpegReadWriteBench.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.javax.imageio.plugins.jpeg; + +import java.awt.Color; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.Iterator; + +import java.util.concurrent.TimeUnit; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import javax.imageio.ImageWriter; +import javax.imageio.stream.ImageInputStream; +import javax.imageio.stream.ImageOutputStream; + +/** + * Measure time taken to read large jpeg image + * make test TEST="micro:javax.imageio.plugins.jpeg.LargeJpegReadWriteBench" + */ +@BenchmarkMode(Mode.AverageTime) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 1) +@Measurement(iterations = 5, time = 1) +@Fork(3) +@State(Scope.Benchmark) +public class LargeJpegReadWriteBench { + + private static final File pwd = new File("."); + private static ImageReader reader; + private static ImageWriter writer; + private static BufferedImage src; + + @Setup + public void setup() throws IOException { + src = createSource(); + ImageInputStream iis = prepareInput(src); + reader = null; + Iterator readerIterator = ImageIO.getImageReadersByFormatName("jpeg"); + if (readerIterator.hasNext()) { + reader = readerIterator.next(); + } else { + throw new RuntimeException("Could not find JPEG reader"); + } + reader.setInput(iis); + + ImageOutputStream ios = prepareOutput(src); + writer = null; + Iterator writerIterator = ImageIO.getImageWritersByFormatName("jpeg"); + if (writerIterator.hasNext()) { + writer = writerIterator.next(); + } else { + throw new RuntimeException("Could not find JPEG writer"); + } + writer.setOutput(ios); + } + + @Benchmark + public void readLargeJpegImage(Blackhole bh) throws IOException { + reader.read(0); + } + + @Benchmark + public void writeLargeJpegImage(Blackhole bh) throws IOException { + writer.write(src); + } + + private static BufferedImage createSource() { + int width = 2000; + int height = 2000; + int squareSize = 20; + + Color red = Color.RED; + Color green = Color.GREEN; + BufferedImage image = new BufferedImage(width, height, + BufferedImage.TYPE_INT_RGB); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if (((x / squareSize) + (y / squareSize)) % 2 == 0) { + image.setRGB(x, y, red.getRGB()); + } else { + image.setRGB(x, y, green.getRGB()); + } + } + } + return image; + } + + private static ImageInputStream prepareInput(BufferedImage src) + throws IOException { + File f = File.createTempFile("src_", ".jpeg", pwd); + if (ImageIO.write(src, "jpeg", f)) { + ImageInputStream iis = ImageIO.createImageInputStream(f); + f.deleteOnExit(); + return iis; + } else { + throw new RuntimeException("Unable to write jpeg image"); + } + } + + private static ImageOutputStream prepareOutput(BufferedImage src) throws IOException { + File f = File.createTempFile("dest_", ".jpeg", pwd); + ImageOutputStream ios = ImageIO.createImageOutputStream(f); + f.deleteOnExit(); + return ios; + } +} From 1a82c79d0ca236f171bb9e23bc815edce2ceba24 Mon Sep 17 00:00:00 2001 From: Daniel Fuchs Date: Fri, 6 Mar 2026 09:28:51 +0000 Subject: [PATCH 07/17] 8378687: Improve delegation of HttpURLConnection Reviewed-by: rhalade, jpai, michaelm, skoivu --- .../classes/sun/net/www/protocol/http/HttpURLConnection.java | 4 ++-- .../protocol/https/AbstractDelegateHttpsURLConnection.java | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java index 480553e9a627..45e641f11eee 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/http/HttpURLConnection.java @@ -571,8 +571,8 @@ public void setRequestMethod(String method) throws ProtocolException { lock(); try { - if (connecting) { - throw new IllegalStateException("connect in progress"); + if (connected || connecting) { + throw new IllegalStateException("Already connected"); } super.setRequestMethod(method); } finally { diff --git a/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java b/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java index 1415658e34d6..88449caaf096 100644 --- a/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java +++ b/src/java.base/share/classes/sun/net/www/protocol/https/AbstractDelegateHttpsURLConnection.java @@ -178,7 +178,7 @@ public void setConnected(boolean conn) { public void connect() throws IOException { if (connected) return; - plainConnect(); + super.connect(); if (cachedResponse != null) { // using cached response return; From 2200a6ee02fe0a1be5961adeebd09b685401c4a8 Mon Sep 17 00:00:00 2001 From: Weijun Wang Date: Tue, 17 Mar 2026 21:03:49 +0000 Subject: [PATCH 08/17] 8377833: Enhance Jar file processing Reviewed-by: ahgross, rhalade, hchao, mullan --- .../share/classes/java/util/jar/JarVerifier.java | 6 +++--- .../sun/security/util/SignatureFileVerifier.java | 15 +++++++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/java.base/share/classes/java/util/jar/JarVerifier.java b/src/java.base/share/classes/java/util/jar/JarVerifier.java index d73231a4c613..e3bdb0307b9c 100644 --- a/src/java.base/share/classes/java/util/jar/JarVerifier.java +++ b/src/java.base/share/classes/java/util/jar/JarVerifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -68,7 +68,7 @@ class JarVerifier { private ArrayList pendingBlocks; /* cache of CodeSigner objects */ - private ArrayList signerCache; + private List signerCache; /* Are we parsing a block? */ private boolean parsingBlockOrSF = false; @@ -288,7 +288,7 @@ private void processEntry(ManifestEntryVerifier mev) String key = uname.substring(0, uname.lastIndexOf('.')); if (signerCache == null) - signerCache = new ArrayList<>(); + signerCache = new LinkedList<>(); if (manDig == null) { synchronized(manifestRawBytes) { diff --git a/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java b/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java index d7e65b6aef0f..0b21ccbd294c 100644 --- a/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java +++ b/src/java.base/share/classes/sun/security/util/SignatureFileVerifier.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -46,7 +46,12 @@ public class SignatureFileVerifier { /* Are we debugging ? */ private static final Debug debug = Debug.getInstance("jar"); - private final ArrayList signerCache; + private final List signerCache; + + // The maximum size of the signerCache. This is for debug only + // and not intended to be adjusted by users. + private static int SIGNER_CACHE_SIZE + = Integer.getInteger("sun.security.util.jar.signer.cache.size", 5); private static final String ATTR_DIGEST = "-DIGEST-" + ManifestDigester.MF_MAIN_ATTRS.toUpperCase(Locale.ENGLISH); @@ -97,7 +102,7 @@ public class SignatureFileVerifier { * * @param rawBytes the raw bytes of the signature block file */ - public SignatureFileVerifier(ArrayList signerCache, + public SignatureFileVerifier(List signerCache, ManifestDigester md, String name, byte[] rawBytes) @@ -282,7 +287,6 @@ public void process(Hashtable signers, } finally { Providers.stopJarVerification(obj); } - } private void processImpl(Hashtable signers, @@ -850,6 +854,9 @@ void updateSigners(CodeSigner[] newSigners, newSigners.length); } signerCache.add(cachedSigners); + if (signerCache.size() > SIGNER_CACHE_SIZE) { + signerCache.remove(0); + } signers.put(name, cachedSigners); } From 1a832b1845e77d44cc92a8630d3fdc4ba0f20ea6 Mon Sep 17 00:00:00 2001 From: Artur Barashev Date: Wed, 8 Apr 2026 12:08:39 +0000 Subject: [PATCH 09/17] 8380672: Improve certification checking Reviewed-by: ahgross, jnibedita, pkumaraswamy, rhalade, weijun, mullan --- .../sun/security/util/HostnameChecker.java | 4 ++-- .../classes/sun/security/x509/DNSName.java | 8 +++++++- .../test/lib/security/CertificateBuilder.java | 20 ++++++++++++++++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/java.base/share/classes/sun/security/util/HostnameChecker.java b/src/java.base/share/classes/sun/security/util/HostnameChecker.java index 65115c9aeaf9..b5a6e48e5707 100644 --- a/src/java.base/share/classes/sun/security/util/HostnameChecker.java +++ b/src/java.base/share/classes/sun/security/util/HostnameChecker.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2002, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -263,7 +263,7 @@ public static X500Name getSubjectX500Name(X509Certificate cert) * The name parameter should represent a DNS name. The * template parameter may contain the wildcard character '*'. */ - private boolean isMatched(String name, String template, + public boolean isMatched(String name, String template, boolean chainsToPublicCA) { // Normalize to Unicode, because PSL is in Unicode. diff --git a/src/java.base/share/classes/sun/security/x509/DNSName.java b/src/java.base/share/classes/sun/security/x509/DNSName.java index ce903a3d16cf..17820d279a50 100644 --- a/src/java.base/share/classes/sun/security/x509/DNSName.java +++ b/src/java.base/share/classes/sun/security/x509/DNSName.java @@ -52,6 +52,8 @@ public class DNSName implements GeneralNameInterface { private final String name; + private static final HostnameChecker HOSTNAME_CHECKER = + HostnameChecker.getInstance(HostnameChecker.TYPE_TLS); private static final String DNS_ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-"; @@ -218,6 +220,9 @@ public int hashCode() { * For example, www.host.example.com would satisfy the constraint but * host1.example.com would not. *

+ * RFC6125: Match wildcard pattern in the input name being constrained, + * any wildcard in this name will be matched as a literal character. + *

* RFC1034: By convention, domain names can be stored with arbitrary case, but * domain name comparisons for all present domain functions are done in a * case-insensitive manner, assuming an ASCII character set, and a high @@ -238,7 +243,8 @@ else if (inputName.getType() != NAME_DNS) String inName = (((DNSName)inputName).getName()).toLowerCase(Locale.ENGLISH); String thisName = name.toLowerCase(Locale.ENGLISH); - if (inName.equals(thisName)) + + if (HOSTNAME_CHECKER.isMatched(thisName, inName, false)) constraintType = NAME_MATCH; else if (thisName.endsWith(inName)) { int inNdx = thisName.lastIndexOf(inName); diff --git a/test/lib/jdk/test/lib/security/CertificateBuilder.java b/test/lib/jdk/test/lib/security/CertificateBuilder.java index 6bf554c3517e..a2d2a7d9eb1b 100644 --- a/test/lib/jdk/test/lib/security/CertificateBuilder.java +++ b/test/lib/jdk/test/lib/security/CertificateBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -24,6 +24,7 @@ package jdk.test.lib.security; import java.io.*; +import java.net.IDN; import java.security.cert.*; import java.security.cert.Extension; import java.util.*; @@ -41,7 +42,9 @@ import sun.security.x509.AlgorithmId; import sun.security.x509.AuthorityInfoAccessExtension; import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.GeneralSubtrees; import sun.security.x509.IPAddressName; +import sun.security.x509.NameConstraintsExtension; import sun.security.x509.SubjectKeyIdentifierExtension; import sun.security.x509.BasicConstraintsExtension; import sun.security.x509.CertificateSerialNumber; @@ -326,7 +329,6 @@ public CertificateBuilder addExtensions(List extList) { * Helper method to add DNSName types for the SAN extension * * @param dnsNames A {@code List} of names to add as DNSName types - * * @throws IOException if an encoding error occurs. */ public CertificateBuilder addSubjectAltNameDNSExt(List dnsNames) @@ -334,7 +336,8 @@ public CertificateBuilder addSubjectAltNameDNSExt(List dnsNames) if (!dnsNames.isEmpty()) { GeneralNames gNames = new GeneralNames(); for (String name : dnsNames) { - gNames.add(new GeneralName(new DNSName(name))); + gNames.add(new GeneralName(new DNSName(new DerValue( + DerValue.tag_IA5String, IDN.toASCII(name))))); } addExtension(new SubjectAlternativeNameExtension(false, gNames)); @@ -437,6 +440,17 @@ public CertificateBuilder addBasicConstraintsExt(boolean crit, boolean isCA, maxPathLen)); } + /** + * Set the Name Constraints Extension for a certificate. + * + * @param permitted permitted names + * @param excluded excluded names + */ + public CertificateBuilder addNameConstraintsExt( + GeneralSubtrees permitted, GeneralSubtrees excluded) { + return addExtension(new NameConstraintsExtension(permitted, excluded)); + } + /** * Add the Authority Key Identifier extension. * From a1dae786f538757eaec64edfa9f53fcc4dc6e72a Mon Sep 17 00:00:00 2001 From: Jayathirth D V Date: Fri, 10 Apr 2026 12:14:14 +0000 Subject: [PATCH 10/17] 8381039: Enhance AWT ImagingLib Reviewed-by: mschoene, rhalade, azvegint, prr --- .../libawt/awt/medialib/awt_ImagingLib.c | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/src/java.desktop/share/native/libawt/awt/medialib/awt_ImagingLib.c b/src/java.desktop/share/native/libawt/awt/medialib/awt_ImagingLib.c index bb93108f111f..b6e10617cc31 100644 --- a/src/java.desktop/share/native/libawt/awt/medialib/awt_ImagingLib.c +++ b/src/java.desktop/share/native/libawt/awt/medialib/awt_ImagingLib.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -2218,7 +2218,11 @@ allocateArray(JNIEnv *env, BufImageS_t *imageP, /* Means we need to fill in alpha */ if (!cvtToDefault && addAlpha) { *mlibImagePP = (*sMlibSysFns.createFP)(MLIB_BYTE, 4, width, height); - if (*mlibImagePP != NULL) { + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } else { unsigned int *dstP = (unsigned int *) mlib_ImageGetData(*mlibImagePP); int dstride = (*mlibImagePP)->stride>>2; @@ -2234,10 +2238,10 @@ allocateArray(JNIEnv *env, BufImageS_t *imageP, dP[x] = sP[x] | 0xff000000; } } + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return 0; } - (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, - JNI_ABORT); - return 0; } else if ((hintP->packing & BYTE_INTERLEAVED) == BYTE_INTERLEAVED) { int nChans = (cmP->isDefaultCompatCM ? 4 : hintP->numChans); @@ -2252,6 +2256,11 @@ allocateArray(JNIEnv *env, BufImageS_t *imageP, hintP->sStride, (unsigned char *)dataP + hintP->dataOffset); + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } } else if ((hintP->packing & SHORT_INTERLEAVED) == SHORT_INTERLEAVED) { *mlibImagePP = (*sMlibSysFns.createStructFP)(MLIB_SHORT, @@ -2261,6 +2270,11 @@ allocateArray(JNIEnv *env, BufImageS_t *imageP, imageP->raster.scanlineStride*2, (unsigned short *)dataP + hintP->channelOffset); + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } } else { /* Release the data array */ @@ -2360,6 +2374,11 @@ allocateRasterArray(JNIEnv *env, RasterS_t *rasterP, width, height, rasterP->scanlineStride*4, (unsigned char *)dataP + offset); + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } *dataPP = dataP; return 0; case sun_awt_image_IntegerComponentRaster_TYPE_BYTE_SAMPLES: @@ -2388,6 +2407,11 @@ allocateRasterArray(JNIEnv *env, RasterS_t *rasterP, width, height, rasterP->scanlineStride, (unsigned char *)dataP + offset); + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } *dataPP = dataP; return 0; case sun_awt_image_IntegerComponentRaster_TYPE_USHORT_SAMPLES: @@ -2418,6 +2442,11 @@ allocateRasterArray(JNIEnv *env, RasterS_t *rasterP, width, height, rasterP->scanlineStride*2, (unsigned char *)dataP + offset); + if (*mlibImagePP == NULL) { + (*env)->ReleasePrimitiveArrayCritical(env, rasterP->jdata, dataP, + JNI_ABORT); + return -1; + } *dataPP = dataP; return 0; From 930a0db76f0501e5983434229224a60bbcc25c4c Mon Sep 17 00:00:00 2001 From: Anthony Scarpino Date: Thu, 16 Apr 2026 17:15:33 +0000 Subject: [PATCH 11/17] 8381519: Enhance Der Value Handling Reviewed-by: mschoene, jnimeh, valeriep --- .../share/classes/sun/security/util/DerValue.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/java.base/share/classes/sun/security/util/DerValue.java b/src/java.base/share/classes/sun/security/util/DerValue.java index ec8b482b07dc..8d86c8dd1439 100644 --- a/src/java.base/share/classes/sun/security/util/DerValue.java +++ b/src/java.base/share/classes/sun/security/util/DerValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -157,6 +157,9 @@ public class DerValue { */ public static final byte tag_SetOf = 0x31; + // Max nested depth for constructed data + private static final int MAX_CONSTRUCTED_NEST = 30; + // This class is mostly immutable except that: // // 1. resetTag() modifies the tag @@ -564,6 +567,14 @@ public ObjectIdentifier getOID() throws IOException { * @return the octet string held in this DER value */ public byte[] getOctetString() throws IOException { + return getOctetString(0); + } + + private byte[] getOctetString(int limit) throws IOException { + if (++limit > MAX_CONSTRUCTED_NEST) { + throw new IOException("Nested OctetString limit reached (" + + MAX_CONSTRUCTED_NEST + ")."); + } if (tag != tag_OctetString && !isConstructed(tag_OctetString)) { throw new IOException( @@ -582,7 +593,7 @@ public byte[] getOctetString() throws IOException { ByteArrayOutputStream bout = new ByteArrayOutputStream(); DerInputStream dis = data(); while (dis.available() > 0) { - bout.write(dis.getDerValue().getOctetString()); + bout.write(dis.getDerValue().getOctetString(limit)); } return bout.toByteArray(); } From b32cb078161356afe7776b33fb90a6c0392d1d46 Mon Sep 17 00:00:00 2001 From: Jamil Nimeh Date: Thu, 23 Apr 2026 00:55:06 +0000 Subject: [PATCH 12/17] 8381796: Enhance Certificate parsing Reviewed-by: ascarpino, abarashev, rhalade, mdonovan --- .../provider/certpath/URICertStore.java | 89 +++++++++++++- .../sun/security/util/SecurityProperties.java | 32 ++++- .../share/conf/security/java.security | 16 +++ .../certpath/ldap/LDAPCertStoreImpl.java | 45 ++++++- .../test/lib/security/CertificateBuilder.java | 114 +++++++++++++----- 5 files changed, 261 insertions(+), 35 deletions(-) diff --git a/src/java.base/share/classes/sun/security/provider/certpath/URICertStore.java b/src/java.base/share/classes/sun/security/provider/certpath/URICertStore.java index 3e1fc8db1644..6eb95f92246d 100644 --- a/src/java.base/share/classes/sun/security/provider/certpath/URICertStore.java +++ b/src/java.base/share/classes/sun/security/provider/certpath/URICertStore.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2006, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2006, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,6 +25,7 @@ package sun.security.provider.certpath; +import java.io.FilterInputStream; import java.io.InputStream; import java.io.IOException; import java.net.HttpURLConnection; @@ -188,6 +189,16 @@ private static int initializeTimeout(String prop, int def) { return timeoutVal; } + /** + * Maximum size for a CRL downloaded through a URICertStore + * in bytes. This can be controlled by the com.sun.security.crl.maxSize + * Security or System property. The System property, if set, overrides + * the Security property. The default size is 20MiB. + */ + private static final long MAX_CRL_DOWNLOAD_SIZE = + SecurityProperties.getOverridableLongProp( + "com.sun.security.crl.maxSize", 20971520, debug); + /** * Enumeration for the allowed schemes we support when following a * URI from an authorityInfoAccess extension on a certificate. @@ -228,6 +239,13 @@ static AllowedScheme nameOf(String name) { private static final boolean CA_ISS_ALLOW_ANY; static { + // Add a debug message for the configured CRL download limit + if (debug != null) { + debug.println("Maximum downloadable CRL size: " + + MAX_CRL_DOWNLOAD_SIZE + + ((MAX_CRL_DOWNLOAD_SIZE < 0) ? " (DISABLED)" : "")); + } + boolean allowAny = false; try { if (Builder.USE_AIA) { @@ -623,7 +641,19 @@ public synchronized Collection engineGetCRLs(CRLSelector selector) if (debug != null) { debug.println("Downloading new CRL..."); } - crl = (X509CRL) factory.generateCRL(in); + InputStream crlIn = (MAX_CRL_DOWNLOAD_SIZE > -1) ? + new SizeLimitedInputStream(in, MAX_CRL_DOWNLOAD_SIZE) : + in; + try { + crl = (X509CRL) factory.generateCRL(crlIn); + } catch (IllegalArgumentException iae) { + // IAE should only be thrown when the CRL exceeds a + // configured maximum length. + if (debug != null) { + debug.println("Discarding CRL: " + iae.getMessage()); + crl = null; + } + } } return getMatchingCRLs(crl, selector); } catch (IOException | CRLException e) { @@ -816,4 +846,59 @@ boolean matchRule(URI filterRule, URI caIssuer) { return true; } } + + /** + * Stream wrapper used when an InputStream passed into a CertificateFactory + * needs to be size limited. It will throw IllegalArgumentException when + * the downloaded resource via the underlying stream exceeds the maximum + * limit. + */ + private static class SizeLimitedInputStream extends FilterInputStream { + + private final long maxBytes; + private long bytesRead = 0; + + private SizeLimitedInputStream(InputStream in, long maxBytes) { + super(in); + this.maxBytes = maxBytes; + } + + @Override + public int read() throws IOException { + if (bytesRead >= maxBytes) { + // We will use IAE here to differentiate this special case + // from other IOEs that the underlying input stream might + // legitimately throw. + throw new IllegalArgumentException("InputStream exceeded max " + + "size of " + maxBytes); + } + + int b = super.read(); + if (b != -1) { + bytesRead++; + } + return b; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + + if (bytesRead >= maxBytes) { + // We will use IAE here to differentiate this special case + // from other IOEs that the underlying input stream might + // legitimately throw. + throw new IllegalArgumentException("InputStream exceeded max " + + "size of " + maxBytes); + } + + long remaining = maxBytes - bytesRead; + int toRead = (int) Math.min(len, remaining); + + int n = super.read(b, off, toRead); + if (n != -1) { + bytesRead += n; + } + return n; + } + } } diff --git a/src/java.base/share/classes/sun/security/util/SecurityProperties.java b/src/java.base/share/classes/sun/security/util/SecurityProperties.java index 98bc71d829be..da69ecbf5d66 100644 --- a/src/java.base/share/classes/sun/security/util/SecurityProperties.java +++ b/src/java.base/share/classes/sun/security/util/SecurityProperties.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 2018 SAP SE. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * @@ -139,6 +139,36 @@ public static int getTimeoutSystemProp(String prop, int def, Debug dbg) { } } + /** + * A convenience routine for fetching a numeric value from a Security + * or System property and returning it as a long. The value from the + * property is obtained according to the logic in + * {@link SecurityProperties#getOverridableProperty(String)} + * + * @param prop the property to query + * @param defaultValue the default value + * @param dbg a Debug object, if null no debug messages will be sent + * @return the value of the property as a {@code long}. If a non-numeric + * value is supplied, the default value will be returned. + */ + public static long getOverridableLongProp(String prop, long defaultValue, + Debug dbg) { + long longVal = defaultValue; + try { + String propVal = SecurityProperties.getOverridableProperty(prop); + if (propVal != null) { + longVal = Long.parseLong(propVal); + } + } catch (NumberFormatException nfe) { + // We will use the default, but add a warning debug message + if (dbg != null) { + dbg.println("Warning: Non-numeric value found in property " + + prop + ", using default value of " + defaultValue); + } + } + return longVal; + } + /** * Convenience method for fetching System property values that are booleans. * diff --git a/src/java.base/share/conf/security/java.security b/src/java.base/share/conf/security/java.security index 976604b5cbcc..7927bab4d431 100644 --- a/src/java.base/share/conf/security/java.security +++ b/src/java.base/share/conf/security/java.security @@ -1701,6 +1701,22 @@ jdk.epkcs8.defaultAlgorithm=PBEWithHmacSHA256AndAES_128 # ldap://ldap.company.com/dc=company,dc=com?caCertificate;binary com.sun.security.allowedAIALocations= +# +# Certificate Revocation List (CRL) Download Size Limitation +# +# This property sets a size limit for CRLs downloaded via URIs provided +# in the CRL Distribution Points certificate extension. This property +# must be a numeric value that is the size in bytes of the DER-encoded CRL. +# For protocols that can return multi-value responses, such as LDAP, the +# size threshold is the sum of all CRLs downloaded from a single search +# query. CRLs that exceed this length will not be processed during certificate +# path validation. This size limit does not apply to CRLs that are imported +# through non-network-based means. A negative value will disable this size +# limitation. A non-numeric value will be ignored and the default size will +# be used instead. The default size limit is 20MiB. +# This property may be overridden by a System property of the same name. +com.sun.security.crl.maxSize = 20971520 + # # PKCS #8 encoding format for newly created ML-KEM and ML-DSA private keys # diff --git a/src/java.naming/share/classes/sun/security/provider/certpath/ldap/LDAPCertStoreImpl.java b/src/java.naming/share/classes/sun/security/provider/certpath/ldap/LDAPCertStoreImpl.java index 8f18e04760a2..ebf09e57bd1b 100644 --- a/src/java.naming/share/classes/sun/security/provider/certpath/ldap/LDAPCertStoreImpl.java +++ b/src/java.naming/share/classes/sun/security/provider/certpath/ldap/LDAPCertStoreImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -52,6 +52,7 @@ import sun.security.provider.certpath.X509CertificatePair; import sun.security.util.Cache; import sun.security.util.Debug; +import sun.security.util.SecurityProperties; /** * Core implementation of a LDAP Cert Store. @@ -96,6 +97,16 @@ final class LDAPCertStoreImpl { private static final String PROP_DISABLE_APP_RESOURCE_FILES = "sun.security.certpath.ldap.disable.app.resource.files"; + /** + * Maximum size for a CRL downloaded through an LDAPCertStoreImpl + * in bytes. This can be controlled by the com.sun.security.crl.maxSize + * Security or System property. The System property, if set, overrides + * the Security property. The default size is 20MiB. + */ + private static final long MAX_CRL_DOWNLOAD_SIZE = + SecurityProperties.getOverridableLongProp( + "com.sun.security.crl.maxSize", 20971520, debug); + static { String s = System.getProperty(PROP_LIFETIME); if (s != null) { @@ -103,6 +114,13 @@ final class LDAPCertStoreImpl { } else { LIFETIME = DEFAULT_CACHE_LIFETIME; } + + // Add a debug message for the configured CRL download limit + if (debug != null) { + debug.println("Maximum downloadable CRL size: " + + MAX_CRL_DOWNLOAD_SIZE + + ((MAX_CRL_DOWNLOAD_SIZE < 0) ? " (DISABLED)" : "")); + } } /** @@ -672,12 +690,12 @@ private Collection getMatchingCrossCerts( return certs; } - /* + /** * Gets CRLs from an attribute id and location in the LDAP directory. * Returns a Collection containing only the CRLs that match the * specified X509CRLSelector. * - * @param name the location holding the attribute + * @param request the LDAP request used for this CRL fetch operation * @param id the attribute identifier * @param sel a X509CRLSelector that the CRLs must match * @return a Collection of CRLs found @@ -689,7 +707,26 @@ private Collection getCRLs(LDAPRequest request, String id, /* fetch the encoded crls from storage */ byte[][] encodedCRL; try { - encodedCRL = request.getValues(id); + byte[][] tmpCrls = request.getValues(id); + if (MAX_CRL_DOWNLOAD_SIZE > -1) { + int totalSize = 0; + for (byte[] tCrl : tmpCrls) { + totalSize += tCrl.length; + } + if (totalSize <= MAX_CRL_DOWNLOAD_SIZE) { + encodedCRL = tmpCrls; + } else { + if (debug != null) { + debug.println("Received " + tmpCrls.length + + " CRL(s). Combined length of " + totalSize + + " exceeds configured maximum. Discarding."); + } + encodedCRL = new byte[0][]; + } + } else { + // Download limits disabled + encodedCRL = tmpCrls; + } } catch (NamingException namingEx) { throw new CertStoreException(namingEx); } diff --git a/test/lib/jdk/test/lib/security/CertificateBuilder.java b/test/lib/jdk/test/lib/security/CertificateBuilder.java index a2d2a7d9eb1b..86aaba2a0b15 100644 --- a/test/lib/jdk/test/lib/security/CertificateBuilder.java +++ b/test/lib/jdk/test/lib/security/CertificateBuilder.java @@ -42,6 +42,7 @@ import sun.security.x509.AlgorithmId; import sun.security.x509.AuthorityInfoAccessExtension; import sun.security.x509.AuthorityKeyIdentifierExtension; +import sun.security.x509.CRLDistributionPointsExtension; import sun.security.x509.GeneralSubtrees; import sun.security.x509.IPAddressName; import sun.security.x509.NameConstraintsExtension; @@ -49,6 +50,7 @@ import sun.security.x509.BasicConstraintsExtension; import sun.security.x509.CertificateSerialNumber; import sun.security.x509.ExtendedKeyUsageExtension; +import sun.security.x509.DistributionPoint; import sun.security.x509.DNSName; import sun.security.x509.GeneralName; import sun.security.x509.GeneralNames; @@ -61,13 +63,13 @@ /** * Helper class that builds and signs X.509 certificates. - * + *

* A CertificateBuilder is created with a default constructor, and then * uses additional public methods to set the public key, desired validity * dates, serial number and extensions. It is expected that the caller will * have generated the necessary key pairs prior to using a CertificateBuilder * to generate certificates. - * + *

* The following methods are mandatory before calling build(): *

    *
  • {@link #setSubjectName(java.lang.String)} @@ -81,12 +83,12 @@ * Additionally, the caller can either provide a {@link List} of * {@link Extension} objects, or use the helper classes to add specific * extension types. - * + *

    * When all required and desired parameters are set, the * {@link #build(java.security.cert.X509Certificate, java.security.PrivateKey, * java.lang.String)} method can be used to create the {@link X509Certificate} * object. - * + *

    * Multiple certificates may be cut from the same settings using subsequent * calls to the build method. Settings may be cleared using the * {@link #reset()} method. @@ -112,20 +114,23 @@ public enum KeyUsage { KEY_CERT_SIGN, CRL_SIGN, ENCIPHER_ONLY, - DECIPHER_ONLY; + DECIPHER_ONLY } /** - * Create a new CertificateBuilder instance. This method sets the subject name, - * public key, authority key id, and serial number. + * Create a new {@code CertificateBuilder} instance. This method sets the + * subject name, public key, authority key id, and serial number. * * @param subjectName entity associated with the public key * @param publicKey the entity's public key * @param caKey public key of certificate signer * @param keyUsages list of key uses - * @return - * @throws CertificateException - * @throws IOException + * @return a {@code CertificateBuilder} configured with the provided + * parameters + * + * @throws CertificateException if an error occurs when obtaining the + * underlying {@link CertificateFactory} + * @throws IOException if any extension encoding errors occur */ public static CertificateBuilder newCertificateBuilder(String subjectName, PublicKey publicKey, PublicKey caKey, KeyUsage... keyUsages) @@ -151,9 +156,13 @@ public static CertificateBuilder newCertificateBuilder(String subjectName, /** * Create a Subject Alternative Name extension for the given DNS name + * * @param critical Sets the extension to critical or non-critical - * @param dnsName DNS name to use in the extension - * @throws IOException + * @param dnsNames one or more DNS names to use in the extension + * @return a {@code SubjectAlternativeNameExtension} configured with + * the {@code dnsNames} as individual DNSName entries. + * + * @throws IOException if any encoding errors occur */ public static SubjectAlternativeNameExtension createDNSSubjectAltNameExt( boolean critical, String... dnsNames) throws IOException { @@ -166,9 +175,13 @@ public static SubjectAlternativeNameExtension createDNSSubjectAltNameExt( /** * Create a Subject Alternative Name extension for the given IP address + * * @param critical Sets the extension to critical or non-critical - * @param ipAddresses IP addresses to use in the extension - * @throws IOException + * @param ipAddresses one or more IP addresses to use in the extension + * @return a {@code SubjectAlternativeNameExtension} configured with + * the {@code ipAddresses} as individual IPAddressName entries. + * + * @throws IOException if any encoding errors occur */ public static SubjectAlternativeNameExtension createIPSubjectAltNameExt( boolean critical, String... ipAddresses) throws IOException { @@ -215,6 +228,9 @@ public CertificateBuilder setSubjectName(X500Principal name) { * Set the subject name for the certificate. * * @param name The subject name in RFC 2253 format + * + * @throws IllegalArgumentException if any parsing errors on the + * {@code name} parameter occur. */ public CertificateBuilder setSubjectName(String name) { try { @@ -241,6 +257,9 @@ public CertificateBuilder setSubjectName(X500Name name) { * Set the public key for this certificate. * * @param pubKey The {@link PublicKey} to be used on this certificate. + * + * @throws NullPointerException if the {@code pubKey} parameter + * is {@code null} */ public CertificateBuilder setPublicKey(PublicKey pubKey) { publicKey = Objects.requireNonNull(pubKey, "Caught null public key"); @@ -252,6 +271,9 @@ public CertificateBuilder setPublicKey(PublicKey pubKey) { * * @param nbDate A {@link Date} object specifying the start of the * certificate validity period. + * + * @throws NullPointerException if the {@code nbDate} parameter + * is {@code null} */ public CertificateBuilder setNotBefore(Date nbDate) { Objects.requireNonNull(nbDate, "Caught null notBefore date"); @@ -264,6 +286,9 @@ public CertificateBuilder setNotBefore(Date nbDate) { * * @param naDate A {@link Date} object specifying the end of the * certificate validity period. + * + * @throws NullPointerException if the {@code naDate} parameter + * is {@code null} */ public CertificateBuilder setNotAfter(Date naDate) { Objects.requireNonNull(naDate, "Caught null notAfter date"); @@ -278,6 +303,9 @@ public CertificateBuilder setNotAfter(Date naDate) { * certificate validity period. * @param naDate A {@link Date} object specifying the end of the * certificate validity period. + * + * @throws NullPointerException if either the {@code nbDate} or + * {@code naDate} parameters are {@code null} */ public CertificateBuilder setValidity(Date nbDate, Date naDate) { return setNotBefore(nbDate).setNotAfter(naDate); @@ -292,6 +320,8 @@ public CertificateBuilder setOneHourValidity() { * Set the serial number on the certificate. * * @param serial A serial number in {@link BigInteger} form. + * + * @throws NullPointerException if {@code serial} is {@code null} */ public CertificateBuilder setSerialNumber(BigInteger serial) { Objects.requireNonNull(serial, "Caught null serial number"); @@ -316,6 +346,8 @@ public CertificateBuilder addExtension(Extension ext) { * * @param extList The {@link List} of extensions to be added to * the certificate. + * + * @throws NullPointerException if {@code extList} is {@code null} */ public CertificateBuilder addExtensions(List extList) { Objects.requireNonNull(extList, "Caught null extension list"); @@ -329,6 +361,7 @@ public CertificateBuilder addExtensions(List extList) { * Helper method to add DNSName types for the SAN extension * * @param dnsNames A {@code List} of names to add as DNSName types + * * @throws IOException if an encoding error occurs. */ public CertificateBuilder addSubjectAltNameDNSExt(List dnsNames) @@ -350,6 +383,7 @@ public CertificateBuilder addSubjectAltNameDNSExt(List dnsNames) * * @param ipAddresses A {@code List} of names to add as IPAddress * types + * * @throws IOException if an encoding error occurs. */ public CertificateBuilder addSubjectAltNameIPExt(List ipAddresses) @@ -365,13 +399,41 @@ public CertificateBuilder addSubjectAltNameIPExt(List ipAddresses) return this; } + /** + * Helper method to add one or more distribution points to the CRL + * Distribution Points extension. This form of the method only supports + * URI name types, but can be extended in the future to support other types. + * + * @param uriNames a list of URIs in String form + * @return the {@code CertificateBuilder} configured to add this + * CRL Distribution Points extension. + * + * @throws IOException if any of the URIs in {@code uriNames} are + * malformed + */ + public CertificateBuilder addCrlDistributionPointsExt(List uriNames) + throws IOException { + if (uriNames != null && !uriNames.isEmpty()) { + GeneralNames gNames = new GeneralNames(); + for (String name : uriNames) { + gNames.add(new GeneralName(new URIName(name))); + } + addExtension(new CRLDistributionPointsExtension(List.of( + new DistributionPoint(gNames, null, null)))); + } + return this; + } + /** * Helper method to add one or more OCSP URIs to the Authority Info Access * certificate extension. Location strings can be in two forms: - * 1) Just a URI by itself: This will be treated as using the OCSP + *

      + *
    1. Just a URI by itself: This will be treated as using the OCSP * access description (legacy behavior). - * 2) An access description name (case-insensitive) followed by a - * pipe (|) and the URI (e.g. OCSP|http://ocsp.company.com/revcheck). + *
    2. An access description name (case-insensitive) followed by a + * pipe (|) and the URI (e.g. + * {@code OCSP|http://ocsp.company.com/revcheck}). + *
    * Current description names are OCSP and CAISSUER. Others may be * added later. * @@ -392,16 +454,12 @@ public CertificateBuilder addAIAExt(List locations) adObj = AccessDescription.Ad_OCSP_Id; uriLoc = tokens[0]; } else { - switch (tokens[0].toUpperCase()) { - case "OCSP": - adObj = AccessDescription.Ad_OCSP_Id; - break; - case "CAISSUER": - adObj = AccessDescription.Ad_CAISSUERS_Id; - break; - default: - throw new IOException("Unknown AD: " + tokens[0]); - } + adObj = switch (tokens[0].toUpperCase()) { + case "OCSP" -> AccessDescription.Ad_OCSP_Id; + case "CAISSUER" -> AccessDescription.Ad_CAISSUERS_Id; + default -> throw new IOException("Unknown AD: " + + tokens[0]); + }; uriLoc = tokens[1]; } acDescList.add(new AccessDescription(adObj, @@ -568,7 +626,7 @@ public X509Certificate build(X509Certificate issuerCert, } /** - * Encode the contents of the outer-most ASN.1 SEQUENCE: + * Encode the contents of the outermost ASN.1 SEQUENCE: * *
          *  Certificate  ::=  SEQUENCE  {
    
    From 2fe80f5d20c38be986cc1efbbb40a42343730c81 Mon Sep 17 00:00:00 2001
    From: Weijun Wang 
    Date: Mon, 27 Apr 2026 21:38:44 +0000
    Subject: [PATCH 13/17] 8381049: Enhance Jar handling
    
    Reviewed-by: hchao, rhalade, mdonovan, abarashev, mullan, mpowers
    ---
     .../classes/sun/security/pkcs/SignerInfo.java |  23 +++-
     .../sun/security/timestamp/TSResponse.java    |   9 +-
     .../jdk/test/lib/security/DerUtilsTest.java   | 116 ++++++++++++++++++
     test/lib/jdk/test/lib/security/DerUtils.java  |  83 ++++++++++++-
     .../lib/security/timestamp/TsaHandler.java    |   9 +-
     .../test/lib/security/timestamp/TsaParam.java |  26 +++-
     .../lib/security/timestamp/TsaSigner.java     |  41 +++++--
     7 files changed, 293 insertions(+), 14 deletions(-)
     create mode 100644 test/lib-test/jdk/test/lib/security/DerUtilsTest.java
    
    diff --git a/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java b/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java
    index 36139d4f4a09..5b2c67500ac9 100644
    --- a/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java
    +++ b/src/java.base/share/classes/sun/security/pkcs/SignerInfo.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 1996, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -341,6 +341,21 @@ SignerInfo verify(PKCS7 block, byte[] data, X509Certificate cert)
                 // if there are authenticated attributes, get the message
                 // digest and compare it with the digest of data
                 if (authenticatedAttributes == null) {
    +                // RFC 5652 Section 5.3. "[signedAttrs] MUST be present if the
    +                // content type of the EncapsulatedContentInfo value being
    +                // signed is not id-data."
    +                if (!content.getContentType().equals(ContentInfo.DATA_OID)) {
    +                    throw new SignatureException("Missing authenticatedAttributes");
    +                } else {
    +                    try {
    +                        var c = new DerValue(data);
    +                        if (c.tag == DerValue.tag_Set) {
    +                            throw new SignatureException("Not a .SF file content");
    +                        }
    +                    } catch (IOException e) {
    +                        // Expected or ignored
    +                    }
    +                }
                     dataSigned = data;
                 } else {
     
    @@ -688,6 +703,12 @@ public Timestamp getTimestamp()
                 return null;
             }
     
    +        // RFC 3161 Section 2.4.2. id-ct-TSTInfo.
    +        if (!tsToken.getContentInfo().getContentType()
    +                .equals(ContentInfo.TIMESTAMP_TOKEN_INFO_OID)) {
    +            throw new SignatureException("Not using id-ct-TSTInfo");
    +        }
    +
             // Extract the content (an encoded timestamp token info)
             byte[] encTsTokenInfo = tsToken.getContentInfo().getData();
             // Extract the signer (the Timestamping Authority)
    diff --git a/src/java.base/share/classes/sun/security/timestamp/TSResponse.java b/src/java.base/share/classes/sun/security/timestamp/TSResponse.java
    index 16ba761bff04..82287540b631 100644
    --- a/src/java.base/share/classes/sun/security/timestamp/TSResponse.java
    +++ b/src/java.base/share/classes/sun/security/timestamp/TSResponse.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2003, 2022, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2003, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -26,6 +26,8 @@
     package sun.security.timestamp;
     
     import java.io.IOException;
    +
    +import sun.security.pkcs.ContentInfo;
     import sun.security.pkcs.PKCS7;
     import sun.security.util.Debug;
     import sun.security.util.DerValue;
    @@ -357,6 +359,11 @@ private void parse(byte[] tsReply) throws IOException {
                 DerValue timestampToken = derValue.data.getDerValue();
                 encodedTsToken = timestampToken.toByteArray();
                 tsToken = new PKCS7(encodedTsToken);
    +            // RFC 3161 Section 2.4.2. id-ct-TSTInfo.
    +            if (!tsToken.getContentInfo().getContentType()
    +                    .equals(ContentInfo.TIMESTAMP_TOKEN_INFO_OID)) {
    +                throw new TimestampException("Not using id-ct-TSTInfo");
    +            }
                 tstInfo = new TimestampToken(tsToken.getContentInfo().getData());
             }
     
    diff --git a/test/lib-test/jdk/test/lib/security/DerUtilsTest.java b/test/lib-test/jdk/test/lib/security/DerUtilsTest.java
    new file mode 100644
    index 000000000000..5ab6dcf59f4c
    --- /dev/null
    +++ b/test/lib-test/jdk/test/lib/security/DerUtilsTest.java
    @@ -0,0 +1,116 @@
    +/*
    + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
    + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
    + *
    + * This code is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU General Public License version 2 only, as
    + * published by the Free Software Foundation.
    + *
    + * This code is distributed in the hope that it will be useful, but WITHOUT
    + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    + * version 2 for more details (a copy is included in the LICENSE file that
    + * accompanied this code).
    + *
    + * You should have received a copy of the GNU General Public License version
    + * 2 along with this work; if not, write to the Free Software Foundation,
    + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
    + *
    + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
    + * or visit www.oracle.com if you need additional information or have any
    + * questions.
    + */
    +
    +import java.io.IOException;
    +import java.util.HexFormat;
    +
    +import jdk.test.lib.Asserts;
    +import jdk.test.lib.Utils;
    +import jdk.test.lib.security.DerUtils;
    +import sun.security.util.DerOutputStream;
    +import sun.security.util.DerValue;
    +import sun.security.util.KnownOIDs;
    +import sun.security.util.ObjectIdentifier;
    +
    +/*
    + * @test
    + * @bug 8381049
    + * @library /test/lib
    + * @modules java.base/sun.security.util
    + * @summary Tests DerUtils navigation, assertions, and editing helpers
    + */
    +public class DerUtilsTest {
    +
    +    public static void main(String[] args) throws Exception {
    +        //0000:0015  [] SEQUENCE
    +        //0002:0004  [0]     OID 1.2.3
    +        //0006:0003  [1]     INTEGER 1
    +        //0009:000C  [2]     OCTET STRING
    +        //              >>> into 10 octets
    +        //000B:000A  [2c]         SEQUENCE
    +        //000D:0005  [2c0]             OID 2.5.4.3 (CommonName)
    +        //0012:0003  [2c1]             INTEGER 2
    +        byte[] der = bytes("30 13 06022a03 020101 04 0a 30 08 0603550403 020102");
    +
    +        // Test innerDerValue
    +        Asserts.assertEQ(DerUtils.innerDerValue(der, "0").getOID(),
    +                ObjectIdentifier.of("1.2.3"));
    +        Asserts.assertEQ(DerUtils.innerDerValue(der, "1").getInteger(), 1);
    +        Asserts.assertEQ(DerUtils.innerDerValue(der, "2c0").getOID(),
    +                ObjectIdentifier.of(KnownOIDs.CommonName));
    +        Asserts.assertEQ(DerUtils.innerDerValue(der, "2c1").getInteger(), 2);
    +        Asserts.assertTrue(DerUtils.innerDerValue(der, "3") == null);
    +
    +        // Test checks
    +        DerUtils.checkAlg(der, "0", ObjectIdentifier.of("1.2.3"));
    +        DerUtils.checkInt(der, "1", 1);
    +        DerUtils.checkAlg(der, "2c0", ObjectIdentifier.of(KnownOIDs.CommonName));
    +        DerUtils.checkInt(der, "2c1", 2);
    +        DerUtils.shouldNotExist(der, "3");
    +
    +        // Test edit
    +        der = DerUtils.edit(der, "0", oidValue("1.2.3.4"));
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 14 06032a0304 020101 04 0a 30 08 0603550403 020102"), der);
    +
    +        der = DerUtils.edit(der, "2c1", intValue(8));
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 14 06032a0304 020101 04 0a 30 08 0603550403 020108"), der);
    +
    +        der = DerUtils.edit(der, "1", null);
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 11 06032a0304 04 0a 30 08 0603550403 020108"), der);
    +
    +        // Test insert
    +        der = DerUtils.insert(der, "0", oidValue("1.2.5"));
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 15 06022a05 06032a0304 04 0a 30 08 0603550403 020108"), der);
    +
    +        der = DerUtils.insert(der, "2c1", intValue(9));
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 18 06022a05 06032a0304 04 0d 30 0b 0603550403 020109 020108"), der);
    +
    +        der = DerUtils.insert(der, "2c1", oidValue("1.2.6"));
    +        Asserts.assertEqualsByteArray(
    +                bytes("30 1c 06022a05 06032a0304 04 11 30 0f 0603550403 06022a06 020109 020108"), der);
    +
    +        // Cannot insert into a position ends with "c"
    +        var derClone = der.clone(); // non-final reference cannot be used in lambda
    +        Utils.runAndCheckException(() -> DerUtils.insert(derClone, "2c",
    +                oidValue("1.2.7")), IOException.class);
    +    }
    +
    +    static DerValue oidValue(String oid) throws IOException {
    +        return DerValue.wrap(new DerOutputStream()
    +                .putOID(ObjectIdentifier.of(oid)).toByteArray());
    +    }
    +
    +    static DerValue intValue(int value) throws IOException {
    +        return DerValue.wrap(new DerOutputStream()
    +                .putInteger(value).toByteArray());
    +    }
    +
    +    public static byte[] bytes(String hex) {
    +        return HexFormat.of().parseHex(hex.replace(" ", ""));
    +    }
    +}
    diff --git a/test/lib/jdk/test/lib/security/DerUtils.java b/test/lib/jdk/test/lib/security/DerUtils.java
    index 234130aa4ca7..2323065b5952 100644
    --- a/test/lib/jdk/test/lib/security/DerUtils.java
    +++ b/test/lib/jdk/test/lib/security/DerUtils.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2018, 2021, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2018, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -24,6 +24,7 @@
     
     import jdk.test.lib.Asserts;
     import sun.security.util.DerInputStream;
    +import sun.security.util.DerOutputStream;
     import sun.security.util.DerValue;
     import sun.security.util.KnownOIDs;
     import sun.security.util.ObjectIdentifier;
    @@ -124,4 +125,84 @@ public static void shouldNotExist(byte[] der, String location)
                 throws Exception {
             Asserts.assertTrue(innerDerValue(der, location) == null);
         }
    +
    +    /// Replaces a `DerValue` (deep) inside with another one.
    +    ///
    +    /// @param data the `DerValue`
    +    /// @param target the location to edit. Cannot be empty.
    +    /// @param replacement replace the value at `target` with this. Can be a `DerValue`
    +    ///         or a `DerOutputStream`. Remove if `null`.
    +    /// @return the new value
    +    public static byte[] edit(byte[] data, String target, Object replacement)
    +            throws IOException {
    +        if (target.isEmpty()) throw new IOException("Must be a sub-location");
    +        return modify0(data, "", target, replacement, false).toByteArray();
    +    }
    +
    +    /// Inserts a `DerValue` (deep) into another one.
    +    ///
    +    /// @param data the `DerValue`
    +    /// @param target the location to insert at. Cannot be empty. The new value
    +    ///        is inserted before the existing value at `target`, and following
    +    ///        values are shifted. After insertion, the value at `target`
    +    ///        is the inserted value. A target ending exactly with `c` is not
    +    ///        a valid insertion position because the content of an OCTET
    +    ///        STRING must be only one `DerValue`.
    +    /// @param addition the value to insert. Can be a `DerValue` or a
    +    ///        `DerOutputStream`
    +    /// @return the new value
    +    public static byte[] insert(byte[] data, String target, Object addition)
    +            throws IOException {
    +        if (target.isEmpty()) throw new IOException("Must be a sub-location");
    +        return modify0(data, "", target, addition, true).toByteArray();
    +    }
    +
    +    /// Implementation of [#edit] and [#insert], recursively.
    +    ///
    +    /// @param data the `DerValue`
    +    /// @param now the current location
    +    /// @param target the location to edit or insert at
    +    /// @param replacement the replacement or inserted value
    +    /// @param insert true to insert before the target, false to replace it
    +    /// @return the new value at this location
    +    private static DerOutputStream modify0(byte[] data, String now, String target,
    +            Object replacement, boolean insert) throws IOException {
    +        var out = new DerOutputStream();
    +        var parent = DerUtils.innerDerValue(data, now);
    +        if (target.equals(now + "c")) {
    +            if (insert) {
    +                throw new IOException("Action cannot be performed at position " + target);
    +            }
    +            if (replacement instanceof DerValue v) {
    +                out.putDerValue(v);
    +            } else if (replacement instanceof DerOutputStream s) {
    +                out.write(s);
    +            }
    +        } else if (target.startsWith(now + "c")) { // not there yet, go inside
    +            return out.write(parent.tag, modify0(data, now + "c", target, replacement, insert));
    +        } else {
    +            for (int i = 0; ; i++) {
    +                // We only support locations of one digit now
    +                if (i > 9) throw new IllegalStateException("Too big " + i);
    +                String pos = now + i;
    +                var sub = DerUtils.innerDerValue(data, pos); // current value
    +                if (sub == null) break; // at the end
    +                if (target.equals(pos)) { // the one we want to change
    +                    if (replacement instanceof DerValue v) {
    +                        out.putDerValue(v);
    +                    } else if (replacement instanceof DerOutputStream s) {
    +                        out.write(s);
    +                    }
    +                    if (insert) {
    +                        out.putDerValue(sub);
    +                    }
    +                } else if (target.startsWith(pos)) { // not there yet, go inside
    +                    out.write(modify0(data, pos, target, replacement, insert));
    +                } else { // the untouched values
    +                    out.putDerValue(sub);
    +                }
    +            }
    +        }
    +        return new DerOutputStream().write(parent.tag, out);
    +    }
     }
    diff --git a/test/lib/jdk/test/lib/security/timestamp/TsaHandler.java b/test/lib/jdk/test/lib/security/timestamp/TsaHandler.java
    index d42d9f031d47..fa3d6abe599b 100644
    --- a/test/lib/jdk/test/lib/security/timestamp/TsaHandler.java
    +++ b/test/lib/jdk/test/lib/security/timestamp/TsaHandler.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020, 2022, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -148,7 +148,6 @@ protected SignerEntry createSignerEntry(String alias) throws Exception {
          */
         protected TsaParam getParam(URI uri) {
             String query = uri.getQuery();
    -
             TsaParam param = TsaParam.newInstance();
             if (query != null) {
                 for (String bufParam : query.split("&")) {
    @@ -186,6 +185,12 @@ protected TsaParam getParam(URI uri) {
                     } else if ("certReq".equalsIgnoreCase(pair[0])) {
                         param.certReq(Boolean.valueOf(pair[1]));
                         System.out.println("certReq: " + param.certReq());
    +                } else if ("noSignedAttrs".equalsIgnoreCase(pair[0])) {
    +                    param.noSignedAttrs(Boolean.valueOf(pair[1]));
    +                    System.out.println("noSignedAttrs: " + param.noSignedAttrs());
    +                } else if ("notTimestampOID".equalsIgnoreCase(pair[0])) {
    +                    param.notTimestampOID(Boolean.valueOf(pair[1]));
    +                    System.out.println("notTimestampOID: " + param.notTimestampOID());
                     }
                 }
             }
    diff --git a/test/lib/jdk/test/lib/security/timestamp/TsaParam.java b/test/lib/jdk/test/lib/security/timestamp/TsaParam.java
    index fca684bd5e6d..8838fb71a3a7 100644
    --- a/test/lib/jdk/test/lib/security/timestamp/TsaParam.java
    +++ b/test/lib/jdk/test/lib/security/timestamp/TsaParam.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -71,6 +71,12 @@ public class TsaParam {
         // Indicate if request TSA server certificate
         private Boolean certReq;
     
    +    // Do not create signedAttrs
    +    private Boolean noSignedAttrs;
    +
    +    // Do not use TIMESTAMP_TOKEN_INFO_OID
    +    private Boolean notTimestampOID;
    +
         public static TsaParam newInstance() {
             return new TsaParam();
         }
    @@ -177,4 +183,22 @@ public TsaParam certReq(Boolean certReq) {
             this.certReq = certReq;
             return this;
         }
    +
    +    public Boolean noSignedAttrs() {
    +        return noSignedAttrs;
    +    }
    +
    +    public TsaParam noSignedAttrs(Boolean noSignedAttrs) {
    +        this.noSignedAttrs = noSignedAttrs;
    +        return this;
    +    }
    +
    +    public Boolean notTimestampOID() {
    +        return notTimestampOID;
    +    }
    +
    +    public TsaParam notTimestampOID(Boolean notTimestampOID) {
    +        this.notTimestampOID = notTimestampOID;
    +        return this;
    +    }
     }
    diff --git a/test/lib/jdk/test/lib/security/timestamp/TsaSigner.java b/test/lib/jdk/test/lib/security/timestamp/TsaSigner.java
    index 22d630f3aa88..7ec2609e779f 100644
    --- a/test/lib/jdk/test/lib/security/timestamp/TsaSigner.java
    +++ b/test/lib/jdk/test/lib/security/timestamp/TsaSigner.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 2020, 2023, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 2020, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -23,8 +23,8 @@
     
     package jdk.test.lib.security.timestamp;
     
    -import java.io.ByteArrayOutputStream;
     import java.math.BigInteger;
    +import java.security.MessageDigest;
     import java.security.Signature;
     import java.security.cert.X509Certificate;
     import java.util.Date;
    @@ -33,6 +33,8 @@
     import jdk.test.lib.hexdump.HexPrinter;
     import sun.security.pkcs.ContentInfo;
     import sun.security.pkcs.PKCS7;
    +import sun.security.pkcs.PKCS9Attribute;
    +import sun.security.pkcs.PKCS9Attributes;
     import sun.security.pkcs.SignerInfo;
     import sun.security.util.*;
     import sun.security.x509.AlgorithmId;
    @@ -202,8 +204,11 @@ private byte[] createResponse(TsaParam requestParam) throws Exception {
                 DerOutputStream eContentOut = new DerOutputStream();
                 eContentOut.putOctetString(tstInfoSeqData);
     
    +            ObjectIdentifier infoOid = respParam.notTimestampOID() == Boolean.TRUE
    +                    ? ContentInfo.DATA_OID
    +                    : ContentInfo.TIMESTAMP_TOKEN_INFO_OID;
                 ContentInfo eContentInfo = new ContentInfo(
    -                    ObjectIdentifier.of(KnownOIDs.TimeStampTokenInfo),
    +                    infoOid,
                         new DerValue(eContentOut.toByteArray()));
     
                 String defaultSigAlgo =  SignatureUtil.getDefaultSigAlgForKey(
    @@ -213,16 +218,36 @@ private byte[] createResponse(TsaParam requestParam) throws Exception {
                 System.out.println(
                         "Signature algorithm: " + signature.getAlgorithm());
                 signature.initSign(signerEntry.privateKey);
    -            signature.update(tstInfoSeqData);
    +
    +            AlgorithmId digestAlg = SignatureUtil.getDigestAlgInPkcs7SignerInfo(
    +                    signature, sigAlgo, signerEntry.privateKey,
    +                    signerEntry.cert.getPublicKey(), false);
    +
    +            PKCS9Attributes authAttrs = null;
    +
    +            if (respParam.noSignedAttrs() == Boolean.TRUE) {
    +                signature.update(tstInfoSeqData);
    +            } else {
    +                authAttrs = new PKCS9Attributes(new PKCS9Attribute[]{
    +                        new PKCS9Attribute(PKCS9Attribute.CONTENT_TYPE_OID,
    +                                infoOid),
    +                        new PKCS9Attribute(PKCS9Attribute.SIGNING_TIME_OID,
    +                                new Date()),
    +                        new PKCS9Attribute(PKCS9Attribute.MESSAGE_DIGEST_OID,
    +                                MessageDigest.getInstance(digestAlg.getName())
    +                                        .digest(tstInfoSeqData))
    +                });
    +                signature.update(authAttrs.getDerEncoding());
    +            }
     
                 SignerInfo signerInfo = new SignerInfo(
                         new X500Name(issuerName),
                         signerEntry.cert.getSerialNumber(),
    -                    SignatureUtil.getDigestAlgInPkcs7SignerInfo(
    -                            signature, sigAlgo, signerEntry.privateKey,
    -                            signerEntry.cert.getPublicKey(), false),
    +                    digestAlg,
    +                    authAttrs,
                         AlgorithmId.get(sigAlgo),
    -                    signature.sign());
    +                    signature.sign(),
    +                    null);
     
                 X509Certificate[] signerCertChain = interceptor.getSignerCertChain(
                         signerEntry.certChain, requestParam.certReq());
    
    From 9566cd195af9e4117318a43fb8a9b6b5f5d57241 Mon Sep 17 00:00:00 2001
    From: Jayathirth D V 
    Date: Mon, 11 May 2026 04:41:11 +0000
    Subject: [PATCH 14/17] 8377158: Enhance XBM image support
    
    Reviewed-by: rhalade, aivanov, prr, azvegint
    ---
     .../sun/awt/image/XbmImageDecoder.java        | 314 +++++++++++-------
     1 file changed, 194 insertions(+), 120 deletions(-)
    
    diff --git a/src/java.desktop/share/classes/sun/awt/image/XbmImageDecoder.java b/src/java.desktop/share/classes/sun/awt/image/XbmImageDecoder.java
    index 1b7cd41daa93..4e79c6a63547 100644
    --- a/src/java.desktop/share/classes/sun/awt/image/XbmImageDecoder.java
    +++ b/src/java.desktop/share/classes/sun/awt/image/XbmImageDecoder.java
    @@ -31,12 +31,8 @@
     import java.awt.image.ImageConsumer;
     import java.awt.image.IndexColorModel;
     import java.io.BufferedInputStream;
    -import java.io.BufferedReader;
    -import java.io.IOException;
     import java.io.InputStream;
    -import java.io.InputStreamReader;
    -import java.util.regex.Matcher;
    -import java.util.regex.Pattern;
    +import java.io.IOException;
     
     import static java.lang.Math.multiplyExact;
     
    @@ -62,9 +58,8 @@ public class XbmImageDecoder extends ImageDecoder {
                                              ImageConsumer.SINGLEPASS |
                                              ImageConsumer.SINGLEFRAME);
     
    +    private static final int MAX_CHAR_LIMIT = 128000;
         private static final int MAX_XBM_SIZE = 16384;
    -    private static final int HEADER_SCAN_LIMIT = 100;
    -
         public XbmImageDecoder(InputStreamImageSource src, InputStream is) {
             super(src, is);
             if (!(input instanceof BufferedInputStream)) {
    @@ -86,150 +81,229 @@ private static void error(String s1) throws ImageFormatException {
          * produce an image from the stream.
          */
         public void produceImage() throws IOException, ImageFormatException {
    +        char[] nm = new char[80];
    +        int c;
    +        int i = 0;
    +        int state = 0;
             int H = 0;
             int W = 0;
             int x = 0;
             int y = 0;
    -        int n = 0;
    -        int state = 0;
    +        boolean consumeWidthValue = false;
    +        boolean consumeHeightValue = false;
    +        boolean validWidthConsumed = false;
    +        boolean validHeightConsumed = false;
    +        // number of tokens seen as part of this define statement
    +        int defineTokenCount = 0;
             byte[] raster = null;
             IndexColorModel model = null;
    +        int charCount = 0;
     
    -        String matchRegex = "\\s*(0[xX])?((?:(?!,|\\};).)+)(,|\\};)";
    -        String replaceRegex = "0[xX]|,|\\s+|\\};";
    -
    -        String line;
    -        int lineNum = 0;
    -
    -        try (BufferedReader br = new BufferedReader(new InputStreamReader(input))) {
    -            // loop to process XBM header - width, height and create raster
    -            while (!aborted && (line = br.readLine()) != null
    -                    && lineNum <= HEADER_SCAN_LIMIT) {
    -                lineNum++;
    -                // process #define stmts
    -                if (line.trim().startsWith("#define")) {
    -                    String[] token = line.split("\\s+");
    -                    if (token.length != 3) {
    -                        error("Error while parsing define statement");
    +        //read header info
    +        while (!aborted && (c = input.read()) != -1) {
    +            charCount++;
    +            if (charCount > MAX_CHAR_LIMIT) {
    +                error("Incomplete image after reading "
    +                    + "the maximum allowed number of characters: "
    +                    + MAX_CHAR_LIMIT);
    +            }
    +            if ('a' <= c && c <= 'z' ||
    +                'A' <= c && c <= 'Z' ||
    +                '0' <= c && c <= '9' || c == '#' || c == '_') {
    +                if (i < nm.length) {
    +                    nm[i++] = (char) c;
    +                } else {
    +                    error("XBM header contains literal greater than size 80");
    +                }
    +            } else if (i > 0) {
    +                int nc = i;
    +                i = 0;
    +                if (defineTokenCount >= 1) {
    +                    // we are inside a #define line
    +                    defineTokenCount++;
    +                }
    +                if (defineTokenCount == 0) {
    +                    if (nc == 7 &&
    +                        nm[0] == '#' &&
    +                        nm[1] == 'd' &&
    +                        nm[2] == 'e' &&
    +                        nm[3] == 'f' &&
    +                        nm[4] == 'i' &&
    +                        nm[5] == 'n' &&
    +                        nm[6] == 'e')
    +                    {
    +                        defineTokenCount++;
    +                        continue;
    +                    }
    +                } else if (defineTokenCount == 2) {
    +                    // consume second token in #define line
    +                    if (state < 2) {
    +                        if (nm[nc - 1] == 'h' &&
    +                            !validWidthConsumed) {
    +                            consumeWidthValue = true;
    +                        } else if ((nm[nc - 1] == 't' && nc > 1 &&
    +                                    nm[nc - 2] == 'h') &&
    +                                   !validHeightConsumed) {
    +                            consumeHeightValue = true;
    +                        }
                         }
    -                    try {
    -                        if (state < 2) {
    -                            if (token[1].endsWith("h")) {
    -                                W = Integer.parseInt(token[2]);
    -                            } else if (token[1].endsWith("ht")) {
    -                                H = Integer.parseInt(token[2]);
    +                } else if (defineTokenCount == 3) {
    +                    defineTokenCount = 0;
    +                    // consume third token in #define line
    +                    int n = 0;
    +                    for (int p = 0; p < nc; p++) {
    +                        if ('0' <= (c = nm[p]) && c <= '9') {
    +                            n = n * 10 + c - '0';
    +                            if (n > MAX_XBM_SIZE) {
    +                                error("Width/Height cannot be more than: "
    +                                    + MAX_XBM_SIZE);
                                 }
    -                            // After the 1st dimension is set, state becomes 1;
    -                            // after the 2nd dimension is set, state becomes 2
    -                            ++state;
    +                        } else {
    +                            error("Invalid width/height value");
                             }
    -                    } catch (NumberFormatException nfe) {
    -                        // parseInt() can throw NFE
    -                        error("Error while parsing width or height.");
                         }
    -                }
     
    -                if (state == 2) {
    -                    if (W <= 0 || H <= 0) {
    -                        error("Invalid values for width or height.");
    +                    if (n > 0 && (consumeWidthValue || consumeHeightValue)) {
    +                        if (consumeWidthValue) {
    +                            if (!validWidthConsumed) {
    +                                W = n;
    +                                validWidthConsumed = true;
    +                                state++;
    +                            }
    +                            consumeWidthValue = false;
    +                        } else if (consumeHeightValue) {
    +                            if (!validHeightConsumed) {
    +                                H = n;
    +                                validHeightConsumed = true;
    +                                state++;
    +                            }
    +                            consumeHeightValue = false;
    +                        }
                         }
    -                    if (multiplyExact(W, H) > MAX_XBM_SIZE) {
    -                        error("Large XBM file size."
    +                    // verify the consumed width & height value and initialize
    +                    // required constructs
    +                    if (state == 2) {
    +                        if (multiplyExact(W, H) > MAX_XBM_SIZE) {
    +                            error("Large XBM file size."
                                     + " Maximum allowed size: " + MAX_XBM_SIZE);
    -                    }
    -                    model = new IndexColorModel(8, 2, XbmColormap,
    +                        }
    +                        model = new IndexColorModel(8, 2, XbmColormap,
                                 0, false, 0);
    -                    setDimensions(W, H);
    -                    setColorModel(model);
    -                    setHints(XbmHints);
    -                    headerComplete();
    -                    raster = new byte[W];
    -                    state = 3;
    -                    break;
    +                        setDimensions(W, H);
    +                        setColorModel(model);
    +                        setHints(XbmHints);
    +                        headerComplete();
    +                        raster = new byte[W];
    +                        state = 3;
    +                        break;
    +                    }
                     }
                 }
    +        }
     
    -            if (state != 3) {
    -                error("Width or Height of XBM file not defined");
    -            }
    -
    -            boolean contFlag = false;
    -            StringBuilder sb = new StringBuilder();
    -
    -            // loop to process image data
    -            while (!aborted && (line = br.readLine()) != null) {
    -                lineNum++;
    -
    -                if (!contFlag) {
    -                    if (line.contains("[]")) {
    -                        contFlag = true;
    -                    } else {
    -                        continue;
    -                    }
    -                }
    +        if (state != 3) {
    +            error("Width or Height of XBM file not defined");
    +        }
     
    -                int end = line.indexOf(';');
    -                if (end >= 0) {
    -                    sb.append(line, 0, end + 1);
    -                    break;
    -                } else {
    -                    sb.append(line).append(System.lineSeparator());
    -                }
    +        // skip until we find '{'
    +        boolean imageDataStarted = false;
    +        while (!aborted && (c = input.read()) != -1) {
    +            charCount++;
    +            if (charCount > MAX_CHAR_LIMIT) {
    +                error("Incomplete image after reading "
    +                    + "the maximum allowed number of characters: "
    +                    + MAX_CHAR_LIMIT);
                 }
    +            if (c == '{') {
    +                imageDataStarted = true;
    +                break;
    +            }
    +        }
     
    -            String resultLine = sb.toString();
    -            int cutOffIndex = resultLine.indexOf('{');
    -            resultLine = resultLine.substring(cutOffIndex + 1);
    -
    -            Matcher matcher = Pattern.compile(matchRegex).matcher(resultLine);
    -            while (matcher.find()) {
    -                if (y >= H) {
    -                    error("Scan size of XBM file exceeds"
    -                            + " the defined width x height");
    -                }
    +        if (!imageDataStarted) {
    +            error("Missing '{' at the start of image data");
    +        }
     
    -                int startIndex = matcher.start();
    -                int endIndex = matcher.end();
    -                String hexByte = resultLine.substring(startIndex, endIndex);
    -                hexByte = hexByte.replaceAll("^\\s+", "");
    +        // used to make sure that we have the final delimiter '};',
    +        // while parsing the image data
    +        int previousChar = '{';
    +        // parse image data
    +        boolean imageDataTerminated = false;
    +        while (!aborted && (c = input.read()) != -1) {
    +            charCount++;
    +            if (charCount > MAX_CHAR_LIMIT) {
    +                error("Incomplete image after reading "
    +                    + "the maximum allowed number of characters: "
    +                    + MAX_CHAR_LIMIT);
    +            }
     
    -                if (!(hexByte.startsWith("0x")
    -                        || hexByte.startsWith("0X"))) {
    -                    error("Invalid hexadecimal number at Ln#:" + lineNum
    -                            + " Col#:" + (startIndex + 1));
    -                }
    -                hexByte = hexByte.replaceAll(replaceRegex, "");
    -                if (hexByte.length() != 2) {
    -                    error("Invalid hexadecimal number at Ln#:" + lineNum
    -                            + " Col#:" + (startIndex + 1));
    +            if (c == ';') {
    +                if (previousChar != '}') {
    +                    error("Abrupt end of image data without '};' delimiter");
                     }
    +                imageDataTerminated = true;
    +                break;
    +            }
    +            if (!Character.isWhitespace(c)) {
    +                previousChar = c;
    +            }
     
    -                try {
    -                    n = Integer.parseInt(hexByte, 16);
    -                } catch (NumberFormatException nfe) {
    -                    error("Error parsing hexadecimal at Ln#:" + lineNum
    -                            + " Col#:" + (startIndex + 1));
    +            if (',' != c && '}' != c &&
    +                !Character.isWhitespace(c)) {
    +                nm[i++] = (char) c;
    +                if (i > 4) {
    +                    error("Image hex data should be 3 or 4 characters long");
                     }
    -                for (int mask = 1; mask <= 0x80; mask <<= 1) {
    -                    if (x < W) {
    -                        if ((n & mask) != 0)
    -                            raster[x] = 1;
    +            } else if (i == 3 || i == 4) {
    +                // consume valid hex image data
    +                int n = 0;
    +                int nc = i;
    +                i = 0;
    +                if (nm[0] == '0' &&
    +                    (nm[1] == 'x' || nm[1] == 'X')) {
    +                    for (int p = 2; p < nc; p++) {
    +                        c = nm[p];
    +                        if ('0' <= c && c <= '9')
    +                            c = c - '0';
    +                        else if ('A' <= c && c <= 'F')
    +                            c = c - 'A' + 10;
    +                        else if ('a' <= c && c <= 'f')
    +                            c = c - 'a' + 10;
                             else
    -                            raster[x] = 0;
    +                            error("Corrupt hex image data");
    +                        n = n * 16 + c;
                         }
    -                    x++;
    -                }
    -
    -                if (x >= W) {
    -                    int result = setPixels(0, y, W, 1, model, raster, 0, W);
    -                    if (result <= 0) {
    -                        error("Unexpected error occurred during setPixel()");
    +                    for (int mask = 1; mask <= 0x80; mask <<= 1) {
    +                        if (x < W) {
    +                            if ((n & mask) != 0)
    +                                raster[x] = 1;
    +                            else
    +                                raster[x] = 0;
    +                        }
    +                        x++;
                         }
    -                    x = 0;
    -                    y++;
    +                    if (x >= W) {
    +                        if ((y + 1) > H) {
    +                            error("Scan size of XBM file exceeds"
    +                                + " the defined width x height");
    +                        }
    +                        if (setPixels(0, y, W, 1, model, raster, 0, W) == 0) {
    +                            error("Unexpected error occurred during setPixel()");
    +                        }
    +                        x = 0;
    +                        y++;
    +                    }
    +                } else {
    +                    error("Corrupt hex image data");
                     }
    +            } else if (i == 1 || i == 2) {
    +                error("Image hex data should be 3 or 4 characters long");
                 }
    -            imageComplete(ImageConsumer.STATICIMAGEDONE, true);
             }
    +        if (!imageDataTerminated) {
    +            error("Missing terminator ';'");
    +        }
    +        input.close();
    +        imageComplete(ImageConsumer.STATICIMAGEDONE, true);
         }
     }
    
    From ff8f6e1ef7eae930a2c55af2fd13674c7b2f0cbc Mon Sep 17 00:00:00 2001
    From: Artur Barashev 
    Date: Thu, 21 May 2026 21:05:25 +0000
    Subject: [PATCH 15/17] 8385076: Follow-on fix for Improve certification
     checking
    
    Reviewed-by: ahgross, rhalade, jnibedita, pkumaraswamy, weijun, mullan
    ---
     .../share/classes/sun/security/x509/DNSName.java     | 12 ++++++++++--
     .../sun/security/x509/NameConstraintsExtension.java  | 10 ++++++++--
     2 files changed, 18 insertions(+), 4 deletions(-)
    
    diff --git a/src/java.base/share/classes/sun/security/x509/DNSName.java b/src/java.base/share/classes/sun/security/x509/DNSName.java
    index 17820d279a50..02948c0c04fe 100644
    --- a/src/java.base/share/classes/sun/security/x509/DNSName.java
    +++ b/src/java.base/share/classes/sun/security/x509/DNSName.java
    @@ -229,11 +229,13 @@ public int hashCode() {
          * order zero bit.
          *
          * @param inputName to be checked for being constrained
    +     * @param matchWildcard whether to match a wildcard in inputName
          * @return constraint type above
          * @throws UnsupportedOperationException if name is not exact match, but narrowing and widening are
          *          not supported for this name type.
          */
    -    public int constrains(GeneralNameInterface inputName) throws UnsupportedOperationException {
    +    public int constrains(GeneralNameInterface inputName, boolean matchWildcard)
    +            throws UnsupportedOperationException {
             int constraintType;
             if (inputName == null)
                 constraintType = NAME_DIFF_TYPE;
    @@ -244,7 +246,9 @@ else if (inputName.getType() != NAME_DNS)
                     (((DNSName)inputName).getName()).toLowerCase(Locale.ENGLISH);
                 String thisName = name.toLowerCase(Locale.ENGLISH);
     
    -            if (HOSTNAME_CHECKER.isMatched(thisName, inName, false))
    +            if (inName.equals(thisName) || (matchWildcard
    +                    && inName.contains("*")
    +                    && HOSTNAME_CHECKER.isMatched(thisName, inName, false)))
                     constraintType = NAME_MATCH;
                 else if (thisName.endsWith(inName)) {
                     int inNdx = thisName.lastIndexOf(inName);
    @@ -265,6 +269,10 @@ else if (thisName.endsWith(inName)) {
             return constraintType;
         }
     
    +    public int constrains(GeneralNameInterface inputName) {
    +        return constrains(inputName, false);
    +    }
    +
         /**
          * Return subtree depth of this name for purposes of determining
          * NameConstraints minimum and maximum bounds and for calculating
    diff --git a/src/java.base/share/classes/sun/security/x509/NameConstraintsExtension.java b/src/java.base/share/classes/sun/security/x509/NameConstraintsExtension.java
    index ce6d4721ad72..5b34a7267967 100644
    --- a/src/java.base/share/classes/sun/security/x509/NameConstraintsExtension.java
    +++ b/src/java.base/share/classes/sun/security/x509/NameConstraintsExtension.java
    @@ -1,5 +1,5 @@
     /*
    - * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
    + * Copyright (c) 1997, 2026, Oracle and/or its affiliates. All rights reserved.
      * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
      *
      * This code is free software; you can redistribute it and/or modify it
    @@ -506,9 +506,15 @@ public boolean verify(GeneralNameInterface name) throws IOException {
                     if (exName == null)
                         continue;
     
    +                // Match a wildcard in DNSName only against the excluded subtree
    +                int matchResult =
    +                        exName.getType() == GeneralNameInterface.NAME_DNS
    +                                ? ((DNSName) exName).constrains(name, true)
    +                                : exName.constrains(name);
    +
                     // if name matches or narrows any excluded subtree,
                     // return false
    -                switch (exName.constrains(name)) {
    +                switch (matchResult) {
                     case GeneralNameInterface.NAME_DIFF_TYPE:
                     case GeneralNameInterface.NAME_WIDENS: // name widens excluded
                     case GeneralNameInterface.NAME_SAME_TYPE:
    
    From 18d18af4b14b4e7ab76de2973d3d0604f41461ca Mon Sep 17 00:00:00 2001
    From: Naoto Sato 
    Date: Mon, 20 Jul 2026 18:00:07 +0000
    Subject: [PATCH 16/17] 8388183: Wrong example in documentation for
     String.toLowerCase()
    
    Reviewed-by: iris, jlu
    Backport-of: c6a068a5ee0fcd6719b85103ead1e3fee2b8b42b
    ---
     src/java.base/share/classes/java/lang/String.java | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/src/java.base/share/classes/java/lang/String.java b/src/java.base/share/classes/java/lang/String.java
    index 760f3ebc2550..925f6653ecc4 100644
    --- a/src/java.base/share/classes/java/lang/String.java
    +++ b/src/java.base/share/classes/java/lang/String.java
    @@ -4062,7 +4062,7 @@ public static String join(CharSequence delimiter,
          *   (all)
          *   
          *       ΙΧΘΥΣ
    -     *   ιχθυσ
    +     *   ιχθυς
          *   lowercased all chars in String
          * 
          * 
    
    From e60e178e9f22a0e7ca6e760a52776d484987d772 Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Manuel=20H=C3=A4ssig?= 
    Date: Wed, 22 Jul 2026 13:26:34 +0000
    Subject: [PATCH 17/17] 8387073: C2: StoreNode::Ideal wrongly optimizes away
     scalar store before masked vector store 8388539: [TESTBUG]
     compiler/igvn/TestMaskedStoreIdealization.java fails on Release Builds
     8388523: C2: Test compiler/igvn/TestMaskedStoreIdealization.java failed
    
    Reviewed-by: thartmann, epeter
    Backport-of: 4e30a933bf27da61a203da4bcc7a0e04c7f386c3
    ---
     src/hotspot/share/opto/memnode.cpp            |  17 +-
     test/hotspot/jtreg/TEST.groups                |   3 +-
     .../arraycopy/TestSubwordPartialInlining.java | 151 ++++++
     .../igvn/TestMaskedStoreIdealization.java     | 510 ++++++++++++++++++
     4 files changed, 674 insertions(+), 7 deletions(-)
     create mode 100644 test/hotspot/jtreg/compiler/arraycopy/TestSubwordPartialInlining.java
     create mode 100644 test/hotspot/jtreg/compiler/igvn/TestMaskedStoreIdealization.java
    
    diff --git a/src/hotspot/share/opto/memnode.cpp b/src/hotspot/share/opto/memnode.cpp
    index 5e4aa2f17feb..73fe065664a5 100644
    --- a/src/hotspot/share/opto/memnode.cpp
    +++ b/src/hotspot/share/opto/memnode.cpp
    @@ -3523,8 +3523,9 @@ Node *StoreNode::Ideal(PhaseGVN *phase, bool can_reshape) {
       Node* address = in(MemNode::Address);
       Node* value   = in(MemNode::ValueIn);
       // Back-to-back stores to same address?  Fold em up.  Generally
    -  // unsafe if I have intervening uses.
    -  {
    +  // unsafe if I have intervening uses. Also unsafe for masked or
    +  // scatter vector stores as the wider store.
    +  if (!this->is_StoreVector() || this->Opcode() == Op_StoreVector) {
         Node* st = mem;
         // If Store 'st' has more than one use, we cannot fold 'st' away.
         // For example, 'st' might be the final state at a conditional
    @@ -3532,15 +3533,14 @@ Node *StoreNode::Ideal(PhaseGVN *phase, bool can_reshape) {
         // the same time 'st' is live, which might be unschedulable.  So,
         // require exactly ONE user until such time as we clone 'mem' for
         // each of 'mem's uses (thus making the exactly-1-user-rule hold
    -    // true).
    -    while (st->is_Store() && st->outcnt() == 1) {
    +    // true). Further, 'st' must be a contiguous store, otherwise
    +    // memory_size does not make sense for measuring overlap.
    +    while (st->is_Store() && st->outcnt() == 1 && (!st->is_StoreVector() || st->Opcode() == Op_StoreVector)) {
           // Looking at a dead closed cycle of memory?
           assert(st != st->in(MemNode::Memory), "dead loop in StoreNode::Ideal");
           assert(Opcode() == st->Opcode() ||
                  st->Opcode() == Op_StoreVector ||
                  Opcode() == Op_StoreVector ||
    -             st->Opcode() == Op_StoreVectorScatter ||
    -             Opcode() == Op_StoreVectorScatter ||
                  phase->C->get_alias_index(adr_type()) == Compile::AliasIdxRaw ||
                  (Opcode() == Op_StoreL && st->Opcode() == Op_StoreI) || // expanded ClearArrayNode
                  (Opcode() == Op_StoreI && st->Opcode() == Op_StoreL) || // initialization by arraycopy
    @@ -3549,6 +3549,11 @@ Node *StoreNode::Ideal(PhaseGVN *phase, bool can_reshape) {
     
           if (st->in(MemNode::Address)->eqv_uncast(address) &&
               st->as_Store()->memory_size() <= this->memory_size()) {
    +        assert(!is_predicated_vector() && !is_StoreVectorMasked() &&
    +               !is_StoreVectorScatter() && !is_StoreVectorScatterMasked() &&
    +               !st->is_predicated_vector() && !st->is_StoreVectorMasked() &&
    +               !st->is_StoreVectorScatter() && !st->is_StoreVectorScatterMasked(),
    +               "optimization only correct for full-width stores without holes");
             Node* use = st->raw_out(0);
             if (phase->is_IterGVN()) {
               phase->is_IterGVN()->rehash_node_delayed(use);
    diff --git a/test/hotspot/jtreg/TEST.groups b/test/hotspot/jtreg/TEST.groups
    index e09235f6a390..d9a9b8543d37 100644
    --- a/test/hotspot/jtreg/TEST.groups
    +++ b/test/hotspot/jtreg/TEST.groups
    @@ -184,7 +184,8 @@ tier1_compiler_2 = \
       -compiler/classUnloading/methodUnloading/TestOverloadCompileQueues.java \
       -compiler/codecache/stress \
       -compiler/codegen/aes \
    -  -compiler/gcbarriers/PreserveFPRegistersTest.java
    +  -compiler/gcbarriers/PreserveFPRegistersTest.java \
    +  -compiler/igvn/TestMaskedStoreIdealization.java
     
     tier1_compiler_3 = \
       compiler/intrinsics/ \
    diff --git a/test/hotspot/jtreg/compiler/arraycopy/TestSubwordPartialInlining.java b/test/hotspot/jtreg/compiler/arraycopy/TestSubwordPartialInlining.java
    new file mode 100644
    index 000000000000..42b5e6b8b361
    --- /dev/null
    +++ b/test/hotspot/jtreg/compiler/arraycopy/TestSubwordPartialInlining.java
    @@ -0,0 +1,151 @@
    +/*
    + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
    + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
    + *
    + * This code is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU General Public License version 2 only, as
    + * published by the Free Software Foundation.
    + *
    + * This code is distributed in the hope that it will be useful, but WITHOUT
    + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    + * version 2 for more details (a copy is included in the LICENSE file that
    + * accompanied this code).
    + *
    + * You should have received a copy of the GNU General Public License version
    + * 2 along with this work; if not, write to the Free Software Foundation,
    + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
    + *
    + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
    + * or visit www.oracle.com if you need additional information or have any
    + * questions.
    + */
    +
    +/**
    + * @test
    + * @bug 8387073
    + * @key randomness
    + * @summary Arrays.copyOf(Range) must pad overized elements with 0 with partial inlining as well.
    + * @library /test/lib /
    + * @run driver ${test.main.class}
    + */
    +
    +package compiler.arraycopy;
    +
    +import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.Random;
    +import java.util.Set;
    +import java.util.stream.*;
    +
    +
    +import jdk.test.lib.Utils;
    +import compiler.lib.compile_framework.*;
    +import compiler.lib.template_framework.*;
    +import static compiler.lib.template_framework.Template.scope;
    +import static compiler.lib.template_framework.Template.let;
    +import compiler.lib.template_framework.library.*;
    +
    +public class TestSubwordPartialInlining {
    +    private static final Random RANDOM = Utils.getRandomInstance();
    +    private static final String PACKAGE = "compiler.arraycopy.generated";
    +    private static final String CLASS_NAME = "TestSubwordPartialInliningGenerated";
    +
    +    public static void main(String[] args) {
    +        final CompileFramework comp = new CompileFramework();
    +        comp.addJavaSourceCode(PACKAGE + "." + CLASS_NAME, generate(comp));
    +        comp.compile();
    +        comp.invoke(PACKAGE + "." + CLASS_NAME, "main", new Object[] { args });
    +    }
    +
    +    private static String generate(CompileFramework comp) {
    +        final Set imports = Set.of("java.util.Arrays",
    +                                           "java.util.Random",
    +                                           "jdk.test.lib.Utils",
    +                                           "compiler.lib.generators.*");
    +
    +        final List tests = new ArrayList<>();
    +        tests.addAll(Stream.of(CodeGenerationDataNameType.booleans(), CodeGenerationDataNameType.bytes(), CodeGenerationDataNameType.shorts(), CodeGenerationDataNameType.chars())
    +                           .map(pty -> new TestPerType(pty).generate())
    +                           .toList());
    +        tests.add(PrimitiveType.generateLibraryRNG());
    +
    +        return TestFrameworkClass.render(PACKAGE, CLASS_NAME, imports, comp.getEscapedClassPathOfCompiledClasses(), tests);
    +    }
    +
    +    enum Operation {
    +        COPY_OF,
    +        COPY_OF_RANGE
    +    }
    +
    +    record TestPerType(PrimitiveType pty) {
    +        private String getTestName(Operation op) {
    +            return switch (op) {
    +                case COPY_OF       -> "CopyOf" + pty.boxedTypeName();
    +                case COPY_OF_RANGE -> "CopyOfRange" + pty.boxedTypeName();
    +            };
    +        }
    +
    +        TemplateToken generate() {
    +            final int maxSize = RANDOM.nextInt(0, 4) == 0 ? RANDOM.nextInt(5, 100) : 4;
    +            final int inputSize = RANDOM.nextInt(1, maxSize);
    +            final int copySize = RANDOM.nextInt(inputSize + 1, maxSize + 1);
    +
    +
    +            var runTemplate = Template.make("op", (Operation op) -> scope(
    +                let("pty", pty),
    +                let("testName", getTestName(op)),
    +                """
    +                    @Run(test = "test#{testName}", mode = RunMode.STANDALONE)
    +                    static void run#{testName}() {
    +                        final #pty[] intRes = test#{testName}();
    +                        for (int i = 0; i < 10_000; i++) {
    +                            test#{testName}();
    +                        }
    +                        final #pty[] compRes = test#{testName}();
    +                        if (!Arrays.equals(intRes, compRes)) {
    +                            throw new RuntimeException("wrong result:\\n" +
    +                                                       "  interpreter result: " + Arrays.toString(intRes) + "\\n" +
    +                                                       "  compiled result: " + Arrays.toString(compRes));
    +                        }
    +                    }
    +                """
    +            ));
    +
    +            var testTemplate = Template.make("op", (Operation op) -> scope(
    +                let("pty", pty),
    +                let("testName", getTestName(op)),
    +                let("len", copySize),
    +                let("minRange", RANDOM.nextInt(0, 4) == 0 ? RANDOM.nextInt(0, inputSize) : 0),
    +                """
    +                    @Test
    +                    static #pty[] test#{testName}() {
    +                """,
    +                "       return ",
    +                switch (op) {
    +                    case COPY_OF       -> "Arrays.copyOf(#{pty}Arr, #len)";
    +                    case COPY_OF_RANGE -> "Arrays.copyOfRange(#{pty}Arr, #minRange, #len)";
    +                },
    +                ";\n",
    +                """
    +                    }
    +
    +                """
    +            ));
    +
    +            return Template.make(() -> scope(
    +                Stream.of(Operation.COPY_OF, Operation.COPY_OF_RANGE)
    +                      .map(op -> scope(runTemplate.asToken(op), testTemplate.asToken(op)))
    +                      .toList(),
    +                Hooks.CLASS_HOOK.insert(scope(
    +                    let("pty", pty),
    +                    let("arr", String.join(", ", Collections.nCopies(inputSize, (String) pty.callLibraryRNG()))),
    +                    """
    +                        private static #pty[] #{pty}Arr = { #arr };
    +                    """
    +                ))
    +            )).asToken();
    +        }
    +    }
    +}
    diff --git a/test/hotspot/jtreg/compiler/igvn/TestMaskedStoreIdealization.java b/test/hotspot/jtreg/compiler/igvn/TestMaskedStoreIdealization.java
    new file mode 100644
    index 000000000000..a9950d128e24
    --- /dev/null
    +++ b/test/hotspot/jtreg/compiler/igvn/TestMaskedStoreIdealization.java
    @@ -0,0 +1,510 @@
    +/*
    + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
    + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
    + *
    + * This code is free software; you can redistribute it and/or modify it
    + * under the terms of the GNU General Public License version 2 only, as
    + * published by the Free Software Foundation.
    + *
    + * This code is distributed in the hope that it will be useful, but WITHOUT
    + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    + * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    + * version 2 for more details (a copy is included in the LICENSE file that
    + * accompanied this code).
    + *
    + * You should have received a copy of the GNU General Public License version
    + * 2 along with this work; if not, write to the Free Software Foundation,
    + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
    + *
    + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
    + * or visit www.oracle.com if you need additional information or have any
    + * questions.
    + */
    +
    +/**
    + * @test
    + * @bug 8387073
    + * @key randomness
    + * @summary Narrower stores preceding masked vector stores must not be eliminated.
    + * @requires vm.flagless
    + * @modules jdk.incubator.vector
    + * @library /test/lib /
    + * @run driver ${test.main.class}
    + */
    +
    +package compiler.igvn;
    +
    +import java.util.Arrays;
    +import java.util.ArrayList;
    +import java.util.Collections;
    +import java.util.List;
    +import java.util.Random;
    +import java.util.Set;
    +import java.util.stream.*;
    +
    +import jdk.incubator.vector.VectorShape;
    +
    +import jdk.test.lib.Utils;
    +import compiler.lib.compile_framework.*;
    +import compiler.lib.template_framework.*;
    +import static compiler.lib.template_framework.Template.scope;
    +import static compiler.lib.template_framework.Template.let;
    +import compiler.lib.template_framework.library.*;
    +
    +public class TestMaskedStoreIdealization {
    +    private static final Random RANDOM = Utils.getRandomInstance();
    +    private static final String PACKAGE = "compiler.igvn.generated";
    +    private static final String CLASS_NAME = "TestMaskedStoreIdealizationGenerated";
    +
    +    public static void main(String[] args) {
    +        final CompileFramework comp = new CompileFramework();
    +        comp.addJavaSourceCode(PACKAGE + "." + CLASS_NAME, generate(comp));
    +        comp.compile("--add-modules=jdk.incubator.vector");
    +
    +        List vmArgs = new ArrayList<>(List.of(
    +            "--add-modules=jdk.incubator.vector",
    +            "--add-opens", "jdk.incubator.vector/jdk.incubator.vector=ALL-UNNAMED"
    +        ));
    +        vmArgs.addAll(Arrays.asList(args)); // Forward args
    +        String[] vmArgsArray = vmArgs.toArray(new String[0]);
    +
    +        comp.invoke(PACKAGE + "." + CLASS_NAME, "main", new Object[] { vmArgsArray });
    +    }
    +
    +    private static String generate(CompileFramework comp) {
    +        final Set imports = Set.of("java.util.Arrays",
    +                                           "java.util.Random",
    +                                           "jdk.incubator.vector.*",
    +                                           "jdk.test.lib.Utils",
    +                                           "compiler.lib.generators.*");
    +
    +        // The preferred vector shape is the largest possible vector size.
    +        final int maxVecByteSize = VectorShape.preferredShape().vectorBitSize() / 8;
    +
    +        final List tests = new ArrayList<>();
    +        // Add tests only for the vector shapes that
    +        tests.addAll(CodeGenerationDataNameType.VECTOR_VECTOR_TYPES
    +                        .stream()
    +                        .filter(vec -> vec.byteSize() <= maxVecByteSize && vec.elementType instanceof PrimitiveType)
    +                        .map(vec -> new TestPerShape(vec).generate())
    +                        .collect(Collectors.toList()));
    +        tests.add(PrimitiveType.generateLibraryRNG());
    +
    +        return TestFrameworkClass.render(PACKAGE, CLASS_NAME, imports, comp.getEscapedClassPathOfCompiledClasses(), tests);
    +    }
    +
    +    enum Operation {
    +        STORE_SCATTER,
    +        STORE_MASK,
    +        STORE_SCATTER_MASK,
    +        STORE_VECTOR_AFTER_SCATTER,
    +        RANDOM
    +    }
    +
    +    record TestPerShape(VectorType.Vector vec) {
    +        TemplateToken generate() {
    +            final String testName = vec.elementType.boxedTypeName() + vec.length;
    +
    +            // Select the index where we set the mask to false. The index is biased to
    +            // zero, as the original bug only triggered with the first element.
    +            final int idx = RANDOM.nextBoolean() ? RANDOM.nextInt(0, vec.length) : 0;
    +
    +            var irVerification = Template.make("op", "arraySize", (Operation op, Integer arraySize) -> {
    +                // No IR-verification for random test cases.
    +                if (op == Operation.RANDOM) {
    +                    return scope("");
    +                }
    +
    +                final PrimitiveType pty = (PrimitiveType) vec.elementType;
    +                final String ptyIR = pty.abbrev().equals("S") ? "C" : pty.abbrev();
    +
    +                // Verify that the Scatter store for STORE_VECTOR_AFTER_SCATTER is not eliminated.
    +                var opVerification = Template.make(() -> {
    +                    if (op != Operation.STORE_VECTOR_AFTER_SCATTER) {
    +                        return scope("");
    +                    }
    +
    +                    if (vec.length <= 2) {
    +                        return scope("    // No Vector nodes are emitted for vectors of length 2 or shorter.\n");
    +                    }
    +
    +                    if (vec.elementType.byteSize() < 4) {
    +                        return scope("    // StoreVectorScatter is not emitted for vectors of subword types.\n");
    +                    }
    +
    +                    return scope(
    +                        """
    +                            @IR(counts = {IRNode.STORE_VECTOR_SCATTER, "=1"},
    +                                applyIfCPUFeatureOr = {"avx512", "true", "sve", "true"})
    +                        """
    +                    );
    +                });
    +
    +                return scope(
    +                    let("pty", vec.elementType.name()),
    +                    let("ptyIR", ptyIR),
    +                    let("idx", idx),
    +                    switch (op) {
    +                        case STORE_MASK, STORE_SCATTER_MASK ->
    +                        // For masked operations, depending on the generated mask and index map C2 does not manage to elide a branch from
    +                        // if (mask.allTrue()) {
    +                        //     intoArray(a, offset);
    +                        // } else {
    +                        //     intoArray(a, offset, mask);
    +                        // }
    +                        // leading to both stores of the diamond being live. This is highly profile dependent and cannot be predicted.
    +                        """
    +                            @IR(counts = {IRNode.START + "Store#{ptyIR}" + IRNode.MID + "(Memory: @aryptr:#{pty}\\\\[int:#{arraySize}\\\\]).*(:NotNull:exact\\\\[\\\\d+\\\\]).*" + IRNode.END, ">=1",
    +                                          IRNode.START + "Store#{ptyIR}" + IRNode.MID + "(Memory: @aryptr:#{pty}\\\\[int:#{arraySize}\\\\]).*(:NotNull:exact\\\\[\\\\d+\\\\]).*" + IRNode.END, "<=2"},
    +                                applyIfCPUFeatureOr = {"avx512", "true", "sve", "true"},
    +                                phase = CompilePhase.BEFORE_MATCHING)
    +                        """;
    +                        case STORE_SCATTER ->
    +                        """
    +                            @IR(counts = {IRNode.START + "Store#{ptyIR}" + IRNode.MID + "(Memory: @aryptr:#{pty}\\\\[int:#{arraySize}\\\\]).*(:NotNull:exact\\\\[\\\\d+\\\\]).*" + IRNode.END, "=1"},
    +                                applyIfCPUFeatureOr = {"avx512", "true", "sve", "true"},
    +                                phase = CompilePhase.BEFORE_MATCHING)
    +                        """;
    +                        case STORE_VECTOR_AFTER_SCATTER -> opVerification.asToken();
    +                        case  RANDOM -> "";
    +                    }
    +                );
    +            });
    +
    +            var testBody = Template.make("testCaseRandom", "op", "arraySize", (Random testCaseRandom, Operation op, Integer arraySize) -> {
    +                if (op == Operation.RANDOM) {
    +                    return scope(generateRandomTest(testCaseRandom).asToken());
    +                }
    +
    +                var maskGeneration = Template.make(() -> scope(
    +                    let("idx", idx),
    +                    let("boxedTy", vec.elementType.boxedTypeName()),
    +                    let("species", vec.speciesName),
    +                    "        VectorMask<#boxedTy> mask = VectorMask.fromLong(#species, ",
    +                    testCaseRandom.nextInt(0, 10) == 0 ? testCaseRandom.nextLong() : "-1 - (1 << #idx)",
    +                    ");\n"
    +                ));
    +
    +                var indexMapGeneration = Template.make(() -> {
    +                    // For the scatter tests, the array is one larger than the number of lanes so we can
    +                    // map indices starting at idx to the next index, omitting idx.
    +                    int[] indexMap = IntStream.range(0, vec.length)
    +                                              .map(i -> i >= idx ? i + 1 : i)
    +                                              .toArray();
    +                    String indexMapStr = Arrays.toString(indexMap)
    +                                               .replace('[', '{')
    +                                               .replace(']', '}');
    +                    return scope(
    +                        let("idx", idx),
    +                        let("len", vec.length),
    +                        let("idxMap", indexMapStr),
    +                        """
    +                                final int[] indexMap = #{idxMap};
    +                        """
    +                    );
    +                });
    +
    +                var generation = switch (op) {
    +                    case STORE_SCATTER, STORE_VECTOR_AFTER_SCATTER -> indexMapGeneration.asToken();
    +                    case STORE_MASK                                -> maskGeneration.asToken();
    +                    case STORE_SCATTER_MASK                        ->
    +                        Template.make(() -> scope(indexMapGeneration.asToken(), maskGeneration.asToken())).asToken();
    +                    case RANDOM                                    -> throw new RuntimeException("unreachable");
    +                };
    +
    +                var initStore  = switch (op) {
    +                    case STORE_SCATTER, STORE_VECTOR_AFTER_SCATTER -> "        v.intoArray(a, 0, indexMap, 0);\n";
    +                    case STORE_MASK                                -> "        v.intoArray(a, 0, mask);\n";
    +                    case STORE_SCATTER_MASK                        -> "        v.intoArray(a, 0, indexMap, 0, mask);\n";
    +                    case RANDOM                                    -> throw new RuntimeException("unreachable");
    +                };
    +
    +                var keepStore = switch (op) {
    +                    case STORE_MASK, STORE_SCATTER, STORE_SCATTER_MASK -> "        a[#idx] = arrVal;\n";
    +                    case STORE_VECTOR_AFTER_SCATTER                    -> "        v.intoArray(a, 0);\n";
    +                    case RANDOM                                        -> throw new RuntimeException("unreachable");
    +                };
    +
    +                var holeStore  = switch (op) {
    +                    case STORE_SCATTER              -> "        v.intoArray(a, 0, indexMap, 0);\n";
    +                    case STORE_MASK                 -> "        v.intoArray(a, 0, mask);\n";
    +                    case STORE_SCATTER_MASK         -> "        v.intoArray(a, 0, indexMap, 0, mask);\n";
    +                    case STORE_VECTOR_AFTER_SCATTER -> "";
    +                    case RANDOM                     -> throw new RuntimeException("unreachable");
    +                };
    +
    +                return scope(
    +                    let("pty", vec.elementType.name()),
    +                    let("vecTy", vec.name()),
    +                    let("species", vec.speciesName),
    +                    let("idx", idx),
    +                    """
    +                            #pty[] a = new #pty[#arraySize];
    +                    """,
    +                    generation,
    +                    """
    +
    +                            var v = #vecTy.broadcast(#species, broadcastVal);
    +                    """,
    +                    initStore,
    +                    keepStore,
    +                    holeStore,
    +                    """
    +                            return a;
    +                    """
    +                );
    +            });
    +
    +            var testCase = Template.make("op", (Operation op) -> {
    +                // To get the same test body twice, use a new random instance for each generated test body with a seed fixed per test case.
    +                final int testCaseSeed = RANDOM.nextInt();
    +
    +                String testCaseName = testName + switch (op) {
    +                    case STORE_SCATTER              -> "Scatter";
    +                    case STORE_MASK                 -> "Mask";
    +                    case STORE_SCATTER_MASK         -> "ScatterMask";
    +                    case STORE_VECTOR_AFTER_SCATTER -> "VectorAfterScatter";
    +                    case RANDOM                     -> "Random";
    +                };
    +
    +                // The array size needs to be one larger than the number of lanes for scatter tests, so we can not write to one element.
    +                final int arraySize = switch (op) {
    +                    case STORE_SCATTER, STORE_SCATTER_MASK, STORE_VECTOR_AFTER_SCATTER -> vec.length + 1;
    +                    case STORE_MASK, RANDOM                                            -> vec.length;
    +                };
    +
    +                return scope(
    +                    let("pty", vec.elementType.name()),
    +                    let("testCaseName", testCaseName),
    +                    let("boxedTy", vec.elementType.boxedTypeName()),
    +                    let("vecTy", vec.name()),
    +                    let("lanes", vec.length),
    +                    let("species", vec.speciesName),
    +                    let("idx", idx),
    +                    let("rngCall", vec.elementType.callLibraryRNG()),
    +                    let("broadcastVal", vec.elementType.con()),
    +                    let("arrVal", vec.elementType.con()),
    +                """
    +                    @Run(test = "test#{testCaseName}")
    +                    @Warmup(10_000)
    +                    static void run#{testCaseName}(RunInfo info) {
    +                        final #pty broadcastVal = #broadcastVal;
    +                        final #pty arrVal = #arrVal;
    +                        final #pty[] compiledResult = test#{testCaseName}(broadcastVal, arrVal);
    +
    +                        if (!info.isWarmUp()) {
    +                            final #pty[] interpreterResult = reference#{testCaseName}(broadcastVal, arrVal);
    +                            if (!Arrays.equals(interpreterResult, compiledResult)) {
    +                                throw new RuntimeException("wrong result for test${testCaseName}:\\n" +
    +                                                           "  interpreter result: " + Arrays.toString(interpreterResult) + "\\n" +
    +                                                           "  compiled result: " + Arrays.toString(compiledResult));
    +                            }
    +                        }
    +                    }
    +
    +                    @Test
    +                """,
    +                    irVerification.asToken(op, arraySize),
    +                """
    +                    static #pty[] test#{testCaseName}(#pty broadcastVal, #pty arrVal) {
    +                """,
    +                    testBody.asToken(new Random(testCaseSeed), op, arraySize),
    +                """
    +                    }
    +
    +                    @DontCompile
    +                    static #pty[] reference#{testCaseName}(#pty broadcastVal, #pty arrVal) {
    +                """,
    +                    testBody.asToken(new Random(testCaseSeed), op, arraySize),
    +                """
    +                    }
    +
    +                """
    +                );
    +            });
    +
    +            return Template.make(() -> scope(
    +                Stream.of(Operation.class.getEnumConstants())
    +                         .map(op -> testCase.asToken(op))
    +                         .toList()
    +            )).asToken();
    +        }
    +
    +        Template.ZeroArgs generateRandomTest(Random testCaseRandom) {
    +            final int arraySize = testCaseRandom.nextInt(vec.length + 1, 5 * vec.length);
    +            var maskHook = new Hook("MaskHook");
    +            var genRandomMask = Template.make("maskName", "vecTy", (String maskName, VectorType.Vector vecTy) -> scope(
    +                let("boxedTy", vecTy.elementType.boxedTypeName()),
    +                let("species", vecTy.speciesName),
    +                let("maskVal", testCaseRandom.nextLong()),
    +                """
    +                        VectorMask<#{boxedTy}> #maskName = VectorMask.fromLong(#species, #maskVal);
    +                """
    +            ));
    +            var genRandomIdxMap = Template.make("mapName", "maxIdx", "len", (String mapName, Integer maxIdx, Integer len) -> {
    +                ArrayList possibleIndices = new ArrayList(IntStream.range(0, maxIdx).boxed().toList());
    +                Collections.shuffle(possibleIndices, testCaseRandom);
    +                return scope(
    +                    let("map", String.join(", ", possibleIndices.stream().limit(vec.length).map(i -> i.toString()).toList())),
    +                """
    +                        int[] #mapName = { #map };
    +                """
    +                );
    +            });
    +            var initialStore = Template.make(() -> {
    +                final int offset = testCaseRandom.nextInt(0, 4) == 0 ? testCaseRandom.nextInt(0, arraySize - vec.length) : 0;
    +                return scope(
    +                    let("offset", offset),
    +                    switch (testCaseRandom.nextInt(0,4)) {
    +                        case 0 -> scope(
    +                            """
    +                                    v.intoArray(a, #offset);
    +                            """
    +                            );
    +                        case 1 -> scope(
    +                            maskHook.insert(genRandomMask.asToken("initMask", vec)),
    +                            """
    +                                    v.intoArray(a, #offset, initMask);
    +                            """
    +                            );
    +                        case 2 -> scope(
    +                            maskHook.insert(genRandomIdxMap.asToken("initMap", arraySize - offset, vec.length)),
    +                            """
    +                                    v.intoArray(a, #offset, initMap, 0);
    +                            """
    +                            );
    +                        case 3 -> scope(
    +                            maskHook.insert(scope(
    +                                genRandomIdxMap.asToken("initMap", arraySize - offset, vec.length),
    +                                genRandomMask.asToken("initMask", vec)
    +                            )),
    +                            """
    +                                    v.intoArray(a, #offset, initMap, 0, initMask);
    +                            """
    +                            );
    +                        default -> throw new RuntimeException("unreachable");
    +                    }
    +                );
    +            });
    +
    +            var storeThatShouldNotBeLost = Template.make(() -> {
    +                final int selection = testCaseRandom.nextInt(0,6);
    +                final VectorType.Vector narrowerVector = CodeGenerationDataNameType.VECTOR_VECTOR_TYPES
    +                                                            .stream()
    +                                                            .filter(v -> v.elementType == vec.elementType && v.length <= vec.length)
    +                                                            // Flip a coin on each reduction step to pick a random element.
    +                                                            .reduce(null, (l, r) -> l == null ? r : (testCaseRandom.nextBoolean() ? l : r));
    +                final int offset = testCaseRandom.nextInt(0, 4) == 0 ? testCaseRandom.nextInt(0, arraySize - narrowerVector.length) : 0;
    +                return scope(
    +                    let("offset", offset),
    +                    switch (selection) {
    +                        case 0, 1, 2, 3 -> scope(
    +                            let("vecTy", narrowerVector),
    +                            let("species", narrowerVector.speciesName),
    +                            """
    +                                    var vKeep = #vecTy.broadcast(#species, arrVal);
    +                            """
    +                            );
    +                        default -> "";
    +                    },
    +                    switch (selection) {
    +                        case 0 -> scope(
    +                            """
    +                                    vKeep.intoArray(a, #offset);
    +                            """
    +                            );
    +                        case 1 -> scope(
    +                            maskHook.insert(genRandomMask.asToken("keepMask", narrowerVector)),
    +                            """
    +                                    vKeep.intoArray(a, #offset, keepMask);
    +                            """
    +                            );
    +                        case 2 -> scope(
    +                            maskHook.insert(genRandomIdxMap.asToken("keepMap", arraySize - offset, narrowerVector.length)),
    +                            """
    +                                    vKeep.intoArray(a, #offset, keepMap, 0);
    +                            """
    +                            );
    +                        case 3 -> scope(
    +                            maskHook.insert(scope(
    +                                genRandomIdxMap.asToken("keepMap", arraySize - offset, narrowerVector.length),
    +                                genRandomMask.asToken("keepMask", narrowerVector)
    +                            )),
    +                            """
    +                                    vKeep.intoArray(a, #offset, keepMap, 0, keepMask);
    +                            """
    +                            );
    +                        case 4 -> scope(
    +                            let("idx", testCaseRandom.nextInt(0, arraySize)),
    +                            """
    +                                    a[#idx] = arrVal;
    +                            """
    +                        );
    +                        case 5 -> scope(
    +                            let("idx", testCaseRandom.nextInt(0, arraySize - vec.length)),
    +                            let("len", testCaseRandom.nextInt(1, vec.length + 1)),
    +                            """
    +                                    Arrays.fill(a, #idx, #idx + #len, arrVal);
    +                            """
    +                        );
    +                        default -> throw new RuntimeException("unreachable");
    +                    }
    +                );
    +            });
    +
    +            var storeWithHole = Template.make(() -> {
    +                final int offset = testCaseRandom.nextInt(0, 4) == 0 ? testCaseRandom.nextInt(0, arraySize - vec.length) : 0;
    +                return scope(
    +                    let("offset", offset),
    +                    switch (testCaseRandom.nextInt(0,3)) {
    +                        case 0 -> scope(
    +                            maskHook.insert(genRandomMask.asToken("holeMask", vec)),
    +                            """
    +                                    v.intoArray(a, #offset, holeMask);
    +                            """
    +                            );
    +                        case 1 -> scope(
    +                            maskHook.insert(genRandomIdxMap.asToken("holeMap", arraySize - offset, vec.length)),
    +                            """
    +                                    v.intoArray(a, #offset, holeMap, 0);
    +                            """
    +                            );
    +                        case 2 -> scope(
    +                            maskHook.insert(scope(
    +                                genRandomIdxMap.asToken("holeMap", arraySize - offset, vec.length),
    +                                genRandomMask.asToken("holeMask", vec)
    +                            )),
    +                            """
    +                                    v.intoArray(a, #offset, holeMap, 0, holeMask);
    +                            """
    +                            );
    +                        default -> throw new RuntimeException("unreachable");
    +                    }
    +                );
    +            });
    +
    +            return Template.make(() -> scope(
    +                let("pty", vec.elementType.name()),
    +                let("arraySize", arraySize),
    +                """
    +                        #pty[] a = new #pty[#arraySize];
    +
    +                """,
    +                maskHook.anchor(scope(
    +                    // The code for generating masks and index maps goes here.
    +                    let("vecTy", vec.name()),
    +                    let("species", vec.speciesName),
    +                """
    +                        var v = #vecTy.broadcast(#species, broadcastVal);
    +                """,
    +                    initialStore.asToken(),
    +                    storeThatShouldNotBeLost.asToken(),
    +                    storeWithHole.asToken()
    +                )),
    +                """
    +                        return a;
    +                """
    +            ));
    +        }
    +    }
    +}