From 26460b6f12ce0763b79acfd98fca260b509a82c5 Mon Sep 17 00:00:00 2001 From: Alan Bateman Date: Tue, 18 Nov 2025 08:06:18 +0000 Subject: [PATCH] 8353835: Implement JEP 500: Prepare to Make Final Mean Final Reviewed-by: liach, vlivanov, dholmes, vyazici --- make/test/JtregNativeJdk.gmk | 1 + src/hotspot/share/ci/ciField.cpp | 12 +- src/hotspot/share/prims/jni.cpp | 30 + src/hotspot/share/prims/jniCheck.cpp | 31 +- src/hotspot/share/runtime/arguments.cpp | 22 +- src/hotspot/share/runtime/fieldDescriptor.cpp | 10 + src/hotspot/share/runtime/fieldDescriptor.hpp | 4 +- .../share/classes/java/lang/Module.java | 161 ++++- .../share/classes/java/lang/ModuleLayer.java | 14 +- .../share/classes/java/lang/System.java | 21 +- .../java/lang/invoke/MethodHandles.java | 10 +- .../java/lang/reflect/AccessibleObject.java | 14 +- .../classes/java/lang/reflect/Field.java | 412 ++++++++++++- .../java/lang/reflect/ReflectAccess.java | 7 +- .../reflect/doc-files/MutationMethods.html | 69 +++ .../jdk/internal/access/JavaLangAccess.java | 36 +- .../access/JavaLangReflectAccess.java | 8 +- .../event/FinalFieldMutationEvent.java | 47 ++ .../jdk/internal/module/ModuleBootstrap.java | 96 ++- .../classes/jdk/internal/module/Modules.java | 41 +- .../classes/sun/launcher/LauncherHelper.java | 13 + .../launcher/resources/launcher.properties | 10 + src/java.base/share/man/java.md | 38 +- .../jfr/events/FinalFieldMutationEvent.java | 55 ++ .../classes/jdk/jfr/internal/JDKEvents.java | 1 + .../jdk/jfr/internal/MirrorEvents.java | 4 +- .../jdk/jfr/internal/PlatformEventType.java | 1 + src/jdk.jfr/share/conf/jfr/default.jfc | 5 + src/jdk.jfr/share/conf/jfr/profile.jfc | 5 + .../jni/mutateFinals/MutateFinals.java | 356 +++++++++++ .../jni/mutateFinals/MutateFinalsTest.java | 170 ++++++ .../jni/mutateFinals/libMutateFinals.c | 160 +++++ .../lang/invoke/MethodHandlesGeneralTest.java | 3 +- .../TestFieldLookupAccessibility.java | 28 +- .../lang/invoke/unreflect/UnreflectTest.java | 64 +- .../AccessibleObject/HiddenClassTest.java | 58 +- .../java/lang/reflect/Field/NegativeTest.java | 4 +- test/jdk/java/lang/reflect/Field/Set.java | 3 +- .../FinalFieldMutationEventTest.java | 203 +++++++ .../Field/mutateFinals/MutateFinalsTest.java | 358 +++++++++++ .../mutateFinals/cli/CommandLineTest.java | 294 +++++++++ .../cli/CommandLineTestHelper.java | 88 +++ .../mutateFinals/jar/ExecutableJarTest.java | 227 +++++++ .../jar/ExecutableJarTestHelper.java | 97 +++ .../Field/mutateFinals/jar/m/module-info.java | 29 + .../reflect/Field/mutateFinals/jar/m/p/C.java | 35 ++ .../mutateFinals/jni/JNIAttachMutator.java | 123 ++++ .../jni/JNIAttachMutatorTest.java | 124 ++++ .../mutateFinals/jni/libJNIAttachMutator.c | 192 ++++++ .../Field/mutateFinals/jni/m/module-info.java | 29 + .../Field/mutateFinals/jni/m/p/C1.java | 35 ++ .../Field/mutateFinals/jni/m/p/C2.java | 35 ++ .../Field/mutateFinals/jni/m/p/C3.java | 35 ++ .../reflect/Field/mutateFinals/jni/m/q/C.java | 35 ++ .../Field/mutateFinals/modules/Driver.java | 35 ++ .../mutateFinals/modules/m1/module-info.java | 27 + .../mutateFinals/modules/m1/p1/M1Mutator.java | 81 +++ .../mutateFinals/modules/m2/module-info.java | 27 + .../mutateFinals/modules/m2/p2/M2Mutator.java | 81 +++ .../mutateFinals/modules/m3/module-info.java | 27 + .../mutateFinals/modules/m3/p3/M3Mutator.java | 81 +++ .../modules/test/module-info.java | 33 ++ .../modules/test/test/TestMain.java | 556 ++++++++++++++++++ .../test/test/fieldholders/PrivateFields.java | 124 ++++ .../test/test/fieldholders/PublicFields.java | 124 ++++ .../test/test/internal/TestMutator.java | 81 +++ .../modules/test/test/spi/Mutator.java | 88 +++ .../Attributes/NullAndEmptyKeysAndValues.java | 4 +- .../util/logging/FileHandlerLongLimit.java | 4 +- .../metadata/TestLookForUntestedEvents.java | 5 + .../pkcs11/Cipher/CancelMultipart.java | 2 +- .../provider/SecureRandom/DRBGS11n.java | 6 +- .../util/ManifestDigester/FindSection.java | 4 +- .../jdk/jshell/CompletionSuggestionTest.java | 2 +- test/lib/jdk/test/lib/jfr/EventNames.java | 1 + .../bench/java/lang/reflect/FieldSet.java | 151 +++++ 76 files changed, 5311 insertions(+), 196 deletions(-) create mode 100644 src/java.base/share/classes/java/lang/reflect/doc-files/MutationMethods.html create mode 100644 src/java.base/share/classes/jdk/internal/event/FinalFieldMutationEvent.java create mode 100644 src/jdk.jfr/share/classes/jdk/jfr/events/FinalFieldMutationEvent.java create mode 100644 test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinals.java create mode 100644 test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinalsTest.java create mode 100644 test/hotspot/jtreg/runtime/jni/mutateFinals/libMutateFinals.c create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/MutateFinalsTest.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTest.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTestHelper.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTest.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTestHelper.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/p/C.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutator.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutatorTest.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/libJNIAttachMutator.c create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C1.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C2.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C3.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/q/C.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/Driver.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/p1/M1Mutator.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/p2/M2Mutator.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/p3/M3Mutator.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/module-info.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/TestMain.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PrivateFields.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PublicFields.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/internal/TestMutator.java create mode 100644 test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/spi/Mutator.java create mode 100644 test/micro/org/openjdk/bench/java/lang/reflect/FieldSet.java diff --git a/make/test/JtregNativeJdk.gmk b/make/test/JtregNativeJdk.gmk index a204467a77b..0482011f561 100644 --- a/make/test/JtregNativeJdk.gmk +++ b/make/test/JtregNativeJdk.gmk @@ -80,6 +80,7 @@ else BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libExplicitAttach := -pthread BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libImplicitAttach := -pthread + BUILD_JDK_JTREG_LIBRARIES_LDFLAGS_libJNIAttachMutator := -pthread BUILD_JDK_JTREG_EXCLUDE += exerevokeall.c ifeq ($(call isTargetOs, linux), true) BUILD_JDK_JTREG_EXECUTABLES_LIBS_exelauncher := -ldl diff --git a/src/hotspot/share/ci/ciField.cpp b/src/hotspot/share/ci/ciField.cpp index 8f33c17d0e6..19e05784f4d 100644 --- a/src/hotspot/share/ci/ciField.cpp +++ b/src/hotspot/share/ci/ciField.cpp @@ -258,17 +258,7 @@ void ciField::initialize_from(fieldDescriptor* fd) { // not be constant is when the field is a *special* static & final field // whose value may change. The three examples are java.lang.System.in, // java.lang.System.out, and java.lang.System.err. - assert(vmClasses::System_klass() != nullptr, "Check once per vm"); - if (k == vmClasses::System_klass()) { - // Check offsets for case 2: System.in, System.out, or System.err - if (_offset == java_lang_System::in_offset() || - _offset == java_lang_System::out_offset() || - _offset == java_lang_System::err_offset()) { - _is_constant = false; - return; - } - } - _is_constant = true; + _is_constant = !fd->is_mutable_static_final(); } else { // An instance field can be constant if it's a final static field or if // it's a final non-static field of a trusted class (classes in diff --git a/src/hotspot/share/prims/jni.cpp b/src/hotspot/share/prims/jni.cpp index 5af8edbb758..2297ce9b790 100644 --- a/src/hotspot/share/prims/jni.cpp +++ b/src/hotspot/share/prims/jni.cpp @@ -1867,6 +1867,32 @@ address jni_GetDoubleField_addr() { return (address)jni_GetDoubleField; } +static void log_debug_if_final_static_field(JavaThread* current, const char* func_name, InstanceKlass* ik, int offset) { + if (log_is_enabled(Debug, jni)) { + fieldDescriptor fd; + bool found = ik->find_field_from_offset(offset, true, &fd); + assert(found, "bad field offset"); + assert(fd.is_static(), "static/instance mismatch"); + if (fd.is_final() && !fd.is_mutable_static_final()) { + ResourceMark rm(current); + log_debug(jni)("%s mutated final static field %s.%s", func_name, ik->external_name(), fd.name()->as_C_string()); + } + } +} + +static void log_debug_if_final_instance_field(JavaThread* current, const char* func_name, InstanceKlass* ik, int offset) { + if (log_is_enabled(Debug, jni)) { + fieldDescriptor fd; + bool found = ik->find_field_from_offset(offset, false, &fd); + assert(found, "bad field offset"); + assert(!fd.is_static(), "static/instance mismatch"); + if (fd.is_final()) { + ResourceMark rm(current); + log_debug(jni)("%s mutated final instance field %s.%s", func_name, ik->external_name(), fd.name()->as_C_string()); + } + } +} + JNI_ENTRY_NO_PRESERVE(void, jni_SetObjectField(JNIEnv *env, jobject obj, jfieldID fieldID, jobject value)) HOTSPOT_JNI_SETOBJECTFIELD_ENTRY(env, obj, (uintptr_t) fieldID, value); oop o = JNIHandles::resolve_non_null(obj); @@ -1879,6 +1905,7 @@ JNI_ENTRY_NO_PRESERVE(void, jni_SetObjectField(JNIEnv *env, jobject obj, jfieldI o = JvmtiExport::jni_SetField_probe(thread, obj, o, k, fieldID, false, JVM_SIGNATURE_CLASS, (jvalue *)&field_value); } HeapAccess::oop_store_at(o, offset, JNIHandles::resolve(value)); + log_debug_if_final_instance_field(thread, "SetObjectField", InstanceKlass::cast(k), offset); HOTSPOT_JNI_SETOBJECTFIELD_RETURN(); JNI_END @@ -1901,6 +1928,7 @@ JNI_ENTRY_NO_PRESERVE(void, jni_Set##Result##Field(JNIEnv *env, jobject obj, jfi o = JvmtiExport::jni_SetField_probe(thread, obj, o, k, fieldID, false, SigType, (jvalue *)&field_value); \ } \ o->Fieldname##_field_put(offset, value); \ + log_debug_if_final_instance_field(thread, "SetField", InstanceKlass::cast(k), offset); \ ReturnProbe; \ JNI_END @@ -2072,6 +2100,7 @@ JNI_ENTRY(void, jni_SetStaticObjectField(JNIEnv *env, jclass clazz, jfieldID fie JvmtiExport::jni_SetField_probe(thread, nullptr, nullptr, id->holder(), fieldID, true, JVM_SIGNATURE_CLASS, (jvalue *)&field_value); } id->holder()->java_mirror()->obj_field_put(id->offset(), JNIHandles::resolve(value)); + log_debug_if_final_static_field(THREAD, "SetStaticObjectField", id->holder(), id->offset()); HOTSPOT_JNI_SETSTATICOBJECTFIELD_RETURN(); JNI_END @@ -2093,6 +2122,7 @@ JNI_ENTRY(void, jni_SetStatic##Result##Field(JNIEnv *env, jclass clazz, jfieldID JvmtiExport::jni_SetField_probe(thread, nullptr, nullptr, id->holder(), fieldID, true, SigType, (jvalue *)&field_value); \ } \ id->holder()->java_mirror()-> Fieldname##_field_put (id->offset(), value); \ + log_debug_if_final_static_field(THREAD, "SetStaticField", id->holder(), id->offset()); \ ReturnProbe;\ JNI_END diff --git a/src/hotspot/share/prims/jniCheck.cpp b/src/hotspot/share/prims/jniCheck.cpp index 43cc61d7363..5f4cf10ebf4 100644 --- a/src/hotspot/share/prims/jniCheck.cpp +++ b/src/hotspot/share/prims/jniCheck.cpp @@ -233,7 +233,7 @@ functionExit(JavaThread* thr) } static inline void -checkStaticFieldID(JavaThread* thr, jfieldID fid, jclass cls, int ftype) +checkStaticFieldID(JavaThread* thr, jfieldID fid, jclass cls, int ftype, bool setter) { fieldDescriptor fd; @@ -258,10 +258,18 @@ checkStaticFieldID(JavaThread* thr, jfieldID fid, jclass cls, int ftype) !(fd.field_type() == T_ARRAY && ftype == T_OBJECT)) { ReportJNIFatalError(thr, fatal_static_field_mismatch); } + + /* check if setting a final field */ + if (setter && fd.is_final() && !fd.is_mutable_static_final()) { + ResourceMark rm(thr); + stringStream ss; + ss.print("SetStaticField called to mutate final static field %s.%s", k_oop->external_name(), fd.name()->as_C_string()); + ReportJNIWarning(thr, ss.as_string()); + } } static inline void -checkInstanceFieldID(JavaThread* thr, jfieldID fid, jobject obj, int ftype) +checkInstanceFieldID(JavaThread* thr, jfieldID fid, jobject obj, int ftype, bool setter) { fieldDescriptor fd; @@ -287,14 +295,21 @@ checkInstanceFieldID(JavaThread* thr, jfieldID fid, jobject obj, int ftype) ReportJNIFatalError(thr, fatal_wrong_field); /* check for proper field type */ - if (!InstanceKlass::cast(k_oop)->find_field_from_offset(offset, - false, &fd)) + if (!InstanceKlass::cast(k_oop)->find_field_from_offset(offset, false, &fd)) ReportJNIFatalError(thr, fatal_instance_field_not_found); if ((fd.field_type() != ftype) && !(fd.field_type() == T_ARRAY && ftype == T_OBJECT)) { ReportJNIFatalError(thr, fatal_instance_field_mismatch); } + + /* check if setting a final field */ + if (setter && fd.is_final()) { + ResourceMark rm(thr); + stringStream ss; + ss.print("SetField called to mutate final instance field %s.%s", k_oop->external_name(), fd.name()->as_C_string()); + ReportJNIWarning(thr, ss.as_string()); + } } static inline void @@ -1204,7 +1219,7 @@ JNI_ENTRY_CHECKED(ReturnType, \ jfieldID fieldID)) \ functionEnter(thr); \ IN_VM( \ - checkInstanceFieldID(thr, fieldID, obj, FieldType); \ + checkInstanceFieldID(thr, fieldID, obj, FieldType, false); \ ) \ ReturnType result = UNCHECKED()->Get##Result##Field(env,obj,fieldID); \ functionExit(thr); \ @@ -1229,7 +1244,7 @@ JNI_ENTRY_CHECKED(void, \ ValueType val)) \ functionEnter(thr); \ IN_VM( \ - checkInstanceFieldID(thr, fieldID, obj, FieldType); \ + checkInstanceFieldID(thr, fieldID, obj, FieldType, true); \ ) \ UNCHECKED()->Set##Result##Field(env,obj,fieldID,val); \ functionExit(thr); \ @@ -1395,7 +1410,7 @@ JNI_ENTRY_CHECKED(ReturnType, \ functionEnter(thr); \ IN_VM( \ jniCheck::validate_class(thr, clazz, false); \ - checkStaticFieldID(thr, fieldID, clazz, FieldType); \ + checkStaticFieldID(thr, fieldID, clazz, FieldType, false); \ ) \ ReturnType result = UNCHECKED()->GetStatic##Result##Field(env, \ clazz, \ @@ -1423,7 +1438,7 @@ JNI_ENTRY_CHECKED(void, \ functionEnter(thr); \ IN_VM( \ jniCheck::validate_class(thr, clazz, false); \ - checkStaticFieldID(thr, fieldID, clazz, FieldType); \ + checkStaticFieldID(thr, fieldID, clazz, FieldType, true); \ ) \ UNCHECKED()->SetStatic##Result##Field(env,clazz,fieldID,value); \ functionExit(thr); \ diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp index 3748c35f00d..1ef2ee9de0d 100644 --- a/src/hotspot/share/runtime/arguments.cpp +++ b/src/hotspot/share/runtime/arguments.cpp @@ -317,6 +317,10 @@ bool needs_module_property_warning = false; #define ENABLE_NATIVE_ACCESS_LEN 20 #define ILLEGAL_NATIVE_ACCESS "illegal.native.access" #define ILLEGAL_NATIVE_ACCESS_LEN 21 +#define ENABLE_FINAL_FIELD_MUTATION "enable.final.field.mutation" +#define ENABLE_FINAL_FIELD_MUTATION_LEN 27 +#define ILLEGAL_FINAL_FIELD_MUTATION "illegal.final.field.mutation" +#define ILLEGAL_FINAL_FIELD_MUTATION_LEN 28 // Return TRUE if option matches 'property', or 'property=', or 'property.'. static bool matches_property_suffix(const char* option, const char* property, size_t len) { @@ -343,7 +347,9 @@ bool Arguments::internal_module_property_helper(const char* property, bool check if (matches_property_suffix(property_suffix, PATCH, PATCH_LEN) || matches_property_suffix(property_suffix, LIMITMODS, LIMITMODS_LEN) || matches_property_suffix(property_suffix, UPGRADE_PATH, UPGRADE_PATH_LEN) || - matches_property_suffix(property_suffix, ILLEGAL_NATIVE_ACCESS, ILLEGAL_NATIVE_ACCESS_LEN)) { + matches_property_suffix(property_suffix, ILLEGAL_NATIVE_ACCESS, ILLEGAL_NATIVE_ACCESS_LEN) || + matches_property_suffix(property_suffix, ENABLE_FINAL_FIELD_MUTATION, ENABLE_FINAL_FIELD_MUTATION_LEN) || + matches_property_suffix(property_suffix, ILLEGAL_FINAL_FIELD_MUTATION, ILLEGAL_FINAL_FIELD_MUTATION_LEN)) { return true; } @@ -1809,6 +1815,7 @@ static unsigned int addexports_count = 0; static unsigned int addopens_count = 0; static unsigned int patch_mod_count = 0; static unsigned int enable_native_access_count = 0; +static unsigned int enable_final_field_mutation = 0; static bool patch_mod_javabase = false; // Check the consistency of vm_init_args @@ -2273,6 +2280,19 @@ jint Arguments::parse_each_vm_init_arg(const JavaVMInitArgs* args, JVMFlagOrigin if (res != JNI_OK) { return res; } + } else if (match_option(option, "--enable-final-field-mutation=", &tail)) { + if (!create_numbered_module_property("jdk.module.enable.final.field.mutation", tail, enable_final_field_mutation++)) { + return JNI_ENOMEM; + } + } else if (match_option(option, "--illegal-final-field-mutation=", &tail)) { + if (strcmp(tail, "allow") == 0 || strcmp(tail, "warn") == 0 || strcmp(tail, "debug") == 0 || strcmp(tail, "deny") == 0) { + PropertyList_unique_add(&_system_properties, "jdk.module.illegal.final.field.mutation", tail, + AddProperty, WriteableProperty, InternalProperty); + } else { + jio_fprintf(defaultStream::error_stream(), + "Value specified to --illegal-final-field-mutation not recognized: '%s'\n", tail); + return JNI_ERR; + } } else if (match_option(option, "--sun-misc-unsafe-memory-access=", &tail)) { if (strcmp(tail, "allow") == 0 || strcmp(tail, "warn") == 0 || strcmp(tail, "debug") == 0 || strcmp(tail, "deny") == 0) { PropertyList_unique_add(&_system_properties, "sun.misc.unsafe.memory.access", tail, diff --git a/src/hotspot/share/runtime/fieldDescriptor.cpp b/src/hotspot/share/runtime/fieldDescriptor.cpp index c5c3bdbd4bc..491157d5bf7 100644 --- a/src/hotspot/share/runtime/fieldDescriptor.cpp +++ b/src/hotspot/share/runtime/fieldDescriptor.cpp @@ -46,6 +46,16 @@ bool fieldDescriptor::is_trusted_final() const { return is_final() && (is_static() || ik->is_hidden() || ik->is_record()); } +bool fieldDescriptor::is_mutable_static_final() const { + InstanceKlass* ik = field_holder(); + // write protected fields (JLS 17.5.4) + if (is_final() && is_static() && ik == vmClasses::System_klass() && + (offset() == java_lang_System::in_offset() || offset() == java_lang_System::out_offset() || offset() == java_lang_System::err_offset())) { + return true; + } + return false; +} + AnnotationArray* fieldDescriptor::annotations() const { InstanceKlass* ik = field_holder(); Array* md = ik->fields_annotations(); diff --git a/src/hotspot/share/runtime/fieldDescriptor.hpp b/src/hotspot/share/runtime/fieldDescriptor.hpp index aae789b1fb7..fa3d1b9d23c 100644 --- a/src/hotspot/share/runtime/fieldDescriptor.hpp +++ b/src/hotspot/share/runtime/fieldDescriptor.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -98,6 +98,8 @@ class fieldDescriptor { bool is_trusted_final() const; + bool is_mutable_static_final() const; + inline void set_is_field_access_watched(const bool value); inline void set_is_field_modification_watched(const bool value); inline void set_has_initialized_final_update(const bool value); diff --git a/src/java.base/share/classes/java/lang/Module.java b/src/java.base/share/classes/java/lang/Module.java index 065e1ac4620..cd2b8095ee4 100644 --- a/src/java.base/share/classes/java/lang/Module.java +++ b/src/java.base/share/classes/java/lang/Module.java @@ -37,6 +37,7 @@ import java.lang.module.ModuleDescriptor.Version; import java.lang.module.ResolvedModule; import java.lang.reflect.AccessFlag; import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Field; import java.net.URI; import java.net.URL; import java.security.CodeSource; @@ -115,6 +116,10 @@ public final class Module implements AnnotatedElement { @Stable private boolean enableNativeAccess; + // true if this module is allowed to mutate final instance fields + @Stable + private boolean enableFinalMutation; + /** * Creates a new named Module. The resulting Module will be defined to the * VM but will not read any other modules, will not have any exports setup @@ -262,7 +267,6 @@ public final class Module implements AnnotatedElement { * in the outer Module class as that would create a circular initializer dependency. */ private static final class EnableNativeAccess { - private EnableNativeAccess() {} private static final Unsafe UNSAFE = Unsafe.getUnsafe(); @@ -331,12 +335,52 @@ public final class Module implements AnnotatedElement { } /** - * Update all unnamed modules to allow access to restricted methods. + * Enable code in all unnamed modules to access restricted methods. */ - static void implAddEnableNativeAccessToAllUnnamed() { + static void addEnableNativeAccessToAllUnnamed() { EnableNativeAccess.trySetEnableNativeAccess(ALL_UNNAMED_MODULE); } + /** + * This class exists to avoid using Unsafe during early initialization of Module. + */ + private static final class EnableFinalMutation { + private static final Unsafe UNSAFE = Unsafe.getUnsafe(); + private static final long ENABLE_FINAL_MUTATION_OFFSET = + UNSAFE.objectFieldOffset(Module.class, "enableFinalMutation"); + + private static boolean isEnableFinalMutation(Module module) { + return UNSAFE.getBooleanVolatile(module, ENABLE_FINAL_MUTATION_OFFSET); + } + + private static boolean tryEnableFinalMutation(Module module) { + return UNSAFE.compareAndSetBoolean(module, ENABLE_FINAL_MUTATION_OFFSET, false, true); + } + } + + /** + * Enable code in all unnamed modules to mutate final instance fields. + */ + static void addEnableFinalMutationToAllUnnamed() { + EnableFinalMutation.tryEnableFinalMutation(ALL_UNNAMED_MODULE); + } + + /** + * Enable code in this named module to mutate final instance fields. + */ + boolean tryEnableFinalMutation() { + Module m = isNamed() ? this : ALL_UNNAMED_MODULE; + return EnableFinalMutation.tryEnableFinalMutation(m); + } + + /** + * Return true if code in this module is allowed to mutate final instance fields. + */ + boolean isFinalMutationEnabled() { + Module m = isNamed() ? this : ALL_UNNAMED_MODULE; + return EnableFinalMutation.isEnableFinalMutation(m); + } + // -- // special Module to mean "all unnamed modules" @@ -718,8 +762,50 @@ public final class Module implements AnnotatedElement { } /** - * Returns {@code true} if this module exports or opens a package to - * the given module via its module declaration or CLI options. + * Returns {@code true} if this module statically exports a package to the given module. + * If the package is exported to the given module via {@code addExports} then this method + * returns {@code false}. + */ + boolean isStaticallyExported(String pn, Module other) { + return isStaticallyExportedOrOpened(pn, other, false); + } + + /** + * Returns {@code true} if this module statically opens a package to the given module. + * If the package is opened to the given module via {@code addOpens} then this method + * returns {@code false}. + */ + boolean isStaticallyOpened(String pn, Module other) { + return isStaticallyExportedOrOpened(pn, other, true); + } + + /** + * Returns {@code true} if this module exports or opens a package to the + * given module via its module declaration or CLI options. + */ + private boolean isStaticallyExportedOrOpened(String pn, Module other, boolean open) { + // all packages in unnamed modules are exported and open + if (!isNamed()) + return true; + + // all packages are exported/open to self + if (other == this && descriptor.packages().contains(pn)) + return true; + + // all packages in open and automatic modules are exported/open + if (descriptor.isOpen() || descriptor.isAutomatic()) + return descriptor.packages().contains(pn); + + // exported/opened via module descriptor + if (isExplicitlyExportedOrOpened(pn, other, open)) + return true; + + return false; + } + + /** + * Returns {@code true} if this module exports or opens a package to the + * given module via its module declaration or CLI options. */ private boolean isExplicitlyExportedOrOpened(String pn, Module other, boolean open) { // test if package is open to everyone or @@ -818,11 +904,16 @@ public final class Module implements AnnotatedElement { return isReflectivelyExportedOrOpened(pn, other, true); } - /** * If the caller's module is this module then update this module to export * the given package to the given module. * + *

Exporting a package with this method does not allow the given module to + * {@linkplain Field#set(Object, Object) reflectively set} or {@linkplain + * java.lang.invoke.MethodHandles.Lookup#unreflectSetter(Field) obtain a method + * handle with write access} to a public final field declared in a public class + * in the package. + * *

This method has no effect if the package is already exported (or * open) to the given module.

* @@ -860,21 +951,27 @@ public final class Module implements AnnotatedElement { if (caller != this) { throw new IllegalCallerException(caller + " != " + this); } - implAddExportsOrOpens(pn, other, /*open*/false, /*syncVM*/true); + implAddExports(pn, other); } return this; } /** - * If this module has opened a package to at least the caller - * module then update this module to open the package to the given module. - * Opening a package with this method allows all types in the package, + * If this module has opened the given package to at least the caller + * module, then update this module to also open the package to the given module. + * + *

Opening a package with this method allows all types in the package, * and all their members, not just public types and their public members, - * to be reflected on by the given module when using APIs that support - * private access or a way to bypass or suppress default Java language + * to be reflected on by the given module when using APIs that either support + * private access or provide a way to bypass or suppress Java language * access control checks. * + *

Opening a package with this method does not allow the given module to + * {@linkplain Field#set(Object, Object) reflectively set} or {@linkplain + * java.lang.invoke.MethodHandles.Lookup#unreflectSetter(Field) obtain a method + * handle with write access} to a final field declared in a class in the package. + * *

This method has no effect if the package is already open * to the given module.

* @@ -913,7 +1010,7 @@ public final class Module implements AnnotatedElement { Module caller = getCallerModule(Reflection.getCallerClass()); if (caller != this && (caller == null || !isOpen(pn, caller))) throw new IllegalCallerException(pn + " is not open to " + caller); - implAddExportsOrOpens(pn, other, /*open*/true, /*syncVM*/true); + implAddOpens(pn, other); } return this; @@ -923,28 +1020,29 @@ public final class Module implements AnnotatedElement { /** * Updates this module to export a package unconditionally. * - * @apiNote This method is for JDK tests only. + * @apiNote Used by Proxy and other dynamic modules. */ void implAddExports(String pn) { - implAddExportsOrOpens(pn, Module.EVERYONE_MODULE, false, true); + implAddExportsOrOpens(pn, Module.EVERYONE_MODULE, false, true, true); } /** * Updates this module to export a package to another module. * - * @apiNote Used by Instrumentation::redefineModule and --add-exports + * @apiNote Used by addExports, Instrumentation::redefineModule, and --add-exports */ void implAddExports(String pn, Module other) { - implAddExportsOrOpens(pn, other, false, true); + implAddExportsOrOpens(pn, other, false, VM.isBooted(), true); } /** * Updates this module to export a package to all unnamed modules. * - * @apiNote Used by the --add-exports command line option. + * @apiNote Used by the --add-exports command line option and the launcher when + * an executable JAR file has the "Add-Exports" attribute in its main manifest. */ void implAddExportsToAllUnnamed(String pn) { - implAddExportsOrOpens(pn, Module.ALL_UNNAMED_MODULE, false, true); + implAddExportsOrOpens(pn, Module.ALL_UNNAMED_MODULE, false, false, true); } /** @@ -954,7 +1052,7 @@ public final class Module implements AnnotatedElement { * @apiNote This method is for VM white-box testing. */ void implAddExportsNoSync(String pn) { - implAddExportsOrOpens(pn.replace('/', '.'), Module.EVERYONE_MODULE, false, false); + implAddExportsOrOpens(pn.replace('/', '.'), Module.EVERYONE_MODULE, false, true, false); } /** @@ -964,7 +1062,7 @@ public final class Module implements AnnotatedElement { * @apiNote This method is for VM white-box testing. */ void implAddExportsNoSync(String pn, Module other) { - implAddExportsOrOpens(pn.replace('/', '.'), other, false, false); + implAddExportsOrOpens(pn.replace('/', '.'), other, false, true, false); } /** @@ -973,35 +1071,40 @@ public final class Module implements AnnotatedElement { * @apiNote This method is for JDK tests only. */ void implAddOpens(String pn) { - implAddExportsOrOpens(pn, Module.EVERYONE_MODULE, true, true); + implAddExportsOrOpens(pn, Module.EVERYONE_MODULE, true, true, true); } /** * Updates this module to open a package to another module. * - * @apiNote Used by Instrumentation::redefineModule and --add-opens + * @apiNote Used by addOpens, Instrumentation::redefineModule, and --add-opens */ void implAddOpens(String pn, Module other) { - implAddExportsOrOpens(pn, other, true, true); + implAddExportsOrOpens(pn, other, true, VM.isBooted(), true); } /** * Updates this module to open a package to all unnamed modules. * - * @apiNote Used by the --add-opens command line option. + * @apiNote Used by the --add-opens command line option and the launcher when + * an executable JAR file has the "Add-Opens" attribute in its main manifest. */ void implAddOpensToAllUnnamed(String pn) { - implAddExportsOrOpens(pn, Module.ALL_UNNAMED_MODULE, true, true); + implAddExportsOrOpens(pn, Module.ALL_UNNAMED_MODULE, true, false, true); } /** * Updates a module to export or open a module to another module. - * - * If {@code syncVM} is {@code true} then the VM is notified. + * @param pn package name + * @param other the module to export/open the package to + * @param open true to open, false to export + * @param reflectively true if exported/opened reflectively + * @param syncVM true to update the VM */ private void implAddExportsOrOpens(String pn, Module other, boolean open, + boolean reflectively, boolean syncVM) { Objects.requireNonNull(other); Objects.requireNonNull(pn); @@ -1031,7 +1134,7 @@ public final class Module implements AnnotatedElement { } } - if (VM.isBooted()) { + if (reflectively) { // add package name to ReflectionData.exports if absent Map map = ReflectionData.exports .computeIfAbsent(this, other, diff --git a/src/java.base/share/classes/java/lang/ModuleLayer.java b/src/java.base/share/classes/java/lang/ModuleLayer.java index 5dfd93796d2..9d922f787a6 100644 --- a/src/java.base/share/classes/java/lang/ModuleLayer.java +++ b/src/java.base/share/classes/java/lang/ModuleLayer.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, 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 @@ -28,6 +28,7 @@ package java.lang; import java.lang.module.Configuration; import java.lang.module.ModuleDescriptor; import java.lang.module.ResolvedModule; +import java.lang.reflect.Field; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; @@ -252,6 +253,12 @@ public final class ModuleLayer { * module {@code target}. This method is a no-op if {@code source} * already exports the package to at least {@code target}. * + *

Exporting a package with this method does not allow the target module to + * {@linkplain Field#set(Object, Object) reflectively set} or {@linkplain + * java.lang.invoke.MethodHandles.Lookup#unreflectSetter(Field) obtain a method + * handle with write access} to a public final field declared in a public class + * in the package. + * * @param source * The source module * @param pn @@ -278,6 +285,11 @@ public final class ModuleLayer { * module {@code target}. This method is a no-op if {@code source} * already opens the package to at least {@code target}. * + *

Opening a package with this method does not allow the target module + * to {@linkplain Field#set(Object, Object) reflectively set} or {@linkplain + * java.lang.invoke.MethodHandles.Lookup#unreflectSetter(Field) obtain a method + * handle with write access} to a final field declared in a class in the package. + * * @param source * The source module * @param pn diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index c88cf4ac797..5c2b47afe3d 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2096,18 +2096,33 @@ public final class System { public boolean isReflectivelyOpened(Module m, String pn, Module other) { return m.isReflectivelyOpened(pn, other); } - public Module addEnableNativeAccess(Module m) { - return m.implAddEnableNativeAccess(); + public void addEnableNativeAccess(Module m) { + m.implAddEnableNativeAccess(); } public boolean addEnableNativeAccess(ModuleLayer layer, String name) { return layer.addEnableNativeAccess(name); } public void addEnableNativeAccessToAllUnnamed() { - Module.implAddEnableNativeAccessToAllUnnamed(); + Module.addEnableNativeAccessToAllUnnamed(); } public void ensureNativeAccess(Module m, Class owner, String methodName, Class currentClass, boolean jni) { m.ensureNativeAccess(owner, methodName, currentClass, jni); } + public boolean isStaticallyExported(Module m, String pn, Module other) { + return m.isStaticallyExported(pn, other); + } + public boolean isStaticallyOpened(Module m, String pn, Module other) { + return m.isStaticallyOpened(pn, other); + } + public boolean isFinalMutationEnabled(Module m) { + return m.isFinalMutationEnabled(); + } + public boolean tryEnableFinalMutation(Module m) { + return m.tryEnableFinalMutation(); + } + public void addEnableFinalMutationToAllUnnamed() { + Module.addEnableFinalMutationToAllUnnamed(); + } public ServicesCatalog getServicesCatalog(ModuleLayer layer) { return layer.getServicesCatalog(); } diff --git a/src/java.base/share/classes/java/lang/invoke/MethodHandles.java b/src/java.base/share/classes/java/lang/invoke/MethodHandles.java index 92a37587926..feb8aaaa1a9 100644 --- a/src/java.base/share/classes/java/lang/invoke/MethodHandles.java +++ b/src/java.base/share/classes/java/lang/invoke/MethodHandles.java @@ -3429,12 +3429,15 @@ return mh1; * or if the field is {@code final} and write access * is not enabled on the {@code Field} object * @throws NullPointerException if the argument is null + * @see Mutation methods */ public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { return unreflectField(f, true); } private MethodHandle unreflectField(Field f, boolean isSetter) throws IllegalAccessException { + @SuppressWarnings("deprecation") + boolean isAccessible = f.isAccessible(); MemberName field = new MemberName(f, isSetter); if (isSetter && field.isFinal()) { if (field.isTrustedFinalField()) { @@ -3442,12 +3445,15 @@ return mh1; : "final field has no write access"; throw field.makeAccessException(msg, this); } + // check if write access to final field allowed + if (!field.isStatic() && isAccessible) { + SharedSecrets.getJavaLangReflectAccess().checkAllowedToUnreflectFinalSetter(lookupClass, f); + } } assert(isSetter ? MethodHandleNatives.refKindIsSetter(field.getReferenceKind()) : MethodHandleNatives.refKindIsGetter(field.getReferenceKind())); - @SuppressWarnings("deprecation") - Lookup lookup = f.isAccessible() ? IMPL_LOOKUP : this; + Lookup lookup = isAccessible ? IMPL_LOOKUP : this; return lookup.getDirectField(field.getReferenceKind(), f.getDeclaringClass(), field); } diff --git a/src/java.base/share/classes/java/lang/reflect/AccessibleObject.java b/src/java.base/share/classes/java/lang/reflect/AccessibleObject.java index a045f9c196a..1637d26b571 100644 --- a/src/java.base/share/classes/java/lang/reflect/AccessibleObject.java +++ b/src/java.base/share/classes/java/lang/reflect/AccessibleObject.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1997, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1997, 2025, 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 @@ -160,18 +160,6 @@ public class AccessibleObject implements AnnotatedElement { * to the caller and the package containing the declaring class is not open * to the caller's module.

* - *

This method cannot be used to enable {@linkplain Field#set write} - * access to a non-modifiable final field. The following fields - * are non-modifiable: - *

    - *
  • static final fields declared in any class or interface
  • - *
  • final fields declared in a {@linkplain Class#isHidden() hidden class}
  • - *
  • final fields declared in a {@linkplain Class#isRecord() record}
  • - *
- *

The {@code accessible} flag when {@code true} suppresses Java language access - * control checks to only enable {@linkplain Field#get read} access to - * these non-modifiable final fields. - * * @param flag the new value for the {@code accessible} flag * @throws InaccessibleObjectException if access cannot be enabled * diff --git a/src/java.base/share/classes/java/lang/reflect/Field.java b/src/java.base/share/classes/java/lang/reflect/Field.java index e26d8b03ff8..663e3453343 100644 --- a/src/java.base/share/classes/java/lang/reflect/Field.java +++ b/src/java.base/share/classes/java/lang/reflect/Field.java @@ -25,7 +25,18 @@ package java.lang.reflect; +import java.lang.annotation.Annotation; +import java.net.URL; +import java.security.CodeSource; +import java.util.Map; +import java.util.Set; +import java.util.Objects; import jdk.internal.access.SharedSecrets; +import jdk.internal.event.FinalFieldMutationEvent; +import jdk.internal.loader.ClassLoaders; +import jdk.internal.misc.VM; +import jdk.internal.module.ModuleBootstrap; +import jdk.internal.module.Modules; import jdk.internal.reflect.CallerSensitive; import jdk.internal.reflect.FieldAccessor; import jdk.internal.reflect.Reflection; @@ -35,10 +46,6 @@ import sun.reflect.generics.repository.FieldRepository; import sun.reflect.generics.factory.CoreReflectionFactory; import sun.reflect.generics.factory.GenericsFactory; import sun.reflect.generics.scope.ClassScope; -import java.lang.annotation.Annotation; -import java.util.Map; -import java.util.Set; -import java.util.Objects; import sun.reflect.annotation.AnnotationParser; import sun.reflect.annotation.AnnotationSupport; import sun.reflect.annotation.TypeAnnotation; @@ -165,6 +172,27 @@ class Field extends AccessibleObject implements Member { } /** + * {@inheritDoc} + * + *

If this reflected object represents a non-final field, and this method is + * used to enable access, then both {@linkplain #get(Object) read} + * and {@linkplain #set(Object, Object) write} access to the field + * are enabled. + * + *

If this reflected object represents a non-modifiable final field + * then enabling access only enables read access. Any attempt to {@linkplain + * #set(Object, Object) set} the field value throws an {@code + * IllegalAccessException}. The following fields are non-modifiable: + *

    + *
  • static final fields declared in any class or interface
  • + *
  • final fields declared in a {@linkplain Class#isRecord() record}
  • + *
  • final fields declared in a {@linkplain Class#isHidden() hidden class}
  • + *
+ *

If this reflected object represents a non-static final field in a class that + * is not a record class or hidden class, then enabling access will enable read + * access. Whether write access is allowed or not is checked when attempting to + * {@linkplain #set(Object, Object) set} the field value. + * * @throws InaccessibleObjectException {@inheritDoc} */ @Override @@ -762,18 +790,59 @@ class Field extends AccessibleObject implements Member { * the underlying field is inaccessible, the method throws an * {@code IllegalAccessException}. * - *

If the underlying field is final, this {@code Field} object has - * write access if and only if the following conditions are met: + *

If the underlying field is final, this {@code Field} object has write + * access if and only if all of the following conditions are true, where {@code D} is + * the field's {@linkplain #getDeclaringClass() declaring class}: + * *

    - *
  • {@link #setAccessible(boolean) setAccessible(true)} has succeeded for - * this {@code Field} object;
  • - *
  • the field is non-static; and
  • - *
  • the field's declaring class is not a {@linkplain Class#isHidden() - * hidden class}; and
  • - *
  • the field's declaring class is not a {@linkplain Class#isRecord() - * record class}.
  • + *
  • {@link #setAccessible(boolean) setAccessible(true)} has succeeded for this + * {@code Field} object.
  • + *
  • final field mutation is enabled + * for the caller's module.
  • + *
  • At least one of the following conditions holds: + *
      + *
    1. {@code D} and the caller class are in the same module.
    2. + *
    3. The field is {@code public} and {@code D} is {@code public} in a package + * that the module containing {@code D} exports to at least the caller's module.
    4. + *
    5. {@code D} is in a package that is {@linkplain Module#isOpen(String, Module) + * open} to the caller's module.
    6. + *
    + *
  • + *
  • {@code D} is not a {@linkplain Class#isRecord() record class}.
  • + *
  • {@code D} is not a {@linkplain Class#isHidden() hidden class}.
  • + *
  • The field is non-static.
  • *
- * If any of the above checks is not met, this method throws an + * + *

If any of the above conditions is not met, this method throws an + * {@code IllegalAccessException}. + * + *

These conditions are more restrictive than the conditions specified by {@link + * #setAccessible(boolean)} to suppress access checks. In particular, updating a + * module to export or open a package cannot be used to allow write access + * to final fields with the {@code set} methods defined by {@code Field}. + * Condition (b) is not met if the module containing {@code D} has been updated with + * {@linkplain Module#addExports(String, Module) addExports} to export the package to + * the caller's module. Condition (c) is not met if the module containing {@code D} + * has been updated with {@linkplain Module#addOpens(String, Module) addOpens} to open + * the package to the caller's module. + * + *

This method may be called by + * JNI code with no caller class on the stack. In that case, and when the + * underlying field is final, this {@code Field} object has write access + * if and only if all of the following conditions are true, where {@code D} is the + * field's {@linkplain #getDeclaringClass() declaring class}: + * + *

    + *
  • {@code setAccessible(true)} has succeeded for this {@code Field} object.
  • + *
  • final field mutation is enabled for the unnamed module.
  • + *
  • The field is {@code public} and {@code D} is {@code public} in a package that + * is {@linkplain Module#isExported(String) exported} to all modules.
  • + *
  • {@code D} is not a {@linkplain Class#isRecord() record class}.
  • + *
  • {@code D} is not a {@linkplain Class#isHidden() hidden class}.
  • + *
  • The field is non-static.
  • + *
+ * + *

If any of the above conditions is not met, this method throws an * {@code IllegalAccessException}. * *

Setting a final field in this way @@ -818,6 +887,8 @@ class Field extends AccessibleObject implements Member { * and the field is an instance field. * @throws ExceptionInInitializerError if the initialization provoked * by this method fails. + * + * @see Mutation methods */ @CallerSensitive @ForceInline // to ensure Reflection.getCallerClass optimization @@ -828,8 +899,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().set(obj, value); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.set(obj, value); } else { - getOverrideFieldAccessor().set(obj, value); + setFinal(Reflection.getCallerClass(), obj, () -> fa.set(obj, value)); } } @@ -867,8 +944,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setBoolean(obj, z); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setBoolean(obj, z); } else { - getOverrideFieldAccessor().setBoolean(obj, z); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setBoolean(obj, z)); } } @@ -906,8 +989,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setByte(obj, b); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setByte(obj, b); } else { - getOverrideFieldAccessor().setByte(obj, b); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setByte(obj, b)); } } @@ -945,8 +1034,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setChar(obj, c); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setChar(obj, c); } else { - getOverrideFieldAccessor().setChar(obj, c); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setChar(obj, c)); } } @@ -984,8 +1079,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setShort(obj, s); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setShort(obj, s); } else { - getOverrideFieldAccessor().setShort(obj, s); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setShort(obj, s)); } } @@ -1023,8 +1124,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setInt(obj, i); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setInt(obj, i); } else { - getOverrideFieldAccessor().setInt(obj, i); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setInt(obj, i)); } } @@ -1062,8 +1169,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setLong(obj, l); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setLong(obj, l); } else { - getOverrideFieldAccessor().setLong(obj, l); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setLong(obj, l)); } } @@ -1101,8 +1214,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setFloat(obj, f); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setFloat(obj, f); } else { - getOverrideFieldAccessor().setFloat(obj, f); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setFloat(obj, f)); } } @@ -1140,8 +1259,14 @@ class Field extends AccessibleObject implements Member { Class caller = Reflection.getCallerClass(); checkAccess(caller, obj); getFieldAccessor().setDouble(obj, d); + return; + } + + FieldAccessor fa = getOverrideFieldAccessor(); + if (!Modifier.isFinal(modifiers)) { + fa.setDouble(obj, d); } else { - getOverrideFieldAccessor().setDouble(obj, d); + setFinal(Reflection.getCallerClass(), obj, () -> fa.setDouble(obj, d)); } } @@ -1304,5 +1429,244 @@ class Field extends AccessibleObject implements Member { getDeclaringClass(), getGenericType(), TypeAnnotation.TypeAnnotationTarget.FIELD); -} + } + + /** + * A function that sets a field to a value. + */ + @FunctionalInterface + private interface FieldSetter { + void setFieldValue() throws IllegalAccessException; + } + + /** + * Attempts to set a final field. + */ + private void setFinal(Class caller, Object obj, FieldSetter setter) throws IllegalAccessException { + if (obj != null && isFinalInstanceInNormalClass()) { + preSetFinal(caller, false); + setter.setFieldValue(); + postSetFinal(caller, false); + } else { + // throws IllegalAccessException if static, or field in record or hidden class + setter.setFieldValue(); + } + } + + /** + * Return true if this field is a final instance field in a normal class (not a + * record class or hidden class), + */ + private boolean isFinalInstanceInNormalClass() { + return Modifier.isFinal(modifiers) + && !Modifier.isStatic(modifiers) + && !clazz.isRecord() + && !clazz.isHidden(); + } + + /** + * Check that the caller is allowed to unreflect for mutation a final instance field + * in a normal class. + * @throws IllegalAccessException if not allowed + */ + void checkAllowedToUnreflectFinalSetter(Class caller) throws IllegalAccessException { + Objects.requireNonNull(caller); + preSetFinal(caller, true); + postSetFinal(caller, true); + } + + /** + * Invoke before attempting to mutate, or unreflect for mutation, a final instance + * field in a normal class. + * @throws IllegalAccessException if not allowed + */ + private void preSetFinal(Class caller, boolean unreflect) throws IllegalAccessException { + assert isFinalInstanceInNormalClass(); + + if (caller != null) { + // check if declaring class in package that is open to caller, or public field + // and declaring class is public in package exported to caller + if (!isFinalDeeplyAccessible(caller)) { + throw new IllegalAccessException(notAccessibleToCallerMessage(caller, unreflect)); + } + } else { + // no java caller, only allowed if field is public in exported package + if (!Reflection.verifyPublicMemberAccess(clazz, modifiers)) { + throw new IllegalAccessException(notAccessibleToNoCallerMessage(unreflect)); + } + } + + // check if field mutation is enabled for caller module or illegal final field + // mutation is allowed + var mode = ModuleBootstrap.illegalFinalFieldMutation(); + if (mode == ModuleBootstrap.IllegalFinalFieldMutation.DENY + && !Modules.isFinalMutationEnabled(moduleToCheck(caller))) { + throw new IllegalAccessException(callerNotAllowedToMutateMessage(caller, unreflect)); + } + } + + /** + * Invoke after mutating a final instance field, or when unreflecting a final instance + * field for mutation, to print a warning and record a JFR event. + */ + private void postSetFinal(Class caller, boolean unreflect) { + assert isFinalInstanceInNormalClass(); + + var mode = ModuleBootstrap.illegalFinalFieldMutation(); + if (mode == ModuleBootstrap.IllegalFinalFieldMutation.WARN) { + // first mutation prints warning + Module moduleToCheck = moduleToCheck(caller); + if (Modules.tryEnableFinalMutation(moduleToCheck)) { + String warningMsg = finalFieldMutationWarning(caller, unreflect); + String targetModule = (caller != null && moduleToCheck.isNamed()) + ? moduleToCheck.getName() + : "ALL-UNNAMED"; + VM.initialErr().printf(""" + WARNING: %s + WARNING: Use --enable-final-field-mutation=%s to avoid a warning + WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled + """, warningMsg, targetModule); + } + } else if (mode == ModuleBootstrap.IllegalFinalFieldMutation.DEBUG) { + // print warning and stack trace + var sb = new StringBuilder(finalFieldMutationWarning(caller, unreflect)); + StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE) + .forEach(sf -> { + sb.append(System.lineSeparator()).append("\tat " + sf); + }); + VM.initialErr().println(sb); + } + + // record JFR event + FinalFieldMutationEvent.offer(getDeclaringClass(), getName()); + } + + /** + * Returns true if this final field is "deeply accessible" to the caller. + * The field is deeply accessible if declaring class is in a package that is open + * to the caller's module, or the field is public in a public class that is exported + * to the caller's module. + * + * Updates to the module of the declaring class at runtime with {@code Module.addExports} + * or {@code Module.addOpens} have no impact on the result of this method. + */ + private boolean isFinalDeeplyAccessible(Class caller) { + assert isFinalInstanceInNormalClass(); + + // all fields in unnamed modules are deeply accessible + Module declaringModule = clazz.getModule(); + if (!declaringModule.isNamed()) return true; + + // all fields in the caller's module are deeply accessible + Module callerModule = caller.getModule(); + if (callerModule == declaringModule) return true; + + // public field, public class, package exported to caller's module + String pn = clazz.getPackageName(); + if (Modifier.isPublic(modifiers) + && Modifier.isPublic(clazz.getModifiers()) + && Modules.isStaticallyExported(declaringModule, pn, callerModule)) { + return true; + } + + // package open to caller's module + return Modules.isStaticallyOpened(declaringModule, pn, callerModule); + } + + /** + * Returns the Module to use for access checks with the given caller. + */ + private Module moduleToCheck(Class caller) { + if (caller != null) { + return caller.getModule(); + } else { + // no java caller, only allowed if field is public in exported package + return ClassLoaders.appClassLoader().getUnnamedModule(); + } + } + + /** + * Returns the warning message to print when this final field is mutated by + * the given possibly-null caller. + */ + private String finalFieldMutationWarning(Class caller, boolean unreflect) { + assert Modifier.isFinal(modifiers); + String source; + if (caller != null) { + source = caller + " in " + caller.getModule(); + CodeSource cs = caller.getProtectionDomain().getCodeSource(); + if (cs != null) { + URL url = cs.getLocation(); + if (url != null) { + source += " (" + url + ")"; + } + } + } else { + source = "JNI attached thread with no caller frame"; + } + return String.format("Final field %s in %s has been %s by %s", + name, + clazz, + (unreflect) ? "unreflected for mutation" : "mutated reflectively", + source); + } + + /** + * Returns the message for an IllegalAccessException when a final field cannot be + * mutated because the declaring class is in a package that is not "deeply accessible" + * to the caller. + */ + private String notAccessibleToCallerMessage(Class caller, boolean unreflect) { + String exportsOrOpens = Modifier.isPublic(modifiers) + && Modifier.isPublic(clazz.getModifiers()) ? "exports" : "opens"; + return String.format("%s, %s does not explicitly \"%s\" package %s to %s", + cannotSetFieldMessage(caller, unreflect), + clazz.getModule(), + exportsOrOpens, + clazz.getPackageName(), + caller.getModule()); + } + + /** + * Returns the exception message for the IllegalAccessException when this + * final field cannot be mutated because the caller module is not allowed + * to mutate final fields. + */ + private String callerNotAllowedToMutateMessage(Class caller, boolean unreflect) { + if (caller != null) { + return String.format("%s, %s is not allowed to mutate final fields", + cannotSetFieldMessage(caller, unreflect), + caller.getModule()); + } else { + return notAccessibleToNoCallerMessage(unreflect); + } + } + + /** + * Returns the message for an IllegalAccessException when a field is not + * accessible to a JNI attached thread. + */ + private String notAccessibleToNoCallerMessage(boolean unreflect) { + return cannotSetFieldMessage("JNI attached thread with no caller frame cannot", unreflect); + } + + /** + * Returns a message to indicate that the caller cannot set/unreflect this final field. + */ + private String cannotSetFieldMessage(Class caller, boolean unreflect) { + return cannotSetFieldMessage(caller + " (in " + caller.getModule() + ") cannot", unreflect); + } + + /** + * Returns a message to indicate that a field cannot be set/unreflected. + */ + private String cannotSetFieldMessage(String prefix, boolean unreflect) { + if (unreflect) { + return prefix + " unreflect final field " + clazz.getName() + "." + name + + " (in " + clazz.getModule() + ") for mutation"; + } else { + return prefix + " set final field " + clazz.getName() + "." + name + + " (in " + clazz.getModule() + ")"; + } + } } diff --git a/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java b/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java index 835ffef616e..ea4e5e06dcf 100644 --- a/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java +++ b/src/java.base/share/classes/java/lang/reflect/ReflectAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, 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 @@ -78,4 +78,9 @@ final class ReflectAccess implements JavaLangReflectAccess { { return ctor.newInstanceWithCaller(args, true, caller); } + + @Override + public void checkAllowedToUnreflectFinalSetter(Class caller, Field f) throws IllegalAccessException { + f.checkAllowedToUnreflectFinalSetter(caller); + } } diff --git a/src/java.base/share/classes/java/lang/reflect/doc-files/MutationMethods.html b/src/java.base/share/classes/java/lang/reflect/doc-files/MutationMethods.html new file mode 100644 index 00000000000..8a731040cd1 --- /dev/null +++ b/src/java.base/share/classes/java/lang/reflect/doc-files/MutationMethods.html @@ -0,0 +1,69 @@ + + + + + Mutation methods + + + +

Mutation methods

+ +

A number of methods in Java SE API provide write access to non-static +final fields. This means that Java code can alter the value of a final field +after the field has been initialized in a constructor. + +The methods that provide write access, known as mutation methods are: +

    +
  • {@link java.lang.reflect.Field#set(Object, Object)}
  • +
  • {@link java.lang.reflect.Field#setBoolean(Object, boolean)}
  • +
  • {@link java.lang.reflect.Field#setByte(Object, byte)}
  • +
  • {@link java.lang.reflect.Field#setChar(Object, char)}
  • +
  • {@link java.lang.reflect.Field#setInt(Object, int)}
  • +
  • {@link java.lang.reflect.Field#setLong(Object, long)}
  • +
  • {@link java.lang.reflect.Field#setFloat(Object, float)}
  • +
  • {@link java.lang.reflect.Field#setDouble(Object, double)}
  • +
  • {@link java.lang.invoke.MethodHandles.Lookup#unreflectSetter(java.lang.reflect.Field)}
  • +
+ +

The use of mutation methods to alter the values of final fields is +strongly inadvisable because it undermines the correctness of programs written in +expectation of final fields being immutable. + +

In the reference implementation, a module can be granted the capability to mutate +final instance fields of classes in packages that are open to the module using +the command line option --enable-final-field-mutation=M1,M2, ... Mn} where +M1, M2, ...Mn are module names (for the unnamed +module, the special value ALL-UNNAMED can be used). Mutation of final +instance fields of classes from modules not listed by that option is deemed +illegal. +The command line option --illegal-final-field-mutation controls how illegal +final mutation is handled. Valid values for this command line option are "warn", "allow", +"debug, and "deny". If this option is not specified then the default is "warn" so that +illegal final field mutation will result in a warning at runtime. + + + + diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index fa6e5b4aac3..86e5f317dc7 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -205,7 +205,7 @@ public interface JavaLangAccess { * Updates the readability so that module m1 reads m2. The new read edge * does not result in a strong reference to m2 (m2 can be GC'ed). * - * This method is the same as m1.addReads(m2) but without a permission check. + * This method is the same as m1.addReads(m2) but without a caller check. */ void addReads(Module m1, Module m2); @@ -259,11 +259,11 @@ public interface JavaLangAccess { /** * Updates module m to allow access to restricted methods. */ - Module addEnableNativeAccess(Module m); + void addEnableNativeAccess(Module m); /** - * Updates module named {@code name} in layer {@code layer} to allow access to restricted methods. - * Returns true iff the given module exists in the given layer. + * Updates module named {@code name} in layer {@code layer} to allow access to + * restricted methods. Returns true iff the given module exists in the given layer. */ boolean addEnableNativeAccess(ModuleLayer layer, String name); @@ -273,7 +273,8 @@ public interface JavaLangAccess { void addEnableNativeAccessToAllUnnamed(); /** - * Ensure that the given module has native access. If not, warn or throw exception depending on the configuration. + * Ensure that the given module has native access. If not, warn or throw exception + * depending on the configuration. * @param m the module in which native access occurred * @param owner the owner of the restricted method being called (or the JNI method being bound) * @param methodName the name of the restricted method being called (or the JNI method being bound) @@ -282,6 +283,31 @@ public interface JavaLangAccess { */ void ensureNativeAccess(Module m, Class owner, String methodName, Class currentClass, boolean jni); + /** + * Enable code in all unnamed modules to mutate final instance fields. + */ + void addEnableFinalMutationToAllUnnamed(); + + /** + * Enable code in a given module to mutate final instance fields. + */ + boolean tryEnableFinalMutation(Module m); + + /** + * Return true if code in a given module is allowed to mutate final instance fields. + */ + boolean isFinalMutationEnabled(Module m); + + /** + * Return true if a given module has statically exported the given package to a given other module. + */ + boolean isStaticallyExported(Module module, String pn, Module other); + + /** + * Return true if a given module has statically opened the given package to a given other module. + */ + boolean isStaticallyOpened(Module module, String pn, Module other); + /** * Returns the ServicesCatalog for the given Layer. */ diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java index d0c415d2dc6..965bac67f45 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangReflectAccess.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2001, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2001, 2025, 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 @@ -67,4 +67,10 @@ public interface JavaLangReflectAccess { /** Returns a new instance created by the given constructor with access check */ public T newInstance(Constructor ctor, Object[] args, Class caller) throws IllegalAccessException, InstantiationException, InvocationTargetException; + + /** + * Check that the caller is allowed to unreflect for mutation a final instance field + * in a class that is not a record or hidden class. + */ + void checkAllowedToUnreflectFinalSetter(Class caller, Field f) throws IllegalAccessException; } diff --git a/src/java.base/share/classes/jdk/internal/event/FinalFieldMutationEvent.java b/src/java.base/share/classes/jdk/internal/event/FinalFieldMutationEvent.java new file mode 100644 index 00000000000..4db29d43bd6 --- /dev/null +++ b/src/java.base/share/classes/jdk/internal/event/FinalFieldMutationEvent.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 jdk.internal.event; + +public class FinalFieldMutationEvent extends Event { + public Class declaringClass; + public String fieldName; + + /** + * Commit a FinalFieldMutationEvent if enabled. + */ + public static void offer(Class declaringClass, String fieldName) { + if (enabled()) { + var event = new FinalFieldMutationEvent(); + event.declaringClass = declaringClass; + event.fieldName = fieldName; + event.commit(); + } + } + + public static boolean enabled() { + // Generated by JFR + return false; + } +} diff --git a/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java b/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java index 45cddc9fb6f..4dfc740024e 100644 --- a/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java +++ b/src/java.base/share/classes/jdk/internal/module/ModuleBootstrap.java @@ -452,9 +452,12 @@ public final class ModuleBootstrap { addExtraReads(bootLayer); addExtraExportsAndOpens(bootLayer); - // add enable native access + // enable native access to modules specified to --enable-native-access addEnableNativeAccess(bootLayer); + // allow final mutation by modules specified to --enable-final-field-mutation + addEnableFinalFieldMutation(bootLayer); + Counters.add("jdk.module.boot.7.adjustModulesTime"); // Step 8: CDS dump phase @@ -721,7 +724,6 @@ public final class ModuleBootstrap { * additional packages specified on the command-line. */ private static void addExtraExportsAndOpens(ModuleLayer bootLayer) { - // --add-exports String prefix = "jdk.module.addexports."; Map> extraExports = decode(prefix); @@ -729,14 +731,12 @@ public final class ModuleBootstrap { addExtraExportsOrOpens(bootLayer, extraExports, false); } - // --add-opens prefix = "jdk.module.addopens."; Map> extraOpens = decode(prefix); if (!extraOpens.isEmpty()) { addExtraExportsOrOpens(bootLayer, extraOpens, true); } - } private static void addExtraExportsOrOpens(ModuleLayer bootLayer, @@ -807,6 +807,7 @@ public final class ModuleBootstrap { private static final Set USER_NATIVE_ACCESS_MODULES; private static final Set JDK_NATIVE_ACCESS_MODULES; private static final IllegalNativeAccess ILLEGAL_NATIVE_ACCESS; + private static final IllegalFinalFieldMutation ILLEGAL_FINAL_FIELD_MUTATION; public enum IllegalNativeAccess { ALLOW, @@ -814,14 +815,26 @@ public final class ModuleBootstrap { DENY } + public enum IllegalFinalFieldMutation { + ALLOW, + WARN, + DEBUG, + DENY + } + + static { + ILLEGAL_NATIVE_ACCESS = decodeIllegalNativeAccess(); + USER_NATIVE_ACCESS_MODULES = decodeEnableNativeAccess(); + JDK_NATIVE_ACCESS_MODULES = ModuleLoaderMap.nativeAccessModules(); + ILLEGAL_FINAL_FIELD_MUTATION = decodeIllegalFinalFieldMutation(); + } + public static IllegalNativeAccess illegalNativeAccess() { return ILLEGAL_NATIVE_ACCESS; } - static { - ILLEGAL_NATIVE_ACCESS = addIllegalNativeAccess(); - USER_NATIVE_ACCESS_MODULES = decodeEnableNativeAccess(); - JDK_NATIVE_ACCESS_MODULES = ModuleLoaderMap.nativeAccessModules(); + public static IllegalFinalFieldMutation illegalFinalFieldMutation() { + return ILLEGAL_FINAL_FIELD_MUTATION; } /** @@ -881,7 +894,7 @@ public final class ModuleBootstrap { /** * Process the --illegal-native-access option (and its default). */ - private static IllegalNativeAccess addIllegalNativeAccess() { + private static IllegalNativeAccess decodeIllegalNativeAccess() { String value = getAndRemoveProperty("jdk.module.illegal.native.access"); // don't use a switch: bootstrapping issues! if (value == null) { @@ -899,6 +912,71 @@ public final class ModuleBootstrap { } } + /** + * Process the --illegal-final-field-mutation option. + */ + private static IllegalFinalFieldMutation decodeIllegalFinalFieldMutation() { + String value = getAndRemoveProperty("jdk.module.illegal.final.field.mutation"); + if (value == null) { + return IllegalFinalFieldMutation.WARN; // default + } else if (value.equals("allow")) { + return IllegalFinalFieldMutation.ALLOW; + } else if (value.equals("warn")) { + return IllegalFinalFieldMutation.WARN; + } else if (value.equals("debug")) { + return IllegalFinalFieldMutation.DEBUG; + } else if (value.equals("deny")) { + return IllegalFinalFieldMutation.DENY; + } else { + fail("Value specified to --illegal-final-field-mutation not recognized:" + + " '" + value + "'"); + return null; + } + } + + /** + * Process the modules specified to --enable-final-field-mutation and grant the + * capability to mutate finals to specified named modules or all unnamed modules. + */ + private static void addEnableFinalFieldMutation(ModuleLayer bootLayer) { + for (String name : decodeEnableFinalFieldMutation()) { + if (name.equals("ALL-UNNAMED")) { + JLA.addEnableFinalMutationToAllUnnamed(); + } else { + Module m = bootLayer.findModule(name).orElse(null); + if (m != null) { + JLA.tryEnableFinalMutation(m); + } else { + warnUnknownModule("--enable-final-field-mutation", name); + } + } + } + } + + /** + * Returns the set of module names specified by --enable-final-field-mutation options. + */ + private static Set decodeEnableFinalFieldMutation() { + String prefix = "jdk.module.enable.final.field.mutation."; + int index = 0; + // the system property is removed after decoding + String value = getAndRemoveProperty(prefix + index); + Set modules = new HashSet<>(); + if (value == null) { + return modules; + } + while (value != null) { + for (String s : value.split(",")) { + if (!s.isEmpty()) { + modules.add(s); + } + } + index++; + value = getAndRemoveProperty(prefix + index); + } + return modules; + } + /** * Decodes the values of --add-reads, -add-exports, --add-opens or * --patch-modules options that are encoded in system properties. diff --git a/src/java.base/share/classes/jdk/internal/module/Modules.java b/src/java.base/share/classes/jdk/internal/module/Modules.java index 3c3d148e196..760b3ba9a08 100644 --- a/src/java.base/share/classes/jdk/internal/module/Modules.java +++ b/src/java.base/share/classes/jdk/internal/module/Modules.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2025, 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 @@ -139,6 +139,45 @@ public class Modules { JLA.addEnableNativeAccessToAllUnnamed(); } + /** + * Enable code in all unnamed modules to mutate final instance fields. + */ + public static void addEnableFinalMutationToAllUnnamed() { + JLA.addEnableFinalMutationToAllUnnamed(); + } + + /** + * Enable code in a given module to mutate final instance fields. + */ + public static boolean tryEnableFinalMutation(Module m) { + return JLA.tryEnableFinalMutation(m); + } + + /** + * Return true if code in a given module is allowed to mutate final instance fields. + */ + public static boolean isFinalMutationEnabled(Module m) { + return JLA.isFinalMutationEnabled(m); + } + + /** + * Return true if a given module has statically exported the given package to a given + * other module. "statically exported" means the module declaration, --add-exports on + * the command line, or Add-Exports in the main manifest of an executable JAR. + */ + public static boolean isStaticallyExported(Module m, String pn, Module other) { + return JLA.isStaticallyExported(m, pn, other); + } + + /** + * Return true if a given module has statically opened the given package to a given + * other module. "statically open" means the module declaration, --add-opens on the + * command line, or Add-Opens in the main manifest of an executable JAR. + */ + public static boolean isStaticallyOpened(Module m, String pn, Module other) { + return JLA.isStaticallyOpened(m, pn, other); + } + /** * Updates module m to use a service. * Same as m2.addUses(service) but without a caller check. diff --git a/src/java.base/share/classes/sun/launcher/LauncherHelper.java b/src/java.base/share/classes/sun/launcher/LauncherHelper.java index 81a4fe32110..9badf2beaeb 100644 --- a/src/java.base/share/classes/sun/launcher/LauncherHelper.java +++ b/src/java.base/share/classes/sun/launcher/LauncherHelper.java @@ -103,6 +103,7 @@ public final class LauncherHelper { private static final String ADD_EXPORTS = "Add-Exports"; private static final String ADD_OPENS = "Add-Opens"; private static final String ENABLE_NATIVE_ACCESS = "Enable-Native-Access"; + private static final String ENABLE_FINAL_FIELD_MUTATION = "Enable-Final-Field-Mutation"; private static StringBuilder outBuf = new StringBuilder(); @@ -647,6 +648,8 @@ public final class LauncherHelper { if (opens != null) { addExportsOrOpens(opens, true); } + + // Enable-Native-Access String enableNativeAccess = mainAttrs.getValue(ENABLE_NATIVE_ACCESS); if (enableNativeAccess != null) { if (!enableNativeAccess.equals("ALL-UNNAMED")) { @@ -655,6 +658,16 @@ public final class LauncherHelper { Modules.addEnableNativeAccessToAllUnnamed(); } + // Enable-Final-Field-Mutation + String enableFinalFieldMutation = mainAttrs.getValue(ENABLE_FINAL_FIELD_MUTATION); + if (enableFinalFieldMutation != null) { + if (!enableFinalFieldMutation.equals("ALL-UNNAMED")) { + abort(null, "java.launcher.jar.error.illegal.effm.value", + enableFinalFieldMutation); + } + Modules.addEnableFinalMutationToAllUnnamed(); + } + /* * Hand off to FXHelper if it detects a JavaFX application * This must be done after ensuring a Main-Class entry diff --git a/src/java.base/share/classes/sun/launcher/resources/launcher.properties b/src/java.base/share/classes/sun/launcher/resources/launcher.properties index 83e08a3e11c..739ef8b8a29 100644 --- a/src/java.base/share/classes/sun/launcher/resources/launcher.properties +++ b/src/java.base/share/classes/sun/launcher/resources/launcher.properties @@ -70,6 +70,14 @@ java.launcher.opt.footer = \ \ by code in modules for which native access is not explicitly enabled.\n\ \ is one of "deny", "warn" or "allow". The default value is "warn".\n\ \ This option will be removed in a future release.\n\ +\ --enable-final-field-mutation [,...]\n\ +\ allow code in the specified modules to mutate final instance fields.\n\ +\ can also be ALL-UNNAMED to indicate code on the class path.\n\ +\ --illegal-final-field-mutation=\n\ +\ allow or deny final field mutation by code in modules for which final\n\ +\ field mutation is not explicitly enabled.\n\ +\ is one of "deny", "warn", "debug", or "allow". The default value is "warn".\n\ +\ This option will be removed in a future release.\n\ \ --list-modules\n\ \ list observable modules and exit\n\ \ -d \n\ @@ -291,6 +299,8 @@ java.launcher.jar.error5=\ Error: An unexpected error occurred while trying to close file {0} java.launcher.jar.error.illegal.ena.value=\ Error: illegal value \"{0}\" for Enable-Native-Access manifest attribute. Only 'ALL-UNNAMED' is allowed +java.launcher.jar.error.illegal.effm.value=\ + Error: illegal value \"{0}\" for Enable-Final-Field-Mutation manifest attribute. Only 'ALL-UNNAMED' is allowed java.launcher.init.error=initialization error java.launcher.javafx.error1=\ Error: The JavaFX launchApplication method has the wrong signature, it\n\ diff --git a/src/java.base/share/man/java.md b/src/java.base/share/man/java.md index 43719ff619a..462baa5a4a0 100644 --- a/src/java.base/share/man/java.md +++ b/src/java.base/share/man/java.md @@ -450,7 +450,7 @@ the JVM. > **Note:** This option will be removed in a future release. - `allow`: This mode allows illegal native access in all modules, - without any warings. + without any warnings. - `warn`: This mode is identical to `allow` except that a warning message is issued for the first illegal native access found in a module. @@ -465,6 +465,39 @@ the JVM. run it with `--illegal-native-access=deny` along with any necessary `--enable-native-access` options. +`--enable-final-field-mutation` *module*\[,*module*...\] +: Mutation of final fields is possible with the reflection API of the Java Platform. + However, it compromises safety and performance in all programs. + This option allows code in the specified modules to mutate final fields by reflection. + Attempts by code in any other module to mutate final fields by reflection are deemed _illegal_. + + *module* can be the name of a module on the module path, or `ALL-UNNAMED` to indicate + code on the class path. + +-`--illegal-final-field-mutation=`*parameter* +: This option specifies a mode for how _illegal_ final field mutation is handled: + + > **Note:** This option will be removed in a future release. + + - `allow`: This mode allows illegal final field mutation in all modules, + without any warnings. + + - `warn`: This mode is identical to `allow` except that a warning message is + issued for the first illegal final field mutation performaed in a module. + This mode is the default for the current JDK but will change in a future + release. + + - `debug`: This mode is identical to `allow` except that a warning message + and stack trace are printed for every illegal final field mutation. + + - `deny`: This mode disables final field mutation. That is, any illegal final + field mutation access causes an `IllegalAccessException`. This mode will + become the default in a future release. + + To verify that your application is ready for a future version of the JDK, + run it with `--illegal-final-field-mutation=deny` along with any necessary + `--enable-final-field-mutation` options. + `--finalization=`*value* : Controls whether the JVM performs finalization of objects. Valid values are "enabled" and "disabled". Finalization is enabled by default, so the @@ -701,7 +734,8 @@ the Java HotSpot Virtual Machine. - A class descriptor is in decorated format (`Lname;`) when it should not be. - A `NULL` parameter is allowed, but its use is questionable. - Calling other JNI functions in the scope of `Get/ReleasePrimitiveArrayCritical` - or `Get/ReleaseStringCritical` + or `Get/ReleaseStringCritical`. + - A JNI call was made to mutate a final field. Expect a performance degradation when this option is used. diff --git a/src/jdk.jfr/share/classes/jdk/jfr/events/FinalFieldMutationEvent.java b/src/jdk.jfr/share/classes/jdk/jfr/events/FinalFieldMutationEvent.java new file mode 100644 index 00000000000..8f90ad2c8fd --- /dev/null +++ b/src/jdk.jfr/share/classes/jdk/jfr/events/FinalFieldMutationEvent.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2025, 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. Oracle designates this + * particular file as subject to the "Classpath" exception as provided + * by Oracle in the LICENSE file that accompanied this code. + * + * 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 jdk.jfr.events; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Label; +import jdk.jfr.Name; +import jdk.jfr.internal.Type; +import jdk.jfr.internal.MirrorEvent; +import jdk.jfr.internal.RemoveFields; + +@Category("Java Application") +@Label("Final Field Mutation") +@Name(Type.EVENT_NAME_PREFIX + "FinalFieldMutation") +@RemoveFields("duration") +@StackFilter({ + "java.lang.reflect.Field", + "java.lang.reflect.ReflectAccess", + "java.lang.invoke.MethodHandles$Lookup" +}) +public final class FinalFieldMutationEvent extends MirrorEvent { + + @Label("Declaring Class") + @Description("Declaring class with final field") + public Class declaringClass; + + @Label("Field Name") + @Description("Field name of final field") + public String fieldName; + +} diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java index 503a7955e00..53d40c1e5e1 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/JDKEvents.java @@ -76,6 +76,7 @@ public final class JDKEvents { jdk.internal.event.VirtualThreadSubmitFailedEvent.class, jdk.internal.event.X509CertificateEvent.class, jdk.internal.event.X509ValidationEvent.class, + jdk.internal.event.FinalFieldMutationEvent.class, DirectBufferStatisticsEvent.class, InitialSecurityPropertyEvent.class, MethodTraceEvent.class, diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java index 48dc0d22cea..7cbd893ee1d 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/MirrorEvents.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -34,6 +34,7 @@ import jdk.jfr.events.ExceptionThrownEvent; import jdk.jfr.events.FileForceEvent; import jdk.jfr.events.FileReadEvent; import jdk.jfr.events.FileWriteEvent; +import jdk.jfr.events.FinalFieldMutationEvent; import jdk.jfr.events.ProcessStartEvent; import jdk.jfr.events.SecurityPropertyModificationEvent; import jdk.jfr.events.SecurityProviderServiceEvent; @@ -77,6 +78,7 @@ final class MirrorEvents { register("jdk.internal.event.ErrorThrownEvent", ErrorThrownEvent.class); register("jdk.internal.event.ExceptionStatisticsEvent", ExceptionStatisticsEvent.class); register("jdk.internal.event.ExceptionThrownEvent", ExceptionThrownEvent.class); + register("jdk.internal.event.FinalFieldMutationEvent", FinalFieldMutationEvent.class); }; private static void register(String eventClassName, Class mirrorClass) { diff --git a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java index 30bd96ba86a..eaba86e6327 100644 --- a/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java +++ b/src/jdk.jfr/share/classes/jdk/jfr/internal/PlatformEventType.java @@ -125,6 +125,7 @@ public final class PlatformEventType extends Type { Type.EVENT_NAME_PREFIX + "FileWrite" -> 6; case Type.EVENT_NAME_PREFIX + "FileRead", Type.EVENT_NAME_PREFIX + "FileForce" -> 5; + case Type.EVENT_NAME_PREFIX + "FinalFieldMutation" -> 4; default -> 3; }; } diff --git a/src/jdk.jfr/share/conf/jfr/default.jfc b/src/jdk.jfr/share/conf/jfr/default.jfc index eb3b8626722..e4dc3315d2b 100644 --- a/src/jdk.jfr/share/conf/jfr/default.jfc +++ b/src/jdk.jfr/share/conf/jfr/default.jfc @@ -117,6 +117,11 @@ everyChunk + + true + true + + true true diff --git a/src/jdk.jfr/share/conf/jfr/profile.jfc b/src/jdk.jfr/share/conf/jfr/profile.jfc index 5ffdc8d9e4d..619d7d90a53 100644 --- a/src/jdk.jfr/share/conf/jfr/profile.jfc +++ b/src/jdk.jfr/share/conf/jfr/profile.jfc @@ -117,6 +117,11 @@ everyChunk + + true + true + + true true diff --git a/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinals.java b/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinals.java new file mode 100644 index 00000000000..ecf89f1a5da --- /dev/null +++ b/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinals.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 2025, 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.lang.reflect.Method; +import java.util.Objects; + +/** + * Invoked by MutateFinalsTest, either directly or in a child VM, with the name of the + * test method in this class to execute. + */ + +public class MutateFinals { + + /** + * Usage: java MutateFinals + */ + public static void main(String[] args) throws Exception { + invoke(args[0]); + } + + /** + * Invokes the given method. + */ + static void invoke(String methodName) throws Exception { + Method m = MutateFinals.class.getDeclaredMethod(methodName); + m.invoke(null); + } + + /** + * JNI SetObjectField. + */ + private static void testJniSetObjectField() throws Exception { + class C { + final Object value; + C(Object value) { + this.value = value; + } + } + Object oldValue = new Object(); + Object newValue = new Object(); + var obj = new C(oldValue); + jniSetObjectField(obj, newValue); + assertTrue(obj.value == newValue); + } + + /** + * JNI SetBooleanField. + */ + private static void testJniSetBooleanField() throws Exception { + class C { + final boolean value; + C(boolean value) { + this.value = value; + } + } + var obj = new C(false); + jniSetBooleanField(obj, true); + assertTrue(obj.value); + } + + /** + * JNI SetByteField. + */ + private static void testJniSetByteField() throws Exception { + class C { + final byte value; + C(byte value) { + this.value = value; + } + } + byte oldValue = (byte) 1; + byte newValue = (byte) 2; + var obj = new C(oldValue); + jniSetByteField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetCharField. + */ + private static void testJniSetCharField() throws Exception { + class C { + final char value; + C(char value) { + this.value = value; + } + } + char oldValue = 'A'; + char newValue = 'B'; + var obj = new C(oldValue); + jniSetCharField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetShortField. + */ + private static void testJniSetShortField() throws Exception { + class C { + final short value; + C(short value) { + this.value = value; + } + } + short oldValue = (short) 1; + short newValue = (short) 2; + var obj = new C(oldValue); + jniSetShortField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetIntField. + */ + private static void testJniSetIntField() throws Exception { + class C { + final int value; + C(int value) { + this.value = value; + } + } + int oldValue = 1; + int newValue = 2; + var obj = new C(oldValue); + jniSetIntField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetLongField. + */ + private static void testJniSetLongField() throws Exception { + class C { + final long value; + C(long value) { + this.value = value; + } + } + long oldValue = 1L; + long newValue = 2L; + var obj = new C(oldValue); + jniSetLongField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetFloatField. + */ + private static void testJniSetFloatField() throws Exception { + class C { + final float value; + C(float value) { + this.value = value; + } + } + float oldValue = 1.0f; + float newValue = 2.0f; + var obj = new C(oldValue); + jniSetFloatField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetDoubleField. + */ + private static void testJniSetDoubleField() throws Exception { + class C { + final double value; + C(double value) { + this.value = value; + } + } + double oldValue = 1.0d; + double newValue = 2.0d; + var obj = new C(oldValue); + jniSetDoubleField(obj, newValue); + assertEquals(newValue, obj.value); + } + + /** + * JNI SetStaticObjectField. + */ + private static void testJniSetStaticObjectField() throws Exception { + class C { + static final Object value = new Object(); + } + Object newValue = new Object(); + jniSetStaticObjectField(C.class, newValue); + assertTrue(C.value == newValue); + } + + /** + * JNI SetStaticBooleanField. + */ + private static void testJniSetStaticBooleanField() throws Exception { + class C { + static final boolean value = false; + } + jniSetStaticBooleanField(C.class, true); + // use reflection as field treated as constant by compiler + boolean value = (boolean) C.class.getDeclaredField("value").get(null); + assertTrue(value); + } + + /** + * JNI SetStaticByteField. + */ + private static void testJniSetStaticByteField() throws Exception { + class C { + static final byte value = (byte) 1; + } + byte newValue = (byte) 2; + jniSetStaticByteField(C.class, newValue); + // use reflection as field treated as constant by compiler + byte value = (byte) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticCharField. + */ + private static void testJniSetStaticCharField() throws Exception { + class C { + static final char value = 'A'; + } + char newValue = 'B'; + jniSetStaticCharField(C.class, newValue); + // use reflection as field treated as constant by compiler + char value = (char) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticShortField. + */ + private static void testJniSetStaticShortField() throws Exception { + class C { + static final short value = (short) 1; + } + short newValue = (short) 2; + jniSetStaticShortField(C.class, newValue); + // use reflection as field treated as constant by compiler + short value = (short) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticIntField. + */ + private static void testJniSetStaticIntField() throws Exception { + class C { + static final int value = 1; + } + int newValue = 2; + jniSetStaticIntField(C.class, newValue); + // use reflection as field treated as constant by compiler + int value = (int) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticLongField. + */ + private static void testJniSetStaticLongField() throws Exception { + class C { + static final long value = 1L; + } + long newValue = 2L; + jniSetStaticLongField(C.class, newValue); + // use reflection as field treated as constant by compiler + long value = (long) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticFloatField. + */ + private static void testJniSetStaticFloatField() throws Exception { + class C { + static final float value = 1.0f; + } + float newValue = 2.0f; + jniSetStaticFloatField(C.class, newValue); + // use reflection as field treated as constant by compiler + float value = (float) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + /** + * JNI SetStaticDoubleField. + */ + private static void testJniSetStaticDoubleField() throws Exception { + class C { + static final double value = 1.0d; + } + double newValue = 2.0f; + jniSetStaticDoubleField(C.class, newValue); + // use reflection as field treated as constant by compiler + double value = (double) C.class.getDeclaredField("value").get(null); + assertEquals(newValue, value); + } + + private static native void jniSetObjectField(Object obj, Object value); + private static native void jniSetBooleanField(Object obj, boolean value); + private static native void jniSetByteField(Object obj, byte value); + private static native void jniSetCharField(Object obj, char value); + private static native void jniSetShortField(Object obj, short value); + private static native void jniSetIntField(Object obj, int value); + private static native void jniSetLongField(Object obj, long value); + private static native void jniSetFloatField(Object obj, float value); + private static native void jniSetDoubleField(Object obj, double value); + + private static native void jniSetStaticObjectField(Class clazz, Object value); + private static native void jniSetStaticBooleanField(Class clazz, boolean value); + private static native void jniSetStaticByteField(Class clazz, byte value); + private static native void jniSetStaticCharField(Class clazz, char value); + private static native void jniSetStaticShortField(Class clazz, short value); + private static native void jniSetStaticIntField(Class clazz, int value); + private static native void jniSetStaticLongField(Class clazz, long value); + private static native void jniSetStaticFloatField(Class clazz, float value); + private static native void jniSetStaticDoubleField(Class clazz, double value); + + static { + System.loadLibrary("MutateFinals"); + } + + private static void assertTrue(boolean e) { + if (!e) throw new RuntimeException("Not true as expected"); + } + + private static void assertEquals(Object expected, Object actual) { + if (!Objects.equals(expected, actual)) { + throw new RuntimeException("Actual: " + actual + ", expected: " + expected); + } + } +} diff --git a/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinalsTest.java b/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinalsTest.java new file mode 100644 index 00000000000..0ae4ae231b9 --- /dev/null +++ b/test/hotspot/jtreg/runtime/jni/mutateFinals/MutateFinalsTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test JNI SetXXXField methods to set final instance and final static fields + * @key randomness + * @modules java.management + * @library /test/lib + * @compile MutateFinals.java + * @run junit/native/timeout=300 MutateFinalsTest + */ + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; + +class MutateFinalsTest { + + static String javaLibraryPath; + + @BeforeAll + static void init() { + javaLibraryPath = System.getProperty("java.library.path"); + } + + /** + * The names of the test methods that use JNI to set final instance fields. + */ + static Stream mutateInstanceFieldMethods() { + return Stream.of( + "testJniSetObjectField", + "testJniSetBooleanField", + "testJniSetByteField", + "testJniSetCharField", + "testJniSetShortField", + "testJniSetIntField", + "testJniSetLongField", + "testJniSetFloatField", + "testJniSetDoubleField" + ); + } + + /** + * The names of the test methods that use JNI to set final static fields. + */ + static Stream mutateStaticFieldMethods() { + return Stream.of( + "testJniSetStaticObjectField", + "testJniSetStaticBooleanField", + "testJniSetStaticByteField", + "testJniSetStaticCharField", + "testJniSetStaticShortField", + "testJniSetStaticIntField", + "testJniSetStaticLongField", + "testJniSetStaticFloatField", + "testJniSetStaticDoubleField" + ); + } + + /** + * The names of all test methods that use JNI to set final fields. + */ + static Stream allMutationMethods() { + return Stream.concat(mutateInstanceFieldMethods(), mutateStaticFieldMethods()); + } + + /** + * Mutate a final field with JNI. + */ + @ParameterizedTest + @MethodSource("allMutationMethods") + void testMutateFinal(String methodName) throws Exception { + MutateFinals.invoke(methodName); + } + + /** + * Mutate a final instance field with JNI. The test launches a child VM with -Xcheck:jni + * and expects a warning in the output. + */ + @ParameterizedTest + @MethodSource("mutateInstanceFieldMethods") + void testMutateInstanceFinalWithXCheckJni(String methodName) throws Exception { + test(methodName, "-Xcheck:jni") + .shouldContain("WARNING in native method: SetField called to mutate final instance field") + .shouldHaveExitValue(0); + } + + /** + * Mutate final static fields with JNI. The test launches a child VM with -Xcheck:jni + * and expects a warning in the output. + */ + @ParameterizedTest + @MethodSource("mutateStaticFieldMethods") + void testMutateStaticFinalWithXCheckJni(String methodName) throws Exception { + test(methodName, "-Xcheck:jni") + .shouldContain("WARNING in native method: SetStaticField called to mutate final static field") + .shouldHaveExitValue(0); + } + + /** + * Mutate a final instance field with JNI. The test launches a child VM with -Xlog + * and expects a log message in the output. + */ + @ParameterizedTest + @MethodSource("mutateInstanceFieldMethods") + void testMutateInstanceFinalWithLogging(String methodName) throws Exception { + String type = methodName.contains("Object") ? "Object" : ""; + test(methodName, "-Xlog:jni=debug") + .shouldContain("[debug][jni] Set" + type + "Field mutated final instance field") + .shouldHaveExitValue(0); + } + + /** + * Mutate a final static field with JNI. The test launches a child VM with -Xlog + * and expects a log message in the output. + */ + @ParameterizedTest + @MethodSource("mutateStaticFieldMethods") + void testMutateStaticFinalWithLogging(String methodName) throws Exception { + String type = methodName.contains("Object") ? "Object" : ""; + test(methodName, "-Xlog:jni=debug") + .shouldContain("[debug][jni] SetStatic" + type + "Field mutated final static field") + .shouldHaveExitValue(0); + } + + /** + * Launches MutateFinals with the given method name as parameter, and the given VM options. + */ + private OutputAnalyzer test(String methodName, String... vmopts) throws Exception { + Stream s1 = Stream.of( + "-Djava.library.path=" + javaLibraryPath, + "--enable-native-access=ALL-UNNAMED"); + Stream s2 = Stream.of(vmopts); + Stream s3 = Stream.of("MutateFinals", methodName); + String[] opts = Stream.concat(Stream.concat(s1, s2), s3).toArray(String[]::new); + var outputAnalyzer = ProcessTools + .executeTestJava(opts) + .outputTo(System.err) + .errorTo(System.err); + return outputAnalyzer; + } +} diff --git a/test/hotspot/jtreg/runtime/jni/mutateFinals/libMutateFinals.c b/test/hotspot/jtreg/runtime/jni/mutateFinals/libMutateFinals.c new file mode 100644 index 00000000000..b903169484b --- /dev/null +++ b/test/hotspot/jtreg/runtime/jni/mutateFinals/libMutateFinals.c @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2025, 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. + */ + +#include +#include "jni.h" + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetObjectField(JNIEnv *env, jclass ignore, jobject obj, jobject value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "Ljava/lang/Object;"); + if (fid != NULL) { + (*env)->SetObjectField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetBooleanField(JNIEnv *env, jclass ignore, jobject obj, jboolean value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "Z"); + if (fid != NULL) { + (*env)->SetBooleanField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetByteField(JNIEnv *env, jclass ignore, jobject obj, jbyte value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "B"); + if (fid != NULL) { + (*env)->SetByteField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetCharField(JNIEnv *env, jclass ignore, jobject obj, jchar value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "C"); + if (fid != NULL) { + (*env)->SetCharField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetShortField(JNIEnv *env, jclass ignore, jobject obj, jshort value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "S"); + if (fid != NULL) { + (*env)->SetShortField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetIntField(JNIEnv *env, jclass ignore, jobject obj, jint value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "I"); + if (fid != NULL) { + (*env)->SetIntField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetLongField(JNIEnv *env, jclass ignore, jobject obj, jlong value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "J"); + if (fid != NULL) { + (*env)->SetLongField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetFloatField(JNIEnv *env, jclass ignore, jobject obj, jfloat value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "F"); + if (fid != NULL) { + (*env)->SetFloatField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetDoubleField(JNIEnv *env, jclass ignore, jobject obj, jdouble value) { + jclass clazz = (*env)->GetObjectClass(env, obj); + jfieldID fid = (*env)->GetFieldID(env, clazz, "value", "D"); + if (fid != NULL) { + (*env)->SetDoubleField(env, obj, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticObjectField(JNIEnv *env, jclass ignore, jclass clazz, jobject value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "Ljava/lang/Object;"); + if (fid != NULL) { + (*env)->SetStaticObjectField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticBooleanField(JNIEnv *env, jclass ignore, jclass clazz, jboolean value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "Z"); + if (fid != NULL) { + (*env)->SetStaticBooleanField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticByteField(JNIEnv *env, jclass ignore, jclass clazz, jbyte value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "B"); + if (fid != NULL) { + (*env)->SetStaticByteField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticCharField(JNIEnv *env, jclass ignore, jclass clazz, jchar value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "C"); + if (fid != NULL) { + (*env)->SetStaticCharField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticShortField(JNIEnv *env, jclass ignore, jclass clazz, jshort value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "S"); + if (fid != NULL) { + (*env)->SetStaticShortField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticIntField(JNIEnv *env, jclass ignore, jclass clazz, jint value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "I"); + if (fid != NULL) { + (*env)->SetStaticIntField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticLongField(JNIEnv *env, jclass ignore, jclass clazz, jlong value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "J"); + if (fid != NULL) { + (*env)->SetStaticLongField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticFloatField(JNIEnv *env, jclass ignore, jclass clazz, jfloat value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "F"); + if (fid != NULL) { + (*env)->SetStaticFloatField(env, clazz, fid, value); + } +} + +JNIEXPORT void JNICALL Java_MutateFinals_jniSetStaticDoubleField(JNIEnv *env, jclass ignore, jclass clazz, jdouble value) { + jfieldID fid = (*env)->GetStaticFieldID(env, clazz, "value", "D"); + if (fid != NULL) { + (*env)->SetStaticDoubleField(env, clazz, fid, value); + } +} diff --git a/test/jdk/java/lang/invoke/MethodHandlesGeneralTest.java b/test/jdk/java/lang/invoke/MethodHandlesGeneralTest.java index 7997e1266c2..b60c35fc30b 100644 --- a/test/jdk/java/lang/invoke/MethodHandlesGeneralTest.java +++ b/test/jdk/java/lang/invoke/MethodHandlesGeneralTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -29,6 +29,7 @@ * @run junit/othervm/timeout=2500 -XX:+IgnoreUnrecognizedVMOptions * -XX:-VerifyDependencies * -esa + * --enable-final-field-mutation=ALL-UNNAMED * test.java.lang.invoke.MethodHandlesGeneralTest */ diff --git a/test/jdk/java/lang/invoke/VarHandles/accessibility/TestFieldLookupAccessibility.java b/test/jdk/java/lang/invoke/VarHandles/accessibility/TestFieldLookupAccessibility.java index 9e2d01ec7c5..343b15a1caf 100644 --- a/test/jdk/java/lang/invoke/VarHandles/accessibility/TestFieldLookupAccessibility.java +++ b/test/jdk/java/lang/invoke/VarHandles/accessibility/TestFieldLookupAccessibility.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -27,10 +27,12 @@ * @compile TestFieldLookupAccessibility.java * pkg/A.java pkg/B_extends_A.java pkg/C.java * pkg/subpkg/B_extends_A.java pkg/subpkg/C.java - * @run testng/othervm TestFieldLookupAccessibility + * @run testng/othervm --enable-final-field-mutation=ALL-UNNAMED -DwriteAccess=true TestFieldLookupAccessibility + * @run testng/othervm --illegal-final-field-mutation=deny -DwriteAccess=false TestFieldLookupAccessibility */ -import org.testng.Assert; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import pkg.B_extends_A; @@ -48,6 +50,14 @@ import java.util.stream.Collectors; import java.util.stream.Stream; public class TestFieldLookupAccessibility { + static boolean writeAccess; + + @BeforeClass + static void setup() { + String s = System.getProperty("writeAccess"); + assertNotNull(s); + writeAccess = Boolean.valueOf(s); + } // The set of possible field lookup mechanisms enum FieldLookup { @@ -118,7 +128,11 @@ public class TestFieldLookupAccessibility { } boolean isAccessible(Field f) { - return !(Modifier.isStatic(f.getModifiers()) && Modifier.isFinal(f.getModifiers())); + if (Modifier.isFinal(f.getModifiers())) { + return !Modifier.isStatic(f.getModifiers()) && writeAccess; + } else { + return true; + } } // Setting the accessibility bit of a Field grants access to non-static @@ -226,15 +240,15 @@ public class TestFieldLookupAccessibility { collect(Collectors.toSet()); if (!actualFieldNames.equals(expected)) { if (actualFieldNames.isEmpty()) { - Assert.assertEquals(actualFieldNames, expected, "No accessibility failures:"); + assertEquals(actualFieldNames, expected, "No accessibility failures:"); } else { - Assert.assertEquals(actualFieldNames, expected, "Accessibility failures differ:"); + assertEquals(actualFieldNames, expected, "Accessibility failures differ:"); } } else { if (!actual.values().stream().allMatch(IllegalAccessException.class::isInstance)) { - Assert.fail("Expecting an IllegalArgumentException for all failures " + actual); + fail("Expecting an IllegalArgumentException for all failures " + actual); } } } diff --git a/test/jdk/java/lang/invoke/unreflect/UnreflectTest.java b/test/jdk/java/lang/invoke/unreflect/UnreflectTest.java index 31dc851f4ea..b034d63a223 100644 --- a/test/jdk/java/lang/invoke/unreflect/UnreflectTest.java +++ b/test/jdk/java/lang/invoke/unreflect/UnreflectTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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,9 +24,10 @@ /* * @test * @bug 8238358 8247444 - * @run testng/othervm UnreflectTest * @summary Test Lookup::unreflectSetter and Lookup::unreflectVarHandle on * trusted final fields (declared in hidden classes and records) + * @run junit/othervm --enable-final-field-mutation=ALL-UNNAMED -DwriteAccess=true UnreflectTest + * @run junit/othervm --illegal-final-field-mutation=deny -DwriteAccess=false UnreflectTest */ import java.io.IOException; @@ -38,25 +39,25 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; -import org.testng.annotations.Test; -import static org.testng.Assert.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import static org.junit.jupiter.api.Assertions.*; -public class UnreflectTest { - static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); - static final Class hiddenClass = defineHiddenClass(); - private static Class defineHiddenClass() { +class UnreflectTest { + static Class hiddenClass; + static boolean writeAccess; + + @BeforeAll + static void setup() throws Exception { String classes = System.getProperty("test.classes"); - Path cf = Paths.get(classes, "Fields.class"); - try { - byte[] bytes = Files.readAllBytes(cf); - return MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + Path cf = Path.of(classes, "Fields.class"); + byte[] bytes = Files.readAllBytes(cf); + hiddenClass = MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass(); + + String s = System.getProperty("writeAccess"); + assertNotNull(s); + writeAccess = Boolean.valueOf(s); } /* @@ -64,7 +65,7 @@ public class UnreflectTest { * can write the value of a non-static final field in a normal class */ @Test - public void testFieldsInNormalClass() throws Throwable { + void testFieldsInNormalClass() throws Throwable { // despite the name "HiddenClass", this class is loaded by the // class loader as non-hidden class Class c = Fields.class; @@ -72,7 +73,11 @@ public class UnreflectTest { assertFalse(c.isHidden()); readOnlyAccessibleObject(c, "STATIC_FINAL", null, true); readWriteAccessibleObject(c, "STATIC_NON_FINAL", null, false); - readWriteAccessibleObject(c, "FINAL", o, true); + if (writeAccess) { + readWriteAccessibleObject(c, "FINAL", o, true); + } else { + readOnlyAccessibleObject(c, "FINAL", o, true); + } readWriteAccessibleObject(c, "NON_FINAL", o, false); } @@ -81,7 +86,7 @@ public class UnreflectTest { * has NO write the value of a non-static final field in a hidden class */ @Test - public void testFieldsInHiddenClass() throws Throwable { + void testFieldsInHiddenClass() throws Throwable { assertTrue(hiddenClass.isHidden()); Object o = hiddenClass.newInstance(); readOnlyAccessibleObject(hiddenClass, "STATIC_FINAL", null, true); @@ -99,7 +104,8 @@ public class UnreflectTest { * Test Lookup::unreflectSetter and Lookup::unreflectVarHandle that * cannot write the value of a non-static final field in a record class */ - public void testFieldsInRecordClass() throws Throwable { + @Test + void testFieldsInRecordClass() throws Throwable { assertTrue(TestRecord.class.isRecord()); Object o = new TestRecord(1); readOnlyAccessibleObject(TestRecord.class, "STATIC_FINAL", null, true); @@ -121,16 +127,12 @@ public class UnreflectTest { assertTrue(f.trySetAccessible()); // Field object with read-only access - MethodHandle mh = LOOKUP.unreflectGetter(f); + MethodHandles.Lookup lookup = MethodHandles.lookup(); + MethodHandle mh = lookup.unreflectGetter(f); Object value = Modifier.isStatic(modifier) ? mh.invoke() : mh.invoke(o); assertTrue(value == f.get(o)); - try { - LOOKUP.unreflectSetter(f); - assertTrue(false, "should fail to unreflect a setter for " + name); - } catch (IllegalAccessException e) { - } - - VarHandle vh = LOOKUP.unreflectVarHandle(f); + assertThrows(IllegalAccessException.class, () -> lookup.unreflectSetter(f)); + VarHandle vh = lookup.unreflectVarHandle(f); if (isFinal) { assertFalse(vh.isAccessModeSupported(VarHandle.AccessMode.SET)); } else { @@ -163,7 +165,7 @@ public class UnreflectTest { throw e; } - VarHandle vh = LOOKUP.unreflectVarHandle(f); + VarHandle vh = MethodHandles.lookup().unreflectVarHandle(f); if (isFinal) { assertFalse(vh.isAccessModeSupported(VarHandle.AccessMode.SET)); } else { diff --git a/test/jdk/java/lang/reflect/AccessibleObject/HiddenClassTest.java b/test/jdk/java/lang/reflect/AccessibleObject/HiddenClassTest.java index 273523bee65..6fc03934666 100644 --- a/test/jdk/java/lang/reflect/AccessibleObject/HiddenClassTest.java +++ b/test/jdk/java/lang/reflect/AccessibleObject/HiddenClassTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2020, 2025, 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,9 +23,9 @@ /** * @test - * @build Fields HiddenClassTest - * @run testng/othervm HiddenClassTest * @summary Test java.lang.reflect.AccessibleObject with modules + * @run junit/othervm --enable-final-field-mutation=ALL-UNNAMED -DwriteAccess=true HiddenClassTest + * @run junit/othervm --illegal-final-field-mutation=deny -DwriteAccess=false HiddenClassTest */ import java.io.IOException; @@ -37,22 +37,24 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import org.testng.annotations.Test; -import static org.testng.Assert.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeAll; +import static org.junit.jupiter.api.Assertions.*; -public class HiddenClassTest { - static final Class hiddenClass = defineHiddenClass(); - private static Class defineHiddenClass() { +class HiddenClassTest { + static Class hiddenClass; + static boolean writeAccess; + + @BeforeAll + static void setup() throws Exception { String classes = System.getProperty("test.classes"); - Path cf = Paths.get(classes, "Fields.class"); - try { - byte[] bytes = Files.readAllBytes(cf); - return MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass(); - } catch (IOException e) { - throw new UncheckedIOException(e); - } catch (IllegalAccessException e) { - throw new RuntimeException(e); - } + Path cf = Path.of(classes, "Fields.class"); + byte[] bytes = Files.readAllBytes(cf); + hiddenClass = MethodHandles.lookup().defineHiddenClass(bytes, true).lookupClass(); + + String s = System.getProperty("writeAccess"); + assertNotNull(s); + writeAccess = Boolean.valueOf(s); } /* @@ -60,7 +62,7 @@ public class HiddenClassTest { * in a normal class */ @Test - public void testFieldsInNormalClass() throws Throwable { + void testFieldsInNormalClass() throws Throwable { // despite the name "HiddenClass", this class is loaded by the // class loader as non-hidden class Class c = Fields.class; @@ -68,7 +70,11 @@ public class HiddenClassTest { assertFalse(c.isHidden()); readOnlyAccessibleObject(c, "STATIC_FINAL", null, true); readWriteAccessibleObject(c, "STATIC_NON_FINAL", null, false); - readWriteAccessibleObject(c, "FINAL", o, true); + if (writeAccess) { + readWriteAccessibleObject(c, "FINAL", o, true); + } else { + readOnlyAccessibleObject(c, "FINAL", o, true); + } readWriteAccessibleObject(c, "NON_FINAL", o, false); } @@ -77,7 +83,7 @@ public class HiddenClassTest { * in a hidden class */ @Test - public void testFieldsInHiddenClass() throws Throwable { + void testFieldsInHiddenClass() throws Throwable { assertTrue(hiddenClass.isHidden()); Object o = hiddenClass.newInstance(); readOnlyAccessibleObject(hiddenClass, "STATIC_FINAL", null, true); @@ -96,11 +102,7 @@ public class HiddenClassTest { } assertTrue(f.trySetAccessible()); assertTrue(f.get(o) != null); - try { - f.set(o, null); - assertTrue(false, "should fail to set " + name); - } catch (IllegalAccessException e) { - } + assertThrows(IllegalAccessException.class, () -> f.set(o, null)); } private static void readWriteAccessibleObject(Class c, String name, Object o, boolean isFinal) throws Exception { @@ -113,10 +115,6 @@ public class HiddenClassTest { } assertTrue(f.trySetAccessible()); assertTrue(f.get(o) != null); - try { - f.set(o, null); - } catch (IllegalAccessException e) { - throw e; - } + f.set(o, null); } } diff --git a/test/jdk/java/lang/reflect/Field/NegativeTest.java b/test/jdk/java/lang/reflect/Field/NegativeTest.java index 874686e55b1..fe1f2dd7480 100644 --- a/test/jdk/java/lang/reflect/Field/NegativeTest.java +++ b/test/jdk/java/lang/reflect/Field/NegativeTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2021, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2021, 2025, 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,9 +24,9 @@ /** * @test * @bug 8277451 - * @run testng NegativeTest * @summary Test exception thrown due to bad receiver and bad value on * Field with and without setAccessible(true) + * @run testng/othervm --enable-final-field-mutation=ALL-UNNAMED NegativeTest */ import java.lang.reflect.Field; diff --git a/test/jdk/java/lang/reflect/Field/Set.java b/test/jdk/java/lang/reflect/Field/Set.java index 6232026cee3..b369ea07d45 100644 --- a/test/jdk/java/lang/reflect/Field/Set.java +++ b/test/jdk/java/lang/reflect/Field/Set.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1999, 2004, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1999, 2025, 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 @@ * @bug 4250960 5044412 * @summary Should not be able to set final fields through reflection unless setAccessible(true) passes and is not static * @author David Bowen (modified by Doug Lea) + * @run main/othervm --enable-final-field-mutation=ALL-UNNAMED Set */ import java.lang.reflect.*; diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java b/test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java new file mode 100644 index 00000000000..ea1a28221fb --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Basic test for JFR FinalFieldMutation event + * @requires vm.hasJFR + * @modules jdk.jfr/jdk.jfr.events + * @run junit FinalFieldMutationEventTest + * @run junit/othervm --illegal-final-field-mutation=allow FinalFieldMutationEventTest + * @run junit/othervm --illegal-final-field-mutation=warn FinalFieldMutationEventTest + * @run junit/othervm --illegal-final-field-mutation=debug FinalFieldMutationEventTest + * @run junit/othervm --illegal-final-field-mutation=deny FinalFieldMutationEventTest + */ + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Stream; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedClass; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordedMethod; +import jdk.jfr.consumer.RecordingFile; +import jdk.jfr.events.FinalFieldMutationEvent; +import jdk.jfr.events.StackFilter; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class FinalFieldMutationEventTest { + private static final String EVENT_NAME = "jdk.FinalFieldMutation"; + + /** + * Test class with final field. + */ + private static class C { + final int value; + C(int value) { + this.value = value; + } + } + + /** + * Test jdk.FinalFieldMutation is recorded when mutating a final field. + */ + @Test + void testFieldSet() throws Exception { + Field field = C.class.getDeclaredField("value"); + field.setAccessible(true); + + try (Recording recording = new Recording()) { + recording.enable(EVENT_NAME).withStackTrace(); + + boolean mutated = false; + recording.start(); + try { + var obj = new C(100); + try { + field.setInt(obj, 200); + mutated = true; + } catch (IllegalAccessException e) { + // denied + } + } finally { + recording.stop(); + } + + // FinalFieldMutation event should be recorded if field mutated + List events = find(recording, EVENT_NAME); + System.err.println(events); + if (mutated) { + assertEquals(1, events.size(), "1 event expected"); + checkEvent(events.get(0), field, "FinalFieldMutationEventTest::testFieldSet"); + } else { + assertEquals(0, events.size(), "No events expected"); + } + } + } + + /** + * Test jdk.FinalFieldMutation is recorded when unreflecting a field field for mutation. + */ + @Test + void testUnreflectSetter() throws Exception { + Field field = C.class.getDeclaredField("value"); + field.setAccessible(true); + + try (Recording recording = new Recording()) { + recording.enable(EVENT_NAME).withStackTrace(); + + boolean unreflected = false; + recording.start(); + try { + MethodHandles.lookup().unreflectSetter(field); + unreflected = true; + } catch (IllegalAccessException e) { + // denied + } finally { + recording.stop(); + } + + // FinalFieldMutation event should be recorded if field unreflected for set + List events = find(recording, EVENT_NAME); + System.err.println(events); + if (unreflected) { + assertEquals(1, events.size(), "1 event expected"); + checkEvent(events.get(0), field, "FinalFieldMutationEventTest::testUnreflectSetter"); + } else { + assertEquals(0, events.size(), "No events expected"); + } + } + } + + /** + * Test that a FinalFieldMutationEvent event has the declaringClass and fieldName of + * the given Field, and the expected top frame. + */ + private void checkEvent(RecordedEvent e, Field f, String expectedTopFrame) { + RecordedClass clazz = e.getClass("declaringClass"); + assertNotNull(clazz); + assertEquals(f.getDeclaringClass().getName(), clazz.getName()); + assertEquals(f.getName(), e.getString("fieldName")); + + // check the top-frame of the stack trace + RecordedMethod m = e.getStackTrace().getFrames().getFirst().getMethod(); + assertEquals(expectedTopFrame, m.getType().getName() + "::" + m.getName()); + } + + /** + * Tests that FinalFieldMutationEvent's stack filter value names classes/methods that + * exist. This will help detect stale values when the implementation is refactored. + */ + @Test + void testFinalFieldMutationEventStackFilter() throws Exception { + String[] filters = FinalFieldMutationEvent.class.getAnnotation(StackFilter.class).value(); + for (String filter : filters) { + String[] classAndMethod = filter.split("::"); + String cn = classAndMethod[0]; + + // throws if class not found + Class clazz = Class.forName(cn); + + // if the filter has a method name then check a method of that name exists + if (classAndMethod.length > 1) { + String mn = classAndMethod[1]; + Method method = Stream.of(clazz.getDeclaredMethods()) + .filter(m -> m.getName().equals(mn)) + .findFirst() + .orElse(null); + assertNotNull(method, cn + "::" + mn + " not found"); + } + } + } + + /** + * Returns the list of events in the given recording with the given name. + */ + private List find(Recording recording, String name) throws Exception { + Path recordingFile = recordingFile(recording); + return RecordingFile.readAllEvents(recordingFile) + .stream() + .filter(e -> e.getEventType().getName().equals(name)) + .toList(); + } + + /** + * Return the file path to the recording file. + */ + private Path recordingFile(Recording recording) throws Exception { + Path recordingFile = recording.getDestination(); + if (recordingFile == null) { + ProcessHandle h = ProcessHandle.current(); + recordingFile = Path.of("recording-" + recording.getId() + "-pid" + h.pid() + ".jfr"); + recording.dump(recordingFile); + } + return recordingFile; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/MutateFinalsTest.java b/test/jdk/java/lang/reflect/Field/mutateFinals/MutateFinalsTest.java new file mode 100644 index 00000000000..78a7849dfc0 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/MutateFinalsTest.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test Field.set and Lookup.unreflectSetter on final instance fields + * @run junit/othervm -DwriteAccess=true MutateFinalsTest + * @run junit/othervm --enable-final-field-mutation=ALL-UNNAMED -DwriteAccess=true MutateFinalsTest + * @run junit/othervm --illegal-final-field-mutation=allow -DwriteAccess=true MutateFinalsTest + * @run junit/othervm --illegal-final-field-mutation=warn -DwriteAccess=true MutateFinalsTest + * @run junit/othervm --illegal-final-field-mutation=debug -DwriteAccess=true MutateFinalsTest + * @run junit/othervm --illegal-final-field-mutation=deny -DwriteAccess=false MutateFinalsTest + */ + +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +class MutateFinalsTest { + static boolean writeAccess; + + @BeforeAll + static void setup() throws Exception { + String s = System.getProperty("writeAccess"); + assertNotNull(s); + writeAccess = Boolean.valueOf(s); + } + + @Test + void testFieldSet() throws Exception { + class C { + final String value; + C(String value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + String oldValue = "oldValue"; + String newValue = "newValue"; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.set(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.set(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.set(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.set("not a C", newValue)); + assertThrows(IllegalArgumentException.class, () -> f.set(obj, 100)); // not a string + f.set(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.set(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetBoolean() throws Throwable { + class C { + final boolean value; + C(boolean value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + boolean oldValue = false; + boolean newValue = true; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setBoolean(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setBoolean(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setBoolean(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setBoolean("not a C", newValue)); + f.setBoolean(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setBoolean(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetByte() throws Exception { + class C { + final byte value; + C(byte value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + byte oldValue = (byte) 1; + byte newValue = (byte) 2; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setByte(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setByte(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setByte(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setByte("not a C", newValue)); + f.setByte(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setByte(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetChar() throws Exception { + class C { + final char value; + C(char value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + char oldValue = 'A'; + char newValue = 'B'; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setChar(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setChar(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setChar(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setChar("not a C", newValue)); + f.setChar(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setChar(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetShort() throws Exception { + class C { + final short value; + C(short value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + short oldValue = (short) 1; + short newValue = (short) 2; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setShort(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setShort(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setShort(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setShort("not a C", newValue)); + f.setShort(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setShort(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetInt() throws Exception { + class C { + final int value; + C(int value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + int oldValue = 1; + int newValue = 2; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setInt(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setInt(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setInt(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setInt("not a C", newValue)); + f.setInt(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setInt(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetLong() throws Exception { + class C { + final long value; + C(long value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + long oldValue = 1L; + long newValue = 2L; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setLong(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setLong(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setLong(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setLong("not a C", newValue)); + f.setLong(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setLong(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetFloat() throws Exception { + class C { + final float value; + C(float value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + float oldValue = 1.0f; + float newValue = 2.0f; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setFloat(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setFloat(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setFloat(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setFloat("not a C", newValue)); + f.setFloat(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setFloat(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testFieldSetDouble() throws Exception { + class C { + final double value; + C(double value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + double oldValue = 1.0d; + double newValue = 2.0d; + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(NullPointerException.class, () -> f.setDouble(null, newValue)); + assertThrows(IllegalAccessException.class, () -> f.setDouble(obj, newValue)); + assertTrue(obj.value == oldValue); + + f.setAccessible(true); + assertThrows(NullPointerException.class, () -> f.setDouble(null, newValue)); + if (writeAccess) { + assertThrows(IllegalArgumentException.class, () -> f.setDouble("not a C", newValue)); + f.setDouble(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> f.setDouble(obj, newValue)); + assertTrue(obj.value == oldValue); + } + } + + @Test + void testUnreflectSetter() throws Throwable { + class C { + final Object value; + C(Object value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + + Object oldValue = new Object(); + var obj = new C(oldValue); + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> MethodHandles.lookup().unreflectSetter(f)); + + f.setAccessible(true); + if (writeAccess) { + Object newValue = new Object(); + MethodHandles.lookup().unreflectSetter(f).invoke(obj, newValue); + assertTrue(obj.value == newValue); + } else { + assertThrows(IllegalAccessException.class, () -> MethodHandles.lookup().unreflectSetter(f)); + } + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTest.java b/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTest.java new file mode 100644 index 00000000000..34f9bb2bd5b --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTest.java @@ -0,0 +1,294 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test the command line option --enable-final-field-mutation + * @library /test/lib + * @build CommandLineTestHelper + * @run junit CommandLineTest + */ + +import java.util.regex.Pattern; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.*; + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; + +class CommandLineTest { + + // helper class name + private static final String HELPER = "CommandLineTestHelper"; + + // warning output + private static final String WARNING_LINE1 = + "WARNING: Final field value in class " + HELPER; + private static final String WARNING_LINE3 = + "WARNING: Use --enable-final-field-mutation=ALL-UNNAMED to avoid a warning"; + private static final String WARNING_LINE4 = + "WARNING: Mutating final fields will be blocked in a future release unless final field mutation is enabled"; + + // warning line 2 depends on the method + private static final String WARNING_MUTATED = + " has been mutated reflectively by class " + HELPER + " in unnamed module"; + private static final String WARNING_UNREFLECTED = + " has been unreflected for mutation by class " + HELPER + " in unnamed module"; + + /** + * Test that a warning is printed by default. + */ + @Test + void testDefault() throws Exception { + test("testFieldSetInt") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + + test("testUnreflectSetter") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_UNREFLECTED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + } + + /** + * Test allow mutation of finals. + */ + @Test + void testAllow() throws Exception { + test("testFieldSetInt", "--illegal-final-field-mutation=allow") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + + test("testFieldSetInt", "--enable-final-field-mutation=ALL-UNNAMED") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + + // allow ALL-UNNAMED, deny by default + test("testFieldSetInt", "--enable-final-field-mutation=ALL-UNNAMED", "--illegal-final-field-mutation=deny") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + + test("testUnreflectSetter", "--illegal-final-field-mutation=allow") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0); + + test("testUnreflectSetter", "--enable-final-field-mutation=ALL-UNNAMED") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0); + + // allow ALL-UNNAMED, deny by default + test("testUnreflectSetter", "--enable-final-field-mutation=ALL-UNNAMED", "--illegal-final-field-mutation=deny") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0); + } + + /** + * Test warn on first mutation or unreflect of a final field. + */ + @Test + void testWarn() throws Exception { + test("testFieldSetInt", "--illegal-final-field-mutation=warn") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + + test("testUnreflectSetter", "--illegal-final-field-mutation=warn") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_UNREFLECTED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + + // should be one warning only, for Field.set + var output = test("testFieldSetInt+testUnreflectSetter", "--illegal-final-field-mutation=warn") + .shouldContain(WARNING_MUTATED) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0) + .getOutput(); + assertEquals(1, countStrings(output, WARNING_LINE1)); + assertEquals(1, countStrings(output, WARNING_LINE3)); + assertEquals(1, countStrings(output, WARNING_LINE4)); + + // should be one warning only, for Lookup.unreflectSetter + output = test("testUnreflectSetter+testFieldSetInt", "--illegal-final-field-mutation=warn") + .shouldNotContain(WARNING_MUTATED) + .shouldContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0) + .getOutput(); + assertEquals(1, countStrings(output, WARNING_LINE1)); + assertEquals(1, countStrings(output, WARNING_LINE3)); + assertEquals(1, countStrings(output, WARNING_LINE4)); + } + + /** + * Test debug mode. + */ + @Test + void testDebug() throws Exception { + test("testFieldSetInt+testUnreflectSetter", "--illegal-final-field-mutation=debug") + .shouldContain("Final field value in class " + HELPER) + .shouldContain(WARNING_MUTATED) + .shouldContain("java.lang.reflect.Field.setInt") + .shouldContain(WARNING_UNREFLECTED) + .shouldContain("java.lang.invoke.MethodHandles$Lookup.unreflectSetter") + .shouldHaveExitValue(0); + + test("testUnreflectSetter+testFieldSetInt", "--illegal-final-field-mutation=debug") + .shouldContain("Final field value in class " + HELPER) + .shouldContain(WARNING_UNREFLECTED) + .shouldContain("java.lang.invoke.MethodHandles$Lookup.unreflectSetter") + .shouldContain(WARNING_MUTATED) + .shouldContain("java.lang.reflect.Field.setInt") + .shouldHaveExitValue(0); + } + + /** + * Test deny mutation of finals. + */ + @Test + void testDeny() throws Exception { + test("testFieldSetInt", "--illegal-final-field-mutation=deny") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldContain("java.lang.IllegalAccessException") + .shouldNotHaveExitValue(0); + + test("testUnreflectSetter", "--illegal-final-field-mutation=deny") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldContain("java.lang.IllegalAccessException") + .shouldNotHaveExitValue(0); + } + + /** + * Test last usage of --illegal-final-field-mutation "wins". + */ + @Test + void testLastOneWins() throws Exception { + test("testFieldSetInt", "--illegal-final-field-mutation=allow", "--illegal-final-field-mutation=deny") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldContain("java.lang.IllegalAccessException") + .shouldNotHaveExitValue(0); + + test("testFieldSetInt", "--illegal-final-field-mutation=deny", "--illegal-final-field-mutation=warn") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + } + + /** + * Test --illegal-final-field-mutation with bad values. + */ + @ParameterizedTest + @ValueSource(strings = { "", "bad" }) + void testInvalidValues(String value) throws Exception { + test("testFieldSetInt", "--illegal-final-field-mutation=" + value) + .shouldContain("Value specified to --illegal-final-field-mutation not recognized") + .shouldNotHaveExitValue(0); + } + + /** + * Test setting the internal system properties (that correspond to the command line + * options) on the commannd line. They should be ignored. + */ + @Test + void testSetPropertyOnCommandLine() throws Exception { + // --enable-final-field-mutation=ALL-UNNAMED + test("testFieldSetInt", "-Djdk.module.enable.final.field.mutation.0=ALL-UNNAMED") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + + // --illegal-final-field-mutation=allow + test("testFieldSetInt", "-Djdk.module.illegal.final.field.mutation=allow") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + } + + /** + * Test setting the internal system properties (that correspond to the command line + * options) at runtime. They should be ignored. + */ + @Test + void testSetPropertyAtRuntime() throws Exception { + // --enable-final-field-mutation=ALL-UNNAMED + test("setPropertyIllegalFinalFieldMutationAllow+testFieldSetInt") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + + // --illegal-final-field-mutation=allow + test("setPropertyEnableFinalFieldMutationAllUnnamed+testFieldSetInt") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldContain(WARNING_LINE3) + .shouldContain(WARNING_LINE4) + .shouldHaveExitValue(0); + } + + /** + * Launch helper with the given arguments and VM options. + */ + private OutputAnalyzer test(String action, String... vmopts) throws Exception { + Stream s1 = Stream.of(vmopts); + Stream s2 = Stream.of(HELPER, action); + String[] opts = Stream.concat(s1, s2).toArray(String[]::new); + var outputAnalyzer = ProcessTools + .executeTestJava(opts) + .outputTo(System.err) + .errorTo(System.err); + return outputAnalyzer; + } + + /** + * Counts the number of substrings in the given input string. + */ + private int countStrings(String input, String substring) { + return input.split(Pattern.quote(substring)).length - 1; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTestHelper.java b/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTestHelper.java new file mode 100644 index 00000000000..cbda9987c5d --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/cli/CommandLineTestHelper.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, 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.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.invoke.MethodHandles; + +public class CommandLineTestHelper { + + /** + * The argument is a list of names of no-arg static methods in this class to invoke. + * The names are separated with a '+'. + */ + public static void main(String[] args) throws Exception { + String[] methodNames = args.length > 0 ? args[0].split("\\+") : new String[0]; + for (String methodName : methodNames) { + Method m = CommandLineTestHelper.class.getDeclaredMethod(methodName); + m.invoke(null); + } + } + + static void testFieldSetInt() throws Exception { + class C { + final int value; + C(int value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + f.setAccessible(true); + var obj = new C(100); + f.setInt(obj, 200); + if (obj.value != 200) { + throw new RuntimeException("Unexpected value: " + obj.value); + } + } + + static void testUnreflectSetter() throws Throwable { + class C { + final int value; + C(int value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + f.setAccessible(true); + var obj = new C(100); + MethodHandles.lookup().unreflectSetter(f).invoke(obj, 200); + if (obj.value != 200) { + throw new RuntimeException("Unexpected value: " + obj.value); + } + } + + /** + * Set the internal system property that corresponds to the first usage of + * --enable-final-field-mutation. + */ + static void setPropertyEnableFinalFieldMutationAllUnnamed() { + System.setProperty("jdk.module.enable.final.field.mutation.0", "ALL-UNNAMED"); + } + + /** + * Set the internal system property that corresponds to --illegal-final-field-mutation. + */ + static void setPropertyIllegalFinalFieldMutationAllow() { + System.setProperty("jdk.module.illegal.final.field.mutation", "allow"); + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTest.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTest.java new file mode 100644 index 00000000000..a9bdae67cfd --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTest.java @@ -0,0 +1,227 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test the executable JAR file attribute Enable-Final-Field-Mutation + * @library /test/lib + * @build m/* + * @build ExecutableJarTestHelper jdk.test.lib.util.JarUtils + * @run junit ExecutableJarTest + */ + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.stream.Stream; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; +import jdk.test.lib.util.JarUtils; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import static org.junit.jupiter.api.Assertions.*; + +class ExecutableJarTest { + + // helper class name + private static final String HELPER = "ExecutableJarTestHelper"; + + // warning output + private static final String WARNING_LINE1 = + "WARNING: Final field value in class " + HELPER; + + // warning line 2 depends on the method + private static final String WARNING_MUTATED = + " has been mutated reflectively by class " + HELPER + " in unnamed module"; + private static final String WARNING_UNREFLECTED = + " has been unreflected for mutation by class " + HELPER + " in unnamed module"; + + /** + * Test executable JAR with code that uses Field.set to mutate a final field. + * A warning should be printed. + */ + @Test + void testFieldSetExpectingWarning() throws Exception { + String jarFile = createExecutableJar(Map.of()); + testExecutableJar(jarFile, "testFieldSetInt") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with code that uses Lookup.unreflectSetter to get MH to a + * final field. A warning should be printed. + */ + @Test + void testUnreflectExpectingWarning() throws Exception { + String jarFile = createExecutableJar(Map.of()); + testExecutableJar(jarFile, "testUnreflectSetter") + .shouldContain(WARNING_LINE1) + .shouldContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with Enable-Final-Field-Mutation attribute and code that uses + * Field.set to mutate a final field. No warning should be printed. + */ + @Test + void testFieldSetExpectingAllow() throws Exception { + String jarFile = createExecutableJar(Map.of("Enable-Final-Field-Mutation", "ALL-UNNAMED")); + testExecutableJar(jarFile, "testFieldSetInt") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_MUTATED) + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with Enable-Final-Field-Mutation attribute and code that uses + * Lookup.unreflectSetter to get MH to a final field. No warning should be printed. + */ + @Test + void testUnreflectExpectingAllow() throws Exception { + String jarFile = createExecutableJar(Map.of("Enable-Final-Field-Mutation", "ALL-UNNAMED")); + testExecutableJar(jarFile, "testUnreflectSetter") + .shouldNotContain(WARNING_LINE1) + .shouldNotContain(WARNING_UNREFLECTED) + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with Enable-Final-Field-Mutation attribute and code that uses + * Field.set to mutate a final field of class in a named module. The package is opened + * with --add-open. + */ + @Test + void testFieldSetWithAddOpens1() throws Exception { + String jarFile = createExecutableJar(Map.of( + "Enable-Final-Field-Mutation", "ALL-UNNAMED")); + testExecutableJar(jarFile, "testFieldInNamedModule", + "--illegal-final-field-mutation=deny", + "--module-path", modulePath(), + "--add-modules", "m", + "--add-opens", "m/p=ALL-UNNAMED") + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with Enable-Final-Field-Mutation attribute and code that uses + * Field.set to mutate a final field of class in a named module. The package is opened + * with with the Add-Opens attribute. + */ + @Test + void testFieldSetWithAddOpens2() throws Exception { + String jarFile = createExecutableJar(Map.of( + "Enable-Final-Field-Mutation", "ALL-UNNAMED", + "Add-Opens", "m/p")); + testExecutableJar(jarFile, "testFieldInNamedModule", + "--illegal-final-field-mutation=deny", + "--module-path", modulePath(), + "--add-modules", "m") + .shouldHaveExitValue(0); + } + + /** + * Test executable JAR with Enable-Final-Field-Mutation with that a value that is not + * "ALL-UNNAMED". + */ + @ParameterizedTest + @ValueSource(strings = {"java.base", "BadValue", " ", ""}) + void testFinalFieldMutationBadValue(String value) throws Exception { + String jarFile = createExecutableJar(Map.of("Enable-Final-Field-Mutation", value)); + testExecutableJar(jarFile, "testFieldSetInt") + .shouldContain("Error: illegal value \"" + value + "\" for Enable-Final-Field-Mutation" + + " manifest attribute. Only ALL-UNNAMED is allowed") + .shouldNotHaveExitValue(0); + } + + /** + * Launch ExecutableJarTestHelper with the given arguments and VM options. + */ + private OutputAnalyzer test(String action, String... vmopts) throws Exception { + Stream s1 = Stream.of(vmopts); + Stream s2 = Stream.of("ExecutableJarTestHelper", action); + String[] opts = Stream.concat(s1, s2).toArray(String[]::new); + var outputAnalyzer = ProcessTools + .executeTestJava(opts) + .outputTo(System.err) + .errorTo(System.err); + return outputAnalyzer; + } + + /** + * Launch ExecutableJarTestHelper with the given arguments and VM options. + */ + private OutputAnalyzer testExecutableJar(String jarFile, + String action, + String... vmopts) throws Exception { + Stream s1 = Stream.of(vmopts); + Stream s2 = Stream.of("-jar", jarFile, action); + String[] opts = Stream.concat(s1, s2).toArray(String[]::new); + var outputAnalyzer = ProcessTools + .executeTestJava(opts) + .outputTo(System.err) + .errorTo(System.err); + return outputAnalyzer; + } + + /** + * Creates executable JAR named helper.jar with ExecutableJarTestHelper* classes. + */ + private String createExecutableJar(Map map) throws Exception { + Path jarFile = Path.of("helper.jar"); + var man = new Manifest(); + Attributes attrs = man.getMainAttributes(); + attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attrs.put(Attributes.Name.MAIN_CLASS, "ExecutableJarTestHelper"); + map.entrySet().forEach(e -> { + var name = new Attributes.Name(e.getKey()); + attrs.put(name, e.getValue()); + }); + Path dir = Path.of(System.getProperty("test.classes")); + try (Stream stream = Files.list(dir)) { + Path[] files = Files.list(dir).filter(p -> { + String fn = p.getFileName().toString(); + return fn.startsWith("ExecutableJarTestHelper") && fn.endsWith(".class"); + }) + .toArray(Path[]::new); + JarUtils.createJarFile(jarFile, man, dir, files); + } + return jarFile.toString(); + } + + /** + * Return the module path for the modules used by this test. + */ + private String modulePath() { + return Path.of(System.getProperty("test.classes"), "modules").toString(); + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTestHelper.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTestHelper.java new file mode 100644 index 00000000000..e0f7c60f637 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/ExecutableJarTestHelper.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025, 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.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.invoke.MethodHandles; + +public class ExecutableJarTestHelper { + + /** + * The argument is a list of names of no-arg static methods in this class to invoke. + * The names are separated with a '+'. + */ + public static void main(String[] args) throws Exception { + String[] methodNames = args.length > 0 ? args[0].split("\\+") : new String[0]; + for (String methodName : methodNames) { + Method m = ExecutableJarTestHelper.class.getDeclaredMethod(methodName); + m.invoke(null); + } + } + + /** + * Uses Field.set to mutate a final field. + */ + static void testFieldSetInt() throws Exception { + class C { + final int value; + C(int value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + f.setAccessible(true); + var obj = new C(100); + f.setInt(obj, 200); + if (obj.value != 200) { + throw new RuntimeException("Unexpected value: " + obj.value); + } + } + + /** + * Uses Lookup.unreflectSetter to get a method handle to set a final field. + */ + static void testUnreflectSetter() throws Throwable { + class C { + final int value; + C(int value) { + this.value = value; + } + } + Field f = C.class.getDeclaredField("value"); + f.setAccessible(true); + var obj = new C(100); + MethodHandles.lookup().unreflectSetter(f).invoke(obj, 200); + if (obj.value != 200) { + throw new RuntimeException("Unexpected value: " + obj.value); + } + } + + /** + * Uses Field.set to mutate a final field of a class in a named module. + */ + static void testFieldInNamedModule() throws Exception { + Class c = Class.forName("p.C"); + if (!c.getModule().isNamed()) { + throw new RuntimeException(c + " is not in a named module"); + } + Object obj = c.getDeclaredConstructor(int.class).newInstance(100); + Field f = c.getDeclaredField("value"); + f.setAccessible(true); + f.setInt(obj, 200); + int newValue = f.getInt(obj); + if (newValue != 200) { + throw new RuntimeException("Unexpected value: " + newValue); + } + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/module-info.java new file mode 100644 index 00000000000..197a2101894 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025, 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. + */ + +/** + * Module containing a class with a final field used by ExecutableJarTest. + */ +module m { + exports p; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/p/C.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/p/C.java new file mode 100644 index 00000000000..e128330ab5e --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jar/m/p/C.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 p; + +/** + * A class with a final field used by ExecutableJarTest. + */ +public class C { + private final int value; + + public C(int value) { + this.value = value; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutator.java new file mode 100644 index 00000000000..9af4370c652 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutator.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2025, 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.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.concurrent.CountDownLatch; + +/** + * Launched by JNIAttachMutatorTest to test a JNI attached thread attempting to mutate + * a final field. + */ + +public class JNIAttachMutator { + private static final CountDownLatch finished = new CountDownLatch(1); + private static volatile Object obj; + private static volatile Throwable exc; + + // public class, public final field + public static class C1 { + public final int value; + C1(int value) { + this.value = value; + } + } + + // public class, non-public final field + public static class C2 { + final int value; + C2(int value) { + this.value = value; + } + } + + // non-public class, public final field + static class C3 { + public final int value; + C3(int value) { + this.value = value; + } + } + + /** + * Usage: java JNIAttachMutator + */ + public static void main(String[] args) throws Exception { + String cn = args[0]; + boolean expectIAE = Boolean.parseBoolean(args[1]); + + Class clazz = Class.forName(args[0]); + Constructor ctor = clazz.getDeclaredConstructor(int.class); + ctor.setAccessible(true); + obj = ctor.newInstance(100); + + // start native thread + startThread(); + + // wait for native thread to finish + finished.await(); + + if (expectIAE) { + if (exc == null) { + // IAE expected + throw new RuntimeException("IllegalAccessException not thrown"); + } else if (!(exc instanceof IllegalAccessException)) { + // unexpected exception + throw new RuntimeException(exc); + } + } else if (exc != null) { + // no exception expected + throw new RuntimeException(exc); + } + } + + /** + * Invoked by JNI attached thread to get object. + */ + static Object getObject() { + return obj; + } + + /** + * Invoked by JNI attached thread to get Field object with accessible enabled. + */ + static Field getField() throws NoSuchFieldException { + Field f = obj.getClass().getDeclaredField("value"); + f.setAccessible(true); + return f; + } + + /** + * Invoked by JNI attached thread when finished. + */ + static void finish(Throwable ex) { + exc = ex; + finished.countDown(); + } + + private static native void startThread(); + + static { + System.loadLibrary("JNIAttachMutator"); + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutatorTest.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutatorTest.java new file mode 100644 index 00000000000..19f5e21085f --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/JNIAttachMutatorTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test native thread attaching to the VM with JNI AttachCurrentThread and directly + * invoking Field.set to set a final field + * @library /test/lib + * @build m/* + * @compile JNIAttachMutator.java + * @run junit JNIAttachMutatorTest + */ + +import java.nio.file.Path; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import jdk.test.lib.process.ProcessTools; +import jdk.test.lib.process.OutputAnalyzer; + +class JNIAttachMutatorTest { + private static String testClasses; + private static String modulesDir; + private static String javaLibraryPath; + + @BeforeAll + static void setup() { + testClasses = System.getProperty("test.classes"); + modulesDir = Path.of(testClasses, "modules").toString(); + javaLibraryPath = System.getProperty("java.library.path"); + } + + /** + * Final final mutation allowed. All final fields are public, in public classes, + * and in packages exported to all modules. + */ + @ParameterizedTest + @ValueSource(strings = { + "JNIAttachMutator$C1", // unnamed module + "p.C1", // named module + }) + void testAllowed(String cn) throws Exception { + test(cn, false); + } + + /** + * Final final mutation not allowed. + */ + @ParameterizedTest + @ValueSource(strings = { + // unnamed module + "JNIAttachMutator$C2", // public class, non-public final field + "JNIAttachMutator$C3", // non-public class, public final field + + // named module + "p.C2", // public class, non-public final field, exported package + "p.C3", // non-public class, public final field, exported package + "q.C" // public class, public final field, package not exported + }) + void testDenied(String cn) throws Exception { + test(cn, true); + } + + /** + * public final field, public class, package exported to some modules. + */ + @Test + void testQualifiedExports() throws Exception { + test("q.C", true, "--add-exports", "m/q=ALL-UNNAMED"); + } + + /** + * Launches JNIAttachMutator to test a JNI attached thread mutating a final field. + * @param className the class with the field final + * @param expectIAE if IllegalAccessException is expected + * @param extraOps additional VM options + */ + private void test(String className, boolean expectIAE, String... extraOps) throws Exception { + Stream s1 = Stream.of(extraOps); + Stream s2 = Stream.of( + "-cp", testClasses, + "-Djava.library.path=" + javaLibraryPath, + "--module-path", modulesDir, + "--add-modules", "m", + "--add-opens", "m/p=ALL-UNNAMED", // allow setAccessible + "--add-opens", "m/q=ALL-UNNAMED", + "--enable-native-access=ALL-UNNAMED", + "--enable-final-field-mutation=ALL-UNNAMED", + "--illegal-final-field-mutation=deny", + "JNIAttachMutator", + className, + expectIAE ? "true" : "false"); + String[] opts = Stream.concat(s1, s2).toArray(String[]::new); + OutputAnalyzer outputAnalyzer = ProcessTools + .executeTestJava(opts) + .outputTo(System.out) + .errorTo(System.out); + outputAnalyzer.shouldHaveExitValue(0); + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/libJNIAttachMutator.c b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/libJNIAttachMutator.c new file mode 100644 index 00000000000..077b9f5d625 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/libJNIAttachMutator.c @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2025, 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. + */ +#include +#ifdef _WIN32 +#include +#else +#include +#endif +#include "jni.h" + +#define STACK_SIZE 0x100000 + +static JavaVM *vm; + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void* reserved) { + vm = jvm; + return JNI_VERSION_1_8; +} + +/** + * Invokes JNIAttachMutator.getObject() + */ +jobject getObject(JNIEnv* env) { + jclass clazz = (*env)->FindClass(env, "JNIAttachMutator"); + if (clazz == NULL) { + fprintf(stderr, "FindClass failed\n"); + return NULL; + } + jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "getObject", "()Ljava/lang/Object;"); + if (mid == NULL) { + fprintf(stderr, "GetMethodID for getObject failed\n"); + return NULL; + } + jobject obj = (*env)->CallStaticObjectMethod(env, clazz, mid); + if (obj == NULL) { + fprintf(stderr, "CallObjectMethod to getObject failed\n"); + return NULL; + } + return obj; +} + +/** + * Invokes JNIAttachMutator.getField() + */ +jobject getField(JNIEnv* env) { + jclass clazz = (*env)->FindClass(env, "JNIAttachMutator"); + if (clazz == NULL) { + fprintf(stderr, "FindClass failed\n"); + return NULL; + } + jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "getField", "()Ljava/lang/reflect/Field;"); + if (mid == NULL) { + fprintf(stderr, "GetStaticMethodID for getField failed\n"); + return NULL; + } + jobject obj = (*env)->CallStaticObjectMethod(env, clazz, mid); + if (obj == NULL) { + fprintf(stderr, "CallObjectMethod to getField failed\n"); + return NULL; + } + return obj; +} + +/** + * Invokes Field.setInt + */ +jboolean setInt(JNIEnv* env, jobject obj, jobject fieldObj, jint newValue) { + jclass fieldClass = (*env)->GetObjectClass(env, fieldObj); + jmethodID mid = (*env)->GetMethodID(env, fieldClass, "setInt", "(Ljava/lang/Object;I)V"); + if (mid == NULL) { + fprintf(stderr, "GetMethodID for Field.setInt failed\n"); + return JNI_FALSE; + } + (*env)->CallObjectMethod(env, fieldObj, mid, obj, newValue); + return JNI_TRUE; +} + +/** + * Invokes JNIAttachMutator.finish + */ +void finish(JNIEnv* env, jthrowable ex) { + jclass clazz = (*env)->FindClass(env, "JNIAttachMutator"); + if (clazz == NULL) { + fprintf(stderr, "FindClass failed\n"); + return; + } + + // invoke finish + jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "finish", "(Ljava/lang/Throwable;)V"); + if (mid == NULL) { + fprintf(stderr, "GetStaticMethodID failed\n"); + return; + } + (*env)->CallStaticVoidMethod(env, clazz, mid, ex); + if ((*env)->ExceptionOccurred(env)) { + fprintf(stderr, "CallStaticVoidMethod failed\n"); + } +} + +/** + * Attach the current thread with JNI AttachCurrentThread. + */ +void* thread_main(void* arg) { + JNIEnv *env; + jint res; + jthrowable ex; + + res = (*vm)->AttachCurrentThread(vm, (void **) &env, NULL); + if (res != JNI_OK) { + fprintf(stderr, "AttachCurrentThread failed: %d\n", res); + return NULL; + } + + // invoke JNIAttachMutator.getObject to get the object to test + jobject obj = getObject(env); + if (obj == NULL) { + goto done; + } + + // invoke JNIAttachMutator.getField to get the Field object with access enabled + jobject fieldObj = getField(env); + if (fieldObj == NULL) { + goto done; + } + + // invoke Field.setInt to attempt to set the value to 200 + if (!setInt(env, obj, fieldObj, 200)) { + goto done; + } + + done: + + ex = (*env)->ExceptionOccurred(env); + if (ex != NULL) { + (*env)->ExceptionDescribe(env); + (*env)->ExceptionClear(env); + } + finish(env, ex); + + res = (*vm)->DetachCurrentThread(vm); + if (res != JNI_OK) { + fprintf(stderr, "DetachCurrentThread failed: %d\n", res); + } + + return NULL; +} + +#ifdef _WIN32 +static DWORD WINAPI win32_thread_main(void* p) { + thread_main(p); + return 0; +} +#endif + +JNIEXPORT void JNICALL Java_JNIAttachMutator_startThread(JNIEnv *env, jclass clazz) { +#ifdef _WIN32 + HANDLE handle = CreateThread(NULL, STACK_SIZE, win32_thread_main, NULL, 0, NULL); + if (handle == NULL) { + fprintf(stderr, "CreateThread failed: %d\n", GetLastError()); + } +#else + pthread_t tid; + pthread_attr_t attr; + + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, STACK_SIZE); + int res = pthread_create(&tid, &attr, thread_main, NULL); + if (res != 0) { + fprintf(stderr, "pthread_create failed: %d\n", res); + } +#endif +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/module-info.java new file mode 100644 index 00000000000..bb113e926df --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/module-info.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025, 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. + */ + +/** + * Module containing classes with final fields. + */ +module m { + exports p; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C1.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C1.java new file mode 100644 index 00000000000..8d931f2eb47 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C1.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 p; + +/** + * public class, public final field. + */ +public class C1 { + public final int value; + + C1(int value) { + this.value = value; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C2.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C2.java new file mode 100644 index 00000000000..186d8e82575 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C2.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 p; + +/** + * public class, non-public final field. + */ +public class C2 { + final int value; + + C2(int value) { + this.value = value; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C3.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C3.java new file mode 100644 index 00000000000..56b90490abf --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/p/C3.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 p; + +/** + * non-public class, public final field. + */ +class C3 { + public final int value; + + C3(int value) { + this.value = value; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/q/C.java b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/q/C.java new file mode 100644 index 00000000000..1411547347e --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/jni/m/q/C.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 q; + +/** + * public class, public final field. + */ +public class C { + public final int value; + + public C(int value) { + this.value = value; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/Driver.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/Driver.java new file mode 100644 index 00000000000..f0b87b09304 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/Driver.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2025, 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 8353835 + * @summary Test mutating final fields in module test from code in modules test, m1, m2 and m3 + * @build test/* m1/* m2/* m3/* + * @run junit/othervm --illegal-final-field-mutation=deny --enable-final-field-mutation=test,m1,m2,m3 + * test/test.TestMain + * @run junit/othervm --illegal-final-field-mutation=deny --enable-final-field-mutation=test,m1,m2,m3 + * --add-exports test/test.fieldholders=m3 test/test.TestMain + * @run junit/othervm --illegal-final-field-mutation=deny --enable-final-field-mutation=test,m1,m2,m3 + * --add-opens test/test.fieldholders=m2 test/test.TestMain + */ diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/module-info.java new file mode 100644 index 00000000000..c7b4fbdc424 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/module-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, 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. + */ +@SuppressWarnings("module") +module m1 { + requires test; + provides test.spi.Mutator with p1.M1Mutator; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/p1/M1Mutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/p1/M1Mutator.java new file mode 100644 index 00000000000..4fed0c97692 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m1/p1/M1Mutator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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 p1; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +public class M1Mutator implements test.spi.Mutator { + + @Override + public void set(Field f, Object obj, Object value) throws IllegalAccessException { + f.set(obj, value); + } + + @Override + public void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException { + f.setBoolean(obj, value); + } + + @Override + public void setByte(Field f, Object obj, byte value) throws IllegalAccessException { + f.setByte(obj, value); + } + + @Override + public void setChar(Field f, Object obj, char value) throws IllegalAccessException { + f.setChar(obj, value); + } + + @Override + public void setShort(Field f, Object obj, short value) throws IllegalAccessException { + f.setShort(obj, value); + } + + @Override + public void setInt(Field f, Object obj, int value) throws IllegalAccessException { + f.setInt(obj, value); + } + + @Override + public void setLong(Field f, Object obj, long value) throws IllegalAccessException { + f.setLong(obj, value); + } + + @Override + public void setFloat(Field f, Object obj, float value) throws IllegalAccessException { + f.setFloat(obj, value); + } + + @Override + public void setDouble(Field f, Object obj, double value) throws IllegalAccessException { + f.setDouble(obj, value); + } + + @Override + public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { + return MethodHandles.lookup().unreflectSetter(f); + } + +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/module-info.java new file mode 100644 index 00000000000..3c48cb6dfc7 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/module-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, 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. + */ +@SuppressWarnings("module") +module m2 { + requires test; + provides test.spi.Mutator with p2.M2Mutator; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/p2/M2Mutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/p2/M2Mutator.java new file mode 100644 index 00000000000..a3f2252e20c --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m2/p2/M2Mutator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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 p2; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +public class M2Mutator implements test.spi.Mutator { + + @Override + public void set(Field f, Object obj, Object value) throws IllegalAccessException { + f.set(obj, value); + } + + @Override + public void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException { + f.setBoolean(obj, value); + } + + @Override + public void setByte(Field f, Object obj, byte value) throws IllegalAccessException { + f.setByte(obj, value); + } + + @Override + public void setChar(Field f, Object obj, char value) throws IllegalAccessException { + f.setChar(obj, value); + } + + @Override + public void setShort(Field f, Object obj, short value) throws IllegalAccessException { + f.setShort(obj, value); + } + + @Override + public void setInt(Field f, Object obj, int value) throws IllegalAccessException { + f.setInt(obj, value); + } + + @Override + public void setLong(Field f, Object obj, long value) throws IllegalAccessException { + f.setLong(obj, value); + } + + @Override + public void setFloat(Field f, Object obj, float value) throws IllegalAccessException { + f.setFloat(obj, value); + } + + @Override + public void setDouble(Field f, Object obj, double value) throws IllegalAccessException { + f.setDouble(obj, value); + } + + @Override + public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { + return MethodHandles.lookup().unreflectSetter(f); + } + +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/module-info.java new file mode 100644 index 00000000000..8e378882a42 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/module-info.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2025, 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. + */ +@SuppressWarnings("module") +module m3 { + requires test; + provides test.spi.Mutator with p3.M3Mutator; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/p3/M3Mutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/p3/M3Mutator.java new file mode 100644 index 00000000000..c432b8d8f07 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/m3/p3/M3Mutator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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 p3; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +public class M3Mutator implements test.spi.Mutator { + + @Override + public void set(Field f, Object obj, Object value) throws IllegalAccessException { + f.set(obj, value); + } + + @Override + public void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException { + f.setBoolean(obj, value); + } + + @Override + public void setByte(Field f, Object obj, byte value) throws IllegalAccessException { + f.setByte(obj, value); + } + + @Override + public void setChar(Field f, Object obj, char value) throws IllegalAccessException { + f.setChar(obj, value); + } + + @Override + public void setShort(Field f, Object obj, short value) throws IllegalAccessException { + f.setShort(obj, value); + } + + @Override + public void setInt(Field f, Object obj, int value) throws IllegalAccessException { + f.setInt(obj, value); + } + + @Override + public void setLong(Field f, Object obj, long value) throws IllegalAccessException { + f.setLong(obj, value); + } + + @Override + public void setFloat(Field f, Object obj, float value) throws IllegalAccessException { + f.setFloat(obj, value); + } + + @Override + public void setDouble(Field f, Object obj, double value) throws IllegalAccessException { + f.setDouble(obj, value); + } + + @Override + public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { + return MethodHandles.lookup().unreflectSetter(f); + } + +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/module-info.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/module-info.java new file mode 100644 index 00000000000..9e41df42f20 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/module-info.java @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2025, 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. + */ +module test { + requires org.junit.platform.console.standalone; + opens test to org.junit.platform.console.standalone; + + opens test.fieldholders to m1; + exports test.fieldholders to m2; + + exports test.spi; + uses test.spi.Mutator; + provides test.spi.Mutator with test.internal.TestMutator; +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/TestMain.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/TestMain.java new file mode 100644 index 00000000000..17ed2d78e0a --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/TestMain.java @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2025, 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 test; + +import java.lang.reflect.Field; +import java.util.ServiceLoader; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import test.spi.Mutator; +import test.fieldholders.PublicFields; +import test.fieldholders.PrivateFields; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test mutating final fields from different modules. + */ +class TestMain { + static Map> exportedToMutators; + static Map> openToMutators; + + // test module and the name of package with the fieldholder classes + static Module testModule; + static String fieldHoldersPackage; + + @BeforeAll + static void setup() throws Exception { + testModule = TestMain.class.getModule(); + fieldHoldersPackage = PublicFields.class.getPackageName(); + + List allMutators = ServiceLoader.load(Mutator.class) + .stream() + .map(ServiceLoader.Provider::get) + .toList(); + + // mutators that test.fieldholders is exported to + exportedToMutators = allMutators.stream() + .collect(Collectors.partitioningBy(m -> testModule.isExported(fieldHoldersPackage, + m.getClass().getModule()), + Collectors.toList())); + + // mutators that test.fieldholders is open to + openToMutators = allMutators.stream() + .collect(Collectors.partitioningBy(m -> testModule.isOpen(fieldHoldersPackage, + m.getClass().getModule()), + Collectors.toList())); + + + // exported to at least test, m1 and m2 + assertTrue(exportedToMutators.get(Boolean.TRUE).size() >= 3); + + // open to at least test and m1 + assertTrue(openToMutators.get(Boolean.TRUE).size() >= 2); + } + + /** + * Returns a stream of mutators that test.fieldholders is exported to. + */ + static Stream exportedToMutators() { + return exportedToMutators.get(Boolean.TRUE).stream(); + } + + /** + * Returns a stream of mutators that test.fieldholders is open to. + */ + static Stream openToMutators() { + return openToMutators.get(Boolean.TRUE).stream(); + } + + /** + * Returns a stream of mutators that test.fieldholders is not exported to. + */ + static Stream notExportedToMutators() { + List mutators = exportedToMutators.get(Boolean.FALSE); + if (mutators.isEmpty()) { + // can't return an empty stream at this time + return Stream.of(Mutator.throwing()); + } else { + return mutators.stream(); + } + } + + /** + * Returns a stream of mutators that test.fieldholders is not open to. + */ + static Stream notOpenToMutators() { + List mutators = openToMutators.get(Boolean.FALSE); + if (mutators.isEmpty()) { + // can't return an empty stream at this time + return Stream.of(Mutator.throwing()); + } else { + return mutators.stream(); + } + } + + // public field, public class in package exported to mutator + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.objectField(); + var obj = new PublicFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + assertTrue(obj.objectValue() == oldValue); + + f.setAccessible(true); + mutator.set(f, obj, newValue); + assertTrue(obj.objectValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetBooleanExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.booleanField(); + var obj = new PublicFields(); + boolean oldValue = obj.booleanValue(); + boolean newValue = true; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setBoolean(f, obj, newValue)); + assertTrue(obj.booleanValue() == oldValue); + + f.setAccessible(true); + mutator.setBoolean(f, obj, newValue); + assertTrue(obj.booleanValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetByteExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.byteField(); + var obj = new PublicFields(); + byte oldValue = obj.byteValue(); + byte newValue = 10; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setByte(f, obj, newValue)); + assertTrue(obj.byteValue() == oldValue); + + f.setAccessible(true); + mutator.setByte(f, obj, newValue); + assertTrue(obj.byteValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetCharExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.charField(); + var obj = new PublicFields(); + char oldValue = obj.charValue(); + char newValue = 'Z'; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setChar(f, obj, newValue)); + assertTrue(obj.charValue() == oldValue); + + f.setAccessible(true); + mutator.setChar(f, obj, newValue); + assertTrue(obj.charValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetShortExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.shortField(); + var obj = new PublicFields(); + short oldValue = obj.shortValue(); + short newValue = 99; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setShort(f, obj, newValue)); + assertTrue(obj.shortValue() == oldValue); + + f.setAccessible(true); + mutator.setShort(f, obj, newValue); + assertTrue(obj.shortValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetIntExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.intField(); + var obj = new PublicFields(); + int oldValue = obj.intValue(); + int newValue = 999; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setInt(f, obj, newValue)); + assertTrue(obj.intValue() == oldValue); + + f.setAccessible(true); + mutator.setInt(f, obj, newValue); + assertTrue(obj.intValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetLongExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.longField(); + var obj = new PublicFields(); + long oldValue = obj.longValue(); + long newValue = 9999; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setLong(f, obj, newValue)); + assertTrue(obj.longValue() == oldValue); + + f.setAccessible(true); + mutator.setLong(f, obj, newValue); + assertTrue(obj.longValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetFloatExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.floatField(); + var obj = new PublicFields(); + float oldValue = obj.floatValue(); + float newValue = 9.9f; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setFloat(f, obj, newValue)); + assertTrue(obj.floatValue() == oldValue); + + f.setAccessible(true); + mutator.setFloat(f, obj, newValue); + assertTrue(obj.floatValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("exportedToMutators") + void testFieldSetDoublExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.doubleField(); + var obj = new PublicFields(); + double oldValue = obj.doubleValue(); + double newValue = 99.9d; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setDouble(f, obj, newValue)); + assertTrue(obj.doubleValue() == oldValue); + + f.setAccessible(true); + mutator.setDouble(f, obj, newValue); + assertTrue(obj.doubleValue() == newValue); + } + + @ParameterizedTest + @MethodSource("exportedToMutators") + void testUnreflectSetterExportedPackage(Mutator mutator) throws Throwable { + Field f = PublicFields.objectField(); + var obj = new PublicFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + + f.setAccessible(true); + mutator.unreflectSetter(f).invokeExact(obj, newValue); + assertTrue(obj.objectValue() == newValue); + } + + // private field, class in package opened to mutator + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.objectField(); + var obj = new PrivateFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + assertTrue(obj.objectValue() == oldValue); + + f.setAccessible(true); + mutator.set(f, obj, newValue); + assertTrue(obj.objectValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetBooleanOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.booleanField(); + var obj = new PrivateFields(); + boolean oldValue = obj.booleanValue(); + boolean newValue = true; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setBoolean(f, obj, newValue)); + assertTrue(obj.booleanValue() == oldValue); + + f.setAccessible(true); + mutator.setBoolean(f, obj, newValue); + assertTrue(obj.booleanValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetByteOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.byteField(); + var obj = new PrivateFields(); + byte oldValue = obj.byteValue(); + byte newValue = 10; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setByte(f, obj, newValue)); + assertTrue(obj.byteValue() == oldValue); + + f.setAccessible(true); + mutator.setByte(f, obj, newValue); + assertTrue(obj.byteValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetCharOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.charField(); + var obj = new PrivateFields(); + char oldValue = obj.charValue(); + char newValue = 'Z'; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setChar(f, obj, newValue)); + assertTrue(obj.charValue() == oldValue); + + f.setAccessible(true); + mutator.setChar(f, obj, newValue); + assertTrue(obj.charValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetShortOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.shortField(); + var obj = new PrivateFields(); + short oldValue = obj.shortValue(); + short newValue = 'Z'; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setShort(f, obj, newValue)); + assertTrue(obj.shortValue() == oldValue); + + f.setAccessible(true); + mutator.setShort(f, obj, newValue); + assertTrue(obj.shortValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetIntOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.intField(); + var obj = new PrivateFields(); + int oldValue = obj.intValue(); + int newValue = 99; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setInt(f, obj, newValue)); + assertTrue(obj.intValue() == oldValue); + + f.setAccessible(true); + mutator.setInt(f, obj, newValue); + assertTrue(obj.intValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetLongOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.longField(); + var obj = new PrivateFields(); + long oldValue = obj.longValue(); + long newValue = 999; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setLong(f, obj, newValue)); + assertTrue(obj.longValue() == oldValue); + + f.setAccessible(true); + mutator.setLong(f, obj, newValue); + assertTrue(obj.longValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetFloatOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.floatField(); + var obj = new PrivateFields(); + float oldValue = obj.floatValue(); + float newValue = 9.9f; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setFloat(f, obj, newValue)); + assertTrue(obj.floatValue() == oldValue); + + f.setAccessible(true); + mutator.setFloat(f, obj, newValue); + assertTrue(obj.floatValue() == newValue); + } + + @ParameterizedTest() + @MethodSource("openToMutators") + void testFieldSetDoubleOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.doubleField(); + var obj = new PrivateFields(); + double oldValue = obj.doubleValue(); + double newValue = 99.9d; + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.setDouble(f, obj, newValue)); + assertTrue(obj.doubleValue() == oldValue); + + f.setAccessible(true); + mutator.setDouble(f, obj, newValue); + assertTrue(obj.doubleValue() == newValue); + } + + @ParameterizedTest + @MethodSource("openToMutators") + void testUnreflectSetterOpenPackage(Mutator mutator) throws Throwable { + Field f = PrivateFields.objectField(); + var obj = new PrivateFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(false); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + + f.setAccessible(true); + mutator.unreflectSetter(f).invokeExact(obj, newValue); + assertTrue(obj.objectValue() == newValue); + } + + // public field, public class in package not exported to mutator + + @ParameterizedTest + @MethodSource("notExportedToMutators") + void testFieldSetNotExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.objectField(); + var obj = new PublicFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(true); + var e1 = assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + Module mutatorModule = mutator.getClass().getModule(); + if (mutatorModule != testModule) { + assertTrue(e1.getMessage().contains("module " + testModule.getName() + + " does not explicitly \"exports\" package " + + f.getDeclaringClass().getPackageName() + + " to module " + mutatorModule.getName())); + } + assertTrue(obj.objectValue() == oldValue); + + // export package to mutator module, should have no effect on set method + testModule.addExports(fieldHoldersPackage, mutator.getClass().getModule()); + var e2 = assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + assertEquals(e1.getMessage(), e2.getMessage()); + assertTrue(obj.objectValue() == oldValue); + + // open package to mutator module, should have no effect on set method + testModule.addOpens(fieldHoldersPackage, mutator.getClass().getModule()); + var e3 = assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + assertEquals(e1.getMessage(), e3.getMessage()); + assertTrue(obj.objectValue() == oldValue); + } + + @ParameterizedTest + @MethodSource("notExportedToMutators") + void testUnreflectSetterNotExportedPackage(Mutator mutator) throws Exception { + Field f = PublicFields.objectField(); + + f.setAccessible(true); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + + // export package to mutator module, should have no effect on unreflectSetter method + testModule.addExports(fieldHoldersPackage, mutator.getClass().getModule()); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + + // open package to mutator module, should have no effect on unreflectSetter method + testModule.addOpens(fieldHoldersPackage, mutator.getClass().getModule()); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + } + + // private field, class in package not opened to mutator + + @ParameterizedTest + @MethodSource("notOpenToMutators") + void testFieldSetNotOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.objectField(); + var obj = new PrivateFields(); + Object oldValue = obj.objectValue(); + Object newValue = new Object(); + + f.setAccessible(true); + var e1 = assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + Module mutatorModule = mutator.getClass().getModule(); + if (mutatorModule != testModule) { + assertTrue(e1.getMessage().contains("module " + testModule.getName() + + " does not explicitly \"opens\" package " + + f.getDeclaringClass().getPackageName() + + " to module " + mutatorModule.getName())); + } + assertTrue(obj.objectValue() == oldValue); + + // open package to mutator module, should have no effect on set method + testModule.addOpens(fieldHoldersPackage, mutator.getClass().getModule()); + var e2 = assertThrows(IllegalAccessException.class, () -> mutator.set(f, obj, newValue)); + assertEquals(e2.getMessage(), e2.getMessage()); + assertTrue(obj.objectValue() == oldValue); + } + + @ParameterizedTest + @MethodSource("notOpenToMutators") + void testUnreflectSetterNotOpenPackage(Mutator mutator) throws Exception { + Field f = PrivateFields.class.getDeclaredField("obj"); + + f.setAccessible(true); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + + // open package to mutator module, should have no effect on unreflectSetter method + testModule.addOpens(fieldHoldersPackage, mutator.getClass().getModule()); + assertThrows(IllegalAccessException.class, () -> mutator.unreflectSetter(f)); + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PrivateFields.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PrivateFields.java new file mode 100644 index 00000000000..2e7ed57a36f --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PrivateFields.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, 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 test.fieldholders; + +import java.lang.reflect.Field; + +/** + * public class with private final fields. + */ +public class PrivateFields { + private final Object obj; + private final boolean z; + private final byte b; + private final char c; + private final short s; + private final int i; + private final long l; + private final float f; + private final double d; + + public PrivateFields() { + obj = new Object(); + z = false; + b = 0; + c = 0; + s = 0; + i = 0; + l = 0; + f = 0.0f; + d = 0.0d; + } + + public static Field objectField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("obj"); + } + + public static Field booleanField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("z"); + } + + public static Field byteField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("b"); + } + + public static Field charField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("c"); + } + + public static Field shortField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("s"); + } + + public static Field intField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("i"); + } + + public static Field longField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("l"); + } + + public static Field floatField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("f"); + } + + public static Field doubleField() throws NoSuchFieldException { + return PrivateFields.class.getDeclaredField("d"); + } + + public Object objectValue() { + return obj; + } + + public boolean booleanValue() { + return z; + } + + public byte byteValue() { + return b; + } + + public char charValue() { + return c; + } + + public short shortValue() { + return s; + } + + public int intValue() { + return i; + } + + public long longValue() { + return l; + } + + public float floatValue() { + return f; + } + + public double doubleValue() { + return d; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PublicFields.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PublicFields.java new file mode 100644 index 00000000000..5a830249753 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/fieldholders/PublicFields.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2025, 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 test.fieldholders; + +import java.lang.reflect.Field; + +/** + * public class with public final fields. + */ +public class PublicFields { + public final Object obj; + public final boolean z; + public final byte b; + public final char c; + public final short s; + public final int i; + public final long l; + public final float f; + public final double d; + + public PublicFields() { + obj = new Object(); + z = false; + b = 0; + c = 0; + s = 0; + i = 0; + l = 0; + f = 0.0f; + d = 0.0d; + } + + public static Field objectField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("obj"); + } + + public static Field booleanField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("z"); + } + + public static Field byteField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("b"); + } + + public static Field charField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("c"); + } + + public static Field shortField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("s"); + } + + public static Field intField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("i"); + } + + public static Field longField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("l"); + } + + public static Field floatField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("f"); + } + + public static Field doubleField() throws NoSuchFieldException { + return PublicFields.class.getDeclaredField("d"); + } + + public Object objectValue() { + return obj; + } + + public boolean booleanValue() { + return z; + } + + public byte byteValue() { + return b; + } + + public char charValue() { + return c; + } + + public short shortValue() { + return s; + } + + public int intValue() { + return i; + } + + public long longValue() { + return l; + } + + public float floatValue() { + return f; + } + + public double doubleValue() { + return d; + } +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/internal/TestMutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/internal/TestMutator.java new file mode 100644 index 00000000000..33f7056026b --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/internal/TestMutator.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2025, 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 test.internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; + +public class TestMutator implements test.spi.Mutator { + + @Override + public void set(Field f, Object obj, Object value) throws IllegalAccessException { + f.set(obj, value); + } + + @Override + public void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException { + f.setBoolean(obj, value); + } + + @Override + public void setByte(Field f, Object obj, byte value) throws IllegalAccessException { + f.setByte(obj, value); + } + + @Override + public void setChar(Field f, Object obj, char value) throws IllegalAccessException { + f.setChar(obj, value); + } + + @Override + public void setShort(Field f, Object obj, short value) throws IllegalAccessException { + f.setShort(obj, value); + } + + @Override + public void setInt(Field f, Object obj, int value) throws IllegalAccessException { + f.setInt(obj, value); + } + + @Override + public void setLong(Field f, Object obj, long value) throws IllegalAccessException { + f.setLong(obj, value); + } + + @Override + public void setFloat(Field f, Object obj, float value) throws IllegalAccessException { + f.setFloat(obj, value); + } + + @Override + public void setDouble(Field f, Object obj, double value) throws IllegalAccessException { + f.setDouble(obj, value); + } + + @Override + public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { + return MethodHandles.lookup().unreflectSetter(f); + } + +} diff --git a/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/spi/Mutator.java b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/spi/Mutator.java new file mode 100644 index 00000000000..467a84e8a55 --- /dev/null +++ b/test/jdk/java/lang/reflect/Field/mutateFinals/modules/test/test/spi/Mutator.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2025, 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 test.spi; + +import java.lang.invoke.MethodHandle; +import java.lang.reflect.Field; + +public interface Mutator { + void set(Field f, Object obj, Object value) throws IllegalAccessException; + void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException; + void setByte(Field f, Object obj, byte value) throws IllegalAccessException; + void setChar(Field f, Object obj, char value) throws IllegalAccessException; + void setShort(Field f, Object obj, short value) throws IllegalAccessException; + void setInt(Field f, Object obj, int value) throws IllegalAccessException; + void setLong(Field f, Object obj, long value) throws IllegalAccessException; + void setFloat(Field f, Object obj, float value) throws IllegalAccessException; + void setDouble(Field f, Object obj, double value) throws IllegalAccessException; + MethodHandle unreflectSetter(Field f) throws IllegalAccessException; + + static Mutator throwing() { + return new Mutator() { + @Override + public void set(Field f, Object obj, Object value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setBoolean(Field f, Object obj, boolean value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setByte(Field f, Object obj, byte value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setChar(Field f, Object obj, char value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setShort(Field f, Object obj, short value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setInt(Field f, Object obj, int value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setLong(Field f, Object obj, long value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setFloat(Field f, Object obj, float value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public void setDouble(Field f, Object obj, double value) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { + throw new IllegalAccessException(); + } + @Override + public String toString() { + return ""; + } + }; + } +} diff --git a/test/jdk/java/util/jar/Attributes/NullAndEmptyKeysAndValues.java b/test/jdk/java/util/jar/Attributes/NullAndEmptyKeysAndValues.java index 396c0a27b6d..c62ddcced8a 100644 --- a/test/jdk/java/util/jar/Attributes/NullAndEmptyKeysAndValues.java +++ b/test/jdk/java/util/jar/Attributes/NullAndEmptyKeysAndValues.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2018, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2018, 2025, 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 @@ -38,7 +38,7 @@ import static org.testng.Assert.*; * @test * @bug 8066619 * @modules java.base/java.util.jar:+open - * @run testng/othervm NullAndEmptyKeysAndValues + * @run testng/othervm --enable-final-field-mutation=ALL-UNNAMED NullAndEmptyKeysAndValues * @summary Tests manifests with {@code null} and empty string {@code ""} * values as section name, header name, or value in both main and named * attributes sections. diff --git a/test/jdk/java/util/logging/FileHandlerLongLimit.java b/test/jdk/java/util/logging/FileHandlerLongLimit.java index 43594ad3df4..69aa20e8bed 100644 --- a/test/jdk/java/util/logging/FileHandlerLongLimit.java +++ b/test/jdk/java/util/logging/FileHandlerLongLimit.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2024, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2025, 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 @@ -42,7 +42,7 @@ import java.util.logging.LogRecord; * @bug 8059767 * @summary tests that FileHandler can accept a long limit. * @modules java.logging/java.util.logging:open - * @run main/othervm FileHandlerLongLimit + * @run main/othervm --enable-final-field-mutation=ALL-UNNAMED FileHandlerLongLimit * @author danielfuchs * @key randomness */ diff --git a/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java b/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java index d6e126a493f..155ece15b86 100644 --- a/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java +++ b/test/jdk/jdk/jfr/event/metadata/TestLookForUntestedEvents.java @@ -81,6 +81,10 @@ public class TestLookForUntestedEvents { private static final Set coveredVirtualThreadEvents = Set.of( "VirtualThreadPinned", "VirtualThreadSubmitFailed"); + // This event is tested in test/jdk/java/lang/reflect/Field/mutateFinals/FinalFieldMutationEventTest.java + private static final Set coveredFinalFieldMutationEvents = Set.of( + "FinalFieldMutationEvent"); + // This is a "known failure list" for this test. // NOTE: if the event is not covered, a bug should be open, and bug number // noted in the comments for this set. @@ -128,6 +132,7 @@ public class TestLookForUntestedEvents { eventsNotCoveredByTest.removeAll(hardToTestEvents); eventsNotCoveredByTest.removeAll(coveredGcEvents); eventsNotCoveredByTest.removeAll(coveredVirtualThreadEvents); + eventsNotCoveredByTest.removeAll(coveredFinalFieldMutationEvents); eventsNotCoveredByTest.removeAll(coveredContainerEvents); eventsNotCoveredByTest.removeAll(knownNotCoveredEvents); diff --git a/test/jdk/sun/security/pkcs11/Cipher/CancelMultipart.java b/test/jdk/sun/security/pkcs11/Cipher/CancelMultipart.java index 28f3699050c..afd9fb2cfca 100644 --- a/test/jdk/sun/security/pkcs11/Cipher/CancelMultipart.java +++ b/test/jdk/sun/security/pkcs11/Cipher/CancelMultipart.java @@ -26,7 +26,7 @@ * @bug 8258833 * @library /test/lib .. * @modules jdk.crypto.cryptoki/sun.security.pkcs11:open - * @run main/othervm CancelMultipart + * @run main/othervm --enable-final-field-mutation=ALL-UNNAMED CancelMultipart */ import java.lang.reflect.Field; diff --git a/test/jdk/sun/security/provider/SecureRandom/DRBGS11n.java b/test/jdk/sun/security/provider/SecureRandom/DRBGS11n.java index 410bf0cacb2..95b91805882 100644 --- a/test/jdk/sun/security/provider/SecureRandom/DRBGS11n.java +++ b/test/jdk/sun/security/provider/SecureRandom/DRBGS11n.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2016, 2025, 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 @@ -34,8 +34,8 @@ import java.lang.reflect.Field; * @bug 8157308 * @modules java.base/sun.security.provider:+open * @summary Make AbstractDrbg non-Serializable - * @run main DRBGS11n mech - * @run main DRBGS11n capability + * @run main/othervm --enable-final-field-mutation=ALL-UNNAMED DRBGS11n mech + * @run main/othervm --enable-final-field-mutation=ALL-UNNAMED DRBGS11n capability */ public class DRBGS11n { diff --git a/test/jdk/sun/security/util/ManifestDigester/FindSection.java b/test/jdk/sun/security/util/ManifestDigester/FindSection.java index ed80c5bcbbd..f7517427239 100644 --- a/test/jdk/sun/security/util/ManifestDigester/FindSection.java +++ b/test/jdk/sun/security/util/ManifestDigester/FindSection.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2019, 2025, 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 @@ -44,7 +44,7 @@ import static org.testng.Assert.*; * @bug 8217375 * @modules java.base/sun.security.util:+open * @compile ../../tools/jarsigner/Utils.java - * @run testng/othervm FindSection + * @run testng/othervm --enable-final-field-mutation=ALL-UNNAMED FindSection * @summary Check {@link ManifestDigester#findSection}. */ public class FindSection { diff --git a/test/langtools/jdk/jshell/CompletionSuggestionTest.java b/test/langtools/jdk/jshell/CompletionSuggestionTest.java index 7d739efddc6..d31a32b63f8 100644 --- a/test/langtools/jdk/jshell/CompletionSuggestionTest.java +++ b/test/langtools/jdk/jshell/CompletionSuggestionTest.java @@ -32,7 +32,7 @@ * jdk.jshell/jdk.jshell:open * @build toolbox.ToolBox toolbox.JarTask toolbox.JavacTask * @build KullaTesting TestingInputStream Compiler - * @run junit/timeout=480 CompletionSuggestionTest + * @run junit/othervm/timeout=480 --enable-final-field-mutation=ALL-UNNAMED CompletionSuggestionTest */ import java.io.IOException; diff --git a/test/lib/jdk/test/lib/jfr/EventNames.java b/test/lib/jdk/test/lib/jfr/EventNames.java index a00898358a8..e53b242097e 100644 --- a/test/lib/jdk/test/lib/jfr/EventNames.java +++ b/test/lib/jdk/test/lib/jfr/EventNames.java @@ -220,6 +220,7 @@ public class EventNames { public static final String VirtualThreadEnd = PREFIX + "VirtualThreadEnd"; public static final String VirtualThreadPinned = PREFIX + "VirtualThreadPinned"; public static final String VirtualThreadSubmitFailed = PREFIX + "VirtualThreadSubmitFailed"; + public static final String FinalFieldMutation = PREFIX + "FinalFieldMutation"; // Containers public static final String ContainerConfiguration = PREFIX + "ContainerConfiguration"; diff --git a/test/micro/org/openjdk/bench/java/lang/reflect/FieldSet.java b/test/micro/org/openjdk/bench/java/lang/reflect/FieldSet.java new file mode 100644 index 00000000000..4b8bd59a5c8 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/lang/reflect/FieldSet.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025, 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.java.lang.reflect; + +import java.lang.reflect.Field; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.ThreadLocalRandom; + +import org.openjdk.jmh.annotations.*; + +@BenchmarkMode(Mode.AverageTime) +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.NANOSECONDS) +@Warmup(iterations = 10, time = 1, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 10, time = 2, timeUnit = TimeUnit.SECONDS) +@Fork(3) +public class FieldSet { + + static class FieldHolder { + Object obj; + int intValue; + long longValue; + FieldHolder(Object obj, int i, long l) { + this.obj = obj; + this.intValue = i; + this.longValue = l; + } + } + + static class FinalFieldHolder { + final Object obj; + final int intValue; + final long longValue; + FinalFieldHolder(Object obj, int i, long l) { + this.obj = obj; + this.intValue = i; + this.longValue = l; + } + } + + private FieldHolder fieldHolder; + private FinalFieldHolder finalFieldHolder; + + private Field objField1, objField2, objField3; + private Field intField1, intField2, intField3; + private Field longField1, longField2, longField3; + + @Setup + public void setup() throws Exception { + fieldHolder = new FieldHolder(new Object(), 1, 1L); + finalFieldHolder = new FinalFieldHolder(new Object(), 1, 1L); + + // non-final && !override + objField1 = FieldHolder.class.getDeclaredField("obj"); + intField1 = FieldHolder.class.getDeclaredField("intValue"); + longField1 = FieldHolder.class.getDeclaredField("longValue"); + + // non-final && override + objField2 = FieldHolder.class.getDeclaredField("obj"); + objField2.setAccessible(true); + intField2 = FieldHolder.class.getDeclaredField("intValue"); + intField2.setAccessible(true); + longField2 = FieldHolder.class.getDeclaredField("longValue"); + longField2.setAccessible(true); + + // final && override + objField3 = FinalFieldHolder.class.getDeclaredField("obj"); + objField3.setAccessible(true); + intField3 = FinalFieldHolder.class.getDeclaredField("intValue"); + intField3.setAccessible(true); + longField3 = FinalFieldHolder.class.getDeclaredField("longValue"); + longField3.setAccessible(true); + } + + // non-final && !override + + @Benchmark + public void setNonFinalObjectField() throws Exception { + objField1.set(fieldHolder, new Object()); + } + + @Benchmark + public void setNonFinalIntField() throws Exception { + int newValue = ThreadLocalRandom.current().nextInt(); + intField1.setInt(fieldHolder, newValue); + } + + @Benchmark + public void setNonFinalLongField() throws Exception { + long newValue = ThreadLocalRandom.current().nextLong(); + longField1.setLong(fieldHolder, newValue); + } + + // non-final && override + + @Benchmark + public void setNonFinalObjectFieldWithOverride() throws Exception { + objField2.set(fieldHolder, new Object()); + } + + @Benchmark + public void setNonFinalIntFieldWithOverride() throws Exception { + int newValue = ThreadLocalRandom.current().nextInt(); + intField2.setInt(fieldHolder, newValue); + } + + @Benchmark + public void setNonFinalLongFieldWithOverride() throws Exception { + long newValue = ThreadLocalRandom.current().nextLong(); + longField2.setLong(fieldHolder, newValue); + } + + // final && override + + @Benchmark + public void setFinalObjectField()throws Exception { + objField3.set(finalFieldHolder, new Object()); + } + + @Benchmark + public void setFinalIntField() throws Exception { + int newValue = ThreadLocalRandom.current().nextInt(); + intField3.setInt(finalFieldHolder, newValue); + } + + @Benchmark + public void setFinalLongField() throws Exception { + long newValue = ThreadLocalRandom.current().nextLong(); + longField3.setLong(finalFieldHolder, newValue); + } +}