From 2d28c8d438dde90be4be5053cfa6a84e48368afd Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Tue, 19 Aug 2025 15:41:11 +0800 Subject: [PATCH 01/12] UGI --- .../client/KerberosAuthenticator.java | 6 +- .../org/apache/hadoop/util/SubjectUtil.java | 214 ++++++++++++++++++ .../hadoop/security/UserGroupInformation.java | 12 +- 3 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java index 30e65efe10cba..de466a3637d63 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java @@ -19,6 +19,7 @@ import org.apache.hadoop.security.authentication.server.HttpConstants; import org.apache.hadoop.security.authentication.util.AuthToken; import org.apache.hadoop.security.authentication.util.KerberosUtil; +import org.apache.hadoop.util.SubjectUtil; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; @@ -35,8 +36,6 @@ import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; -import java.security.AccessControlContext; -import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.HashMap; @@ -300,8 +299,7 @@ private boolean isNegotiate(HttpURLConnection conn) throws IOException { private void doSpnegoSequence(final AuthenticatedURL.Token token) throws IOException, AuthenticationException { try { - AccessControlContext context = AccessController.getContext(); - Subject subject = Subject.getSubject(context); + Subject subject = SubjectUtil.current(); if (subject == null || (!KerberosUtil.hasKerberosKeyTab(subject) && !KerberosUtil.hasKerberosTicket(subject))) { diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java new file mode 100644 index 0000000000000..a1f161e7f2e37 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -0,0 +1,214 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.util; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +import javax.security.auth.Subject; + +import org.apache.hadoop.classification.InterfaceAudience.Private; + +@Private +public class SubjectUtil { + private static MethodHandle CALL_AS; + private static MethodHandle CURRENT; + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + try { + // Subject.doAs() is deprecated for removal and replaced by Subject.callAs(). + // Lookup first the new API, since for Java versions where both exist, the + // new API delegates to the old API (e.g. Java 18, 19 and 20). + // Otherwise (e.g. Java 17), lookup the old API. + CALL_AS = lookup.findStatic(Subject.class, "callAs", + MethodType.methodType(Object.class, Subject.class, Callable.class)); + } catch (NoSuchMethodException x) { + try { + // Lookup the old API. + MethodType oldSignature = MethodType.methodType( + Object.class, Subject.class, PrivilegedExceptionAction.class); + MethodHandle doAs = lookup.findStatic(Subject.class, "doAs", oldSignature); + // Convert the Callable used in the new API to the PrivilegedAction used + // in the old API. + MethodType convertSignature = MethodType.methodType( + PrivilegedExceptionAction.class, Callable.class); + MethodHandle converter = lookup.findStatic( + SubjectUtil.class, "callableToPrivilegedExceptionAction", convertSignature); + CALL_AS = MethodHandles.filterArguments(doAs, 1, converter); + } catch (NoSuchMethodException e) { + throw new AssertionError(e); + } + } + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + static { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + // Subject.getSubject(AccessControlContext) is deprecated for removal and + // replaced by Subject.current(). + // Lookup first the new API, since for Java versions where both exists, the + // new API delegates to the old API (e.g. Java 18, 19 and 20). + // Otherwise (e.g. Java 17), lookup the old API. + CURRENT = lookup.findStatic( + Subject.class, "current", MethodType.methodType(Subject.class)); + } catch (NoSuchMethodException e) { + MethodHandle getContext = lookupGetContext(); + MethodHandle getSubject = lookupGetSubject(); + CURRENT = MethodHandles.filterReturnValue(getContext, getSubject); + } catch (IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetSubject() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + Class contextKlass = ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + return lookup.findStatic(Subject.class, + "getSubject", MethodType.methodType(Subject.class, contextKlass)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + private static MethodHandle lookupGetContext() { + try { + // Use reflection to work with Java versions that have and don't have + // AccessController. + Class controllerKlass = ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessController"); + Class contextKlass = ClassLoader.getSystemClassLoader() + .loadClass("java.security.AccessControlContext"); + + MethodHandles.Lookup lookup = MethodHandles.lookup(); + return lookup.findStatic( + controllerKlass, "getContext", MethodType.methodType(contextKlass)); + } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { + throw new AssertionError(e); + } + } + + /** + * Maps to Subject.callAs() if available, otherwise maps to Subject.doAs(). + * It also wraps the Callable so that the Subject is propagated to the new thread + * in all Java versions. + * + * @param subject the subject this action runs as + * @param action the action to run + * @return the result of the action + * @param the type of the result + * @throws CompletionException + */ + @SuppressWarnings("unchecked") + public static T callAs(Subject subject, Callable action) throws CompletionException { + try { + return (T) CALL_AS.invoke(subject, action); + } catch (PrivilegedActionException e) { + throw new CompletionException(e.getCause()); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } + + /** + * Maps action to a Callable, and delegates to callAs(). On older JVMs, the + * action may be double wrapped (into Callable, and then back into + * PrivilegedAction). + * + * @param subject the subject this action runs as + * @param action action the action to run + * @return the result of the action + */ + public static T doAs(Subject subject, PrivilegedAction action) { + return callAs(subject, privilegedActionToCallable(action)); + } + + /** + * Maps action to a Callable, and delegates to callAs(). On older JVMs, the + * action may be double wrapped (into Callable, and then back into + * PrivilegedAction). + * + * @param subject the subject this action runs as + * @param action action the action to run + * @return the result of the action + */ + public static T doAs( + Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + try { + return callAs(subject, privilegedExceptionActionToCallable(action)); + } catch (CompletionException ce) { + try { + Exception cause = (Exception) (ce.getCause()); + throw new PrivilegedActionException(cause); + } catch (ClassCastException castException) { + // This should never happen, as PrivilegedExceptionAction should not wrap + // non-checked exceptions + throw new PrivilegedActionException(new UndeclaredThrowableException(ce.getCause())); + } + } + } + + /** + * Maps to Subject.currect() is available, otherwise maps to + * Subject.getSubject() + * + * @return the current subject + */ + public static Subject current() { + try { + return (Subject) CURRENT.invoke(); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } + + @SuppressWarnings("unused") + private static PrivilegedExceptionAction callableToPrivilegedExceptionAction( + Callable callable) { + return callable::call; + } + + private static Callable privilegedExceptionActionToCallable( + PrivilegedExceptionAction action) { + return action::run; + } + + private static Callable privilegedActionToCallable( + PrivilegedAction action) { + return action::run; + } + + @SuppressWarnings("unchecked") + private static RuntimeException sneakyThrow(Throwable e) throws E { + throw (E) e; + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java index 6525460d56180..d0334fba7e785 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java @@ -33,8 +33,6 @@ import java.io.File; import java.io.IOException; import java.lang.reflect.UndeclaredThrowableException; -import java.security.AccessControlContext; -import java.security.AccessController; import java.security.Principal; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; @@ -89,6 +87,7 @@ import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.util.Shell; import org.apache.hadoop.util.StringUtils; +import org.apache.hadoop.util.SubjectUtil; import org.apache.hadoop.util.Time; import org.slf4j.Logger; @@ -585,8 +584,7 @@ public boolean hasKerberosCredentials() { @InterfaceStability.Evolving public static UserGroupInformation getCurrentUser() throws IOException { ensureInitialized(); - AccessControlContext context = AccessController.getContext(); - Subject subject = Subject.getSubject(context); + Subject subject = SubjectUtil.current(); if (subject == null || subject.getPrincipals(User.class).isEmpty()) { return getLoginUser(); } else { @@ -1936,9 +1934,9 @@ protected Subject getSubject() { @InterfaceStability.Evolving public T doAs(PrivilegedAction action) { tracePrivilegedAction(action); - return Subject.doAs(subject, action); + return SubjectUtil.doAs(subject, action); } - + /** * Run the given action as the user, potentially throwing an exception. * @param the return type of the run method @@ -1956,7 +1954,7 @@ public T doAs(PrivilegedExceptionAction action ) throws IOException, InterruptedException { try { tracePrivilegedAction(action); - return Subject.doAs(subject, action); + return SubjectUtil.doAs(subject, action); } catch (PrivilegedActionException pae) { Throwable cause = pae.getCause(); LOG.debug("PrivilegedActionException as: {}", this, cause); From 69a2ccf2e93a8301eeb642c8555d8f487ccc9e21 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 11:41:20 +0800 Subject: [PATCH 02/12] unwrap CompletionException --- .../java/org/apache/hadoop/util/SubjectUtil.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java index a1f161e7f2e37..6f6a034f599dd 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -149,7 +149,17 @@ public static T callAs(Subject subject, Callable action) throws Completio * @return the result of the action */ public static T doAs(Subject subject, PrivilegedAction action) { - return callAs(subject, privilegedActionToCallable(action)); + try { + return callAs(subject, privilegedActionToCallable(action)); + } catch (CompletionException ce) { + Exception cause = (Exception) (ce.getCause()); + if (cause != null) { + throw sneakyThrow(cause); + } else { + // This should never happen, as CompletionException should always wrap an exception + throw ce; + } + } } /** From 9ea6317d26eab726d06714f3293330358267b4a8 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 11:49:55 +0800 Subject: [PATCH 03/12] Update hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/main/java/org/apache/hadoop/util/SubjectUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java index 6f6a034f599dd..7b516187b34a8 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -188,7 +188,7 @@ public static T doAs( } /** - * Maps to Subject.currect() is available, otherwise maps to + * Maps to Subject.current() if available, otherwise maps to * Subject.getSubject() * * @return the current subject From a2f75253f54471477fed330402685add6c201a46 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 14:00:41 +0800 Subject: [PATCH 04/12] Fix codestyle and javadocs --- .../java/org/apache/hadoop/util/SubjectUtil.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java index 7b516187b34a8..096d73aad1449 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -126,7 +126,9 @@ private static MethodHandle lookupGetContext() { * @param action the action to run * @return the result of the action * @param the type of the result - * @throws CompletionException + * @throws CompletionException if {@code action.call()} throws an exception. + * The cause of the {@code CompletionException} is set to the exception + * thrown by {@code action.call()}. */ @SuppressWarnings("unchecked") public static T callAs(Subject subject, Callable action) throws CompletionException { @@ -147,6 +149,7 @@ public static T callAs(Subject subject, Callable action) throws Completio * @param subject the subject this action runs as * @param action action the action to run * @return the result of the action + * @param the type of the result */ public static T doAs(Subject subject, PrivilegedAction action) { try { @@ -164,12 +167,15 @@ public static T doAs(Subject subject, PrivilegedAction action) { /** * Maps action to a Callable, and delegates to callAs(). On older JVMs, the - * action may be double wrapped (into Callable, and then back into - * PrivilegedAction). + * action may be double wrapped (into Callable, and then back into PrivilegedAction). * * @param subject the subject this action runs as * @param action action the action to run * @return the result of the action + * @param the type of the result + * @throws PrivilegedActionException if {@code action.run()} throws an exception. + * The cause of the {@code PrivilegedActionException} is set to the exception + * thrown by {@code action.run()}. */ public static T doAs( Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { @@ -177,7 +183,7 @@ public static T doAs( return callAs(subject, privilegedExceptionActionToCallable(action)); } catch (CompletionException ce) { try { - Exception cause = (Exception) (ce.getCause()); + Exception cause = (Exception) ce.getCause(); throw new PrivilegedActionException(cause); } catch (ClassCastException castException) { // This should never happen, as PrivilegedExceptionAction should not wrap From 5d407b3118312afbe3d847d63b55d6c1dd926e75 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 14:17:02 +0800 Subject: [PATCH 05/12] nit --- .../src/main/java/org/apache/hadoop/util/SubjectUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java index 096d73aad1449..fe1ee74ec2ac3 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -155,7 +155,7 @@ public static T doAs(Subject subject, PrivilegedAction action) { try { return callAs(subject, privilegedActionToCallable(action)); } catch (CompletionException ce) { - Exception cause = (Exception) (ce.getCause()); + Throwable cause = ce.getCause(); if (cause != null) { throw sneakyThrow(cause); } else { From 0a79322c61d8022717b0f821d69ebb65f19ff3b9 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 21:09:08 +0800 Subject: [PATCH 06/12] code style --- .../src/main/java/org/apache/hadoop/util/SubjectUtil.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java index fe1ee74ec2ac3..debf3082094a1 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java @@ -194,8 +194,7 @@ public static T doAs( } /** - * Maps to Subject.current() if available, otherwise maps to - * Subject.getSubject() + * Maps to Subject.current() if available, otherwise maps to Subject.getSubject(). * * @return the current subject */ @@ -227,4 +226,7 @@ private static Callable privilegedActionToCallable( private static RuntimeException sneakyThrow(Throwable e) throws E { throw (E) e; } + + private SubjectUtil() { + } } From 74b45de59c0fc8b4cd9974bfd53d1ff2ba7ee814 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Wed, 20 Aug 2025 22:57:42 +0800 Subject: [PATCH 07/12] rewrite and add dedicated UT --- .../client/KerberosAuthenticator.java | 2 +- .../authentication}/util/SubjectUtil.java | 195 +++++++++----- .../authentication/util/TestSubjectUtil.java | 238 ++++++++++++++++++ .../hadoop/security/UserGroupInformation.java | 2 +- 4 files changed, 375 insertions(+), 62 deletions(-) rename hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/{ => security/authentication}/util/SubjectUtil.java (53%) create mode 100644 hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java index de466a3637d63..d27b93bd50c3d 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/client/KerberosAuthenticator.java @@ -19,7 +19,7 @@ import org.apache.hadoop.security.authentication.server.HttpConstants; import org.apache.hadoop.security.authentication.util.AuthToken; import org.apache.hadoop.security.authentication.util.KerberosUtil; -import org.apache.hadoop.util.SubjectUtil; +import org.apache.hadoop.security.authentication.util.SubjectUtil; import org.ietf.jgss.GSSContext; import org.ietf.jgss.GSSManager; import org.ietf.jgss.GSSName; diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java similarity index 53% rename from hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java rename to hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index debf3082094a1..5727382d8847f 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package org.apache.hadoop.util; +package org.apache.hadoop.security.authentication.util; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -32,44 +32,65 @@ import org.apache.hadoop.classification.InterfaceAudience.Private; +/** + * An utility class that adapt the Security Manager and APIs related to it for + * JDK 8 and above. + *

+ * In JDK 17, the Security Manager and APIs related to it have been deprecated + * and are subject to removal in a future release. There is no replacement for + * the Security Manager. See JEP 411 + * for discussion and alternatives. + *

+ * In JDK 24, the Security Manager has been permanently disabled. See + * JEP 486 for more information. + */ @Private -public class SubjectUtil { - private static MethodHandle CALL_AS; - private static MethodHandle CURRENT; +public final class SubjectUtil { + private static final MethodHandle CALL_AS = lookupCallAs(); + static final boolean HAS_CALL_AS = CALL_AS != null; + private static final MethodHandle DO_AS = HAS_CALL_AS ? null : lookupDoAs(); + private static final MethodHandle DO_AS_THROW_EXCEPTION = + HAS_CALL_AS ? null : lookupDoAsThrowException(); + private static final MethodHandle CURRENT = lookupCurrent(); - static { + private static MethodHandle lookupCallAs() { MethodHandles.Lookup lookup = MethodHandles.lookup(); try { try { - // Subject.doAs() is deprecated for removal and replaced by Subject.callAs(). - // Lookup first the new API, since for Java versions where both exist, the - // new API delegates to the old API (e.g. Java 18, 19 and 20). - // Otherwise (e.g. Java 17), lookup the old API. - CALL_AS = lookup.findStatic(Subject.class, "callAs", + // Subject.callAs() is available since Java 18. + return lookup.findStatic(Subject.class, "callAs", MethodType.methodType(Object.class, Subject.class, Callable.class)); } catch (NoSuchMethodException x) { - try { - // Lookup the old API. - MethodType oldSignature = MethodType.methodType( - Object.class, Subject.class, PrivilegedExceptionAction.class); - MethodHandle doAs = lookup.findStatic(Subject.class, "doAs", oldSignature); - // Convert the Callable used in the new API to the PrivilegedAction used - // in the old API. - MethodType convertSignature = MethodType.methodType( - PrivilegedExceptionAction.class, Callable.class); - MethodHandle converter = lookup.findStatic( - SubjectUtil.class, "callableToPrivilegedExceptionAction", convertSignature); - CALL_AS = MethodHandles.filterArguments(doAs, 1, converter); - } catch (NoSuchMethodException e) { - throw new AssertionError(e); - } + return null; } } catch (IllegalAccessException e) { - throw new AssertionError(e); + throw new ExceptionInInitializerError(e); + } + } + + private static MethodHandle lookupDoAs() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + MethodType signature = MethodType.methodType( + Object.class, Subject.class, PrivilegedAction.class); + return lookup.findStatic(Subject.class, "doAs", signature); + } catch (IllegalAccessException | NoSuchMethodException e) { + throw new ExceptionInInitializerError(e); + } + } + + private static MethodHandle lookupDoAsThrowException() { + MethodHandles.Lookup lookup = MethodHandles.lookup(); + try { + MethodType signature = MethodType.methodType( + Object.class, Subject.class, PrivilegedExceptionAction.class); + return lookup.findStatic(Subject.class, "doAs", signature); + } catch (IllegalAccessException | NoSuchMethodException e) { + throw new ExceptionInInitializerError(e); } } - static { + private static MethodHandle lookupCurrent() { MethodHandles.Lookup lookup = MethodHandles.lookup(); try { // Subject.getSubject(AccessControlContext) is deprecated for removal and @@ -77,14 +98,14 @@ public class SubjectUtil { // Lookup first the new API, since for Java versions where both exists, the // new API delegates to the old API (e.g. Java 18, 19 and 20). // Otherwise (e.g. Java 17), lookup the old API. - CURRENT = lookup.findStatic( + return lookup.findStatic( Subject.class, "current", MethodType.methodType(Subject.class)); } catch (NoSuchMethodException e) { MethodHandle getContext = lookupGetContext(); MethodHandle getSubject = lookupGetSubject(); - CURRENT = MethodHandles.filterReturnValue(getContext, getSubject); + return MethodHandles.filterReturnValue(getContext, getSubject); } catch (IllegalAccessException e) { - throw new AssertionError(e); + throw new ExceptionInInitializerError(e); } } @@ -96,7 +117,7 @@ private static MethodHandle lookupGetSubject() { return lookup.findStatic(Subject.class, "getSubject", MethodType.methodType(Subject.class, contextKlass)); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { - throw new AssertionError(e); + throw new ExceptionInInitializerError(e); } } @@ -113,7 +134,7 @@ private static MethodHandle lookupGetContext() { return lookup.findStatic( controllerKlass, "getContext", MethodType.methodType(contextKlass)); } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) { - throw new AssertionError(e); + throw new ExceptionInInitializerError(e); } } @@ -132,18 +153,26 @@ private static MethodHandle lookupGetContext() { */ @SuppressWarnings("unchecked") public static T callAs(Subject subject, Callable action) throws CompletionException { - try { - return (T) CALL_AS.invoke(subject, action); - } catch (PrivilegedActionException e) { - throw new CompletionException(e.getCause()); - } catch (Throwable t) { - throw sneakyThrow(t); + if (HAS_CALL_AS) { + try { + return (T) CALL_AS.invoke(subject, action); + } catch (Throwable t) { + throw sneakyThrow(t); + } + } else { + try { + return (T) DO_AS.invoke(subject, callableToPrivilegedAction(action)); + } catch (Exception e) { + throw new CompletionException(e); + } catch (Throwable t) { + throw sneakyThrow(t); + } } } /** - * Maps action to a Callable, and delegates to callAs(). On older JVMs, the - * action may be double wrapped (into Callable, and then back into + * Maps action to a Callable, and delegates to callAs(). On older JVMs, + * the action may be double wrapped (into Callable, and then back into * PrivilegedAction). * * @param subject the subject this action runs as @@ -151,16 +180,26 @@ public static T callAs(Subject subject, Callable action) throws Completio * @return the result of the action * @param the type of the result */ + @SuppressWarnings("unchecked") public static T doAs(Subject subject, PrivilegedAction action) { - try { - return callAs(subject, privilegedActionToCallable(action)); - } catch (CompletionException ce) { - Throwable cause = ce.getCause(); - if (cause != null) { - throw sneakyThrow(cause); - } else { - // This should never happen, as CompletionException should always wrap an exception - throw ce; + if (HAS_CALL_AS) { + try { + return callAs(subject, privilegedActionToCallable(action)); + } catch (CompletionException ce) { + Throwable cause = ce.getCause(); + if (cause != null) { + throw sneakyThrow(cause); + } else { + // This should never happen, CompletionException thrown by Subject.callAs + // should always wrap an exception + throw ce; + } + } + } else { + try { + return (T) DO_AS.invoke(subject, action); + } catch (Throwable t) { + throw sneakyThrow(t); } } } @@ -177,18 +216,31 @@ public static T doAs(Subject subject, PrivilegedAction action) { * The cause of the {@code PrivilegedActionException} is set to the exception * thrown by {@code action.run()}. */ + @SuppressWarnings("unchecked") public static T doAs( Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { - try { - return callAs(subject, privilegedExceptionActionToCallable(action)); - } catch (CompletionException ce) { + if (HAS_CALL_AS) { try { - Exception cause = (Exception) ce.getCause(); - throw new PrivilegedActionException(cause); - } catch (ClassCastException castException) { - // This should never happen, as PrivilegedExceptionAction should not wrap - // non-checked exceptions - throw new PrivilegedActionException(new UndeclaredThrowableException(ce.getCause())); + return callAs(subject, privilegedExceptionActionToCallable(action)); + } catch (CompletionException ce) { + try { + Exception cause = (Exception) ce.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { + throw new PrivilegedActionException(cause); + } + } catch (ClassCastException castException) { + // This should never happen, as PrivilegedExceptionAction should not wrap + // non-checked exceptions + throw new PrivilegedActionException(new UndeclaredThrowableException(ce.getCause())); + } + } + } else { + try { + return (T) DO_AS_THROW_EXCEPTION.invoke(subject, action); + } catch (Throwable t) { + throw sneakyThrow(t); } } } @@ -206,7 +258,17 @@ public static Subject current() { } } - @SuppressWarnings("unused") + private static PrivilegedAction callableToPrivilegedAction( + Callable callable) { + return () -> { + try { + return callable.call(); + } catch (Exception e) { + throw sneakyThrow(e); + } + }; + } + private static PrivilegedExceptionAction callableToPrivilegedExceptionAction( Callable callable) { return callable::call; @@ -222,8 +284,21 @@ private static Callable privilegedActionToCallable( return action::run; } + /** + * The sneaky throw concept allows the caller to throw any checked exception without + * defining it explicitly in the method signature. + *

+ * See "Sneaky Throws" in Java + * for more details. + * + * @param e the exception that will be thrown. + * @return unreachable, the method always throws an exception before returning + * @param the thrown exception type, trick the compiler into inferring it as + * a {@code RuntimeException} type. + * @throws E the original exception passes by caller + */ @SuppressWarnings("unchecked") - private static RuntimeException sneakyThrow(Throwable e) throws E { + static RuntimeException sneakyThrow(Throwable e) throws E { throw (E) e; } diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java new file mode 100644 index 0000000000000..581f4b3ceec85 --- /dev/null +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java @@ -0,0 +1,238 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.hadoop.security.authentication.util; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.security.PrivilegedAction; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionException; + +public class TestSubjectUtil { + + // "1.8"->8, "9"->9, "10"->10 + private static final int JAVA_SPEC_VER = Math.max(8, Integer.parseInt( + System.getProperty("java.specification.version").split("\\.")[0])); + + @Test + void testHasCallAs() { + assertEquals(JAVA_SPEC_VER > 17, SubjectUtil.HAS_CALL_AS); + } + + @Test + void testDoAsPrivilegedActionExceptionPropagation() { + // always throw the original exception thrown by action + Throwable e = assertThrows(IllegalArgumentException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { + @Override + public Object run() { + RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); + throw new IllegalArgumentException("Dummy IllegalArgumentException", innerE); + } + }) + ); + assertInstanceOf(IllegalArgumentException.class, e); + assertEquals("Dummy IllegalArgumentException", e.getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + + e = assertThrows(CompletionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { + @Override + public Object run() { + throw new CompletionException("Dummy CompletionException", null); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + assertEquals("Dummy CompletionException", e.getMessage()); + assertNull(e.getCause()); + + e = assertThrows(LinkageError.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { + @Override + public Object run() { + throw new LinkageError("Dummy LinkageError"); + } + }) + ); + assertInstanceOf(LinkageError.class, e); + assertEquals("Dummy LinkageError", e.getMessage()); + assertNull(e.getCause()); + } + + @Test + void testDoAsPrivilegedExceptionActionExceptionPropagation() { + // throw PrivilegedActionException that wrap the original exception when action throw + // a checked exception + Throwable e = assertThrows(PrivilegedActionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + @Override + public Object run() throws Exception { + RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); + throw new IOException("Dummy IOException", innerE); + } + }) + ); + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertInstanceOf(IOException.class, e.getCause()); + assertEquals("Dummy IOException", e.getCause().getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause().getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause()); + + // the original exception when action throw a runtime exception + e = assertThrows(RuntimeException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + @Override + public Object run() throws Exception { + throw new RuntimeException("Dummy RuntimeException"); + } + }) + ); + assertInstanceOf(RuntimeException.class, e); + assertEquals("Dummy RuntimeException", e.getMessage()); + assertNull(e.getCause()); + + // CompletionException is subclass of RuntimeException, same as above case + e = assertThrows(CompletionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + @Override + public Object run() throws Exception { + throw new CompletionException(null); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + assertNull(e.getMessage()); + assertNull(e.getCause()); + + // wrap a PrivilegedActionException when action throws a PrivilegedActionException, because + // PrivilegedActionException is a checked exception + e = assertThrows(PrivilegedActionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + @Override + public Object run() throws Exception { + throw new PrivilegedActionException(null); + } + }) + ); + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + + // throw original error when action throw an error that is not the subclass of Exception + e = assertThrows(LinkageError.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + @Override + public Object run() throws Exception { + throw new LinkageError("Dummy LinkageError"); + } + }) + ); + assertInstanceOf(LinkageError.class, e); + assertEquals("Dummy LinkageError", e.getMessage()); + assertNull(e.getCause()); + } + + @Test + void testCallAsExceptionPropagation() { + // throw PrivilegedActionException that wrap the original exception when action throws + // an error that is the subclass of Exception. try checked exception + Throwable e = assertThrows(CompletionException.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + @Override + public Object call() throws Exception { + RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); + throw new IOException("Dummy IOException", innerE); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + assertEquals("java.io.IOException: Dummy IOException", e.getMessage()); + assertInstanceOf(IOException.class, e.getCause()); + assertEquals("Dummy IOException", e.getCause().getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause().getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause()); + + // same as above, try runtime exception + e = assertThrows(CompletionException.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + @Override + public Object call() throws Exception { + throw new RuntimeException("Dummy RuntimeException"); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + assertEquals("java.lang.RuntimeException: Dummy RuntimeException", e.getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause()); + assertEquals("Dummy RuntimeException", e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + + // wrap a CompletionException even the action throws a PrivilegedActionException + e = assertThrows(CompletionException.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + @Override + public Object call() throws Exception { + throw new PrivilegedActionException(null); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + assertEquals("java.security.PrivilegedActionException", e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + + // throw original error when action throw an error that is not the subclass of Exception + e = assertThrows(LinkageError.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + @Override + public Object call() throws Exception { + throw new LinkageError("Dummy LinkageError"); + } + }) + ); + assertInstanceOf(LinkageError.class, e); + assertEquals("Dummy LinkageError", e.getMessage()); + assertNull(e.getCause()); + } + + @Test + void testSneakyThrow() { + IOException e = assertThrows(IOException.class, this::throwCheckedException); + assertEquals("Dummy IOException", e.getMessage()); + } + + // A method that throw a checked exception, but has no exception declaration in signature + private void throwCheckedException() { + throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException")); + } +} diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java index d0334fba7e785..b6be569026fd7 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/security/UserGroupInformation.java @@ -83,11 +83,11 @@ import org.apache.hadoop.metrics2.lib.MutableRate; import org.apache.hadoop.security.SaslRpcServer.AuthMethod; import org.apache.hadoop.security.authentication.util.KerberosUtil; +import org.apache.hadoop.security.authentication.util.SubjectUtil; import org.apache.hadoop.security.token.Token; import org.apache.hadoop.security.token.TokenIdentifier; import org.apache.hadoop.util.Shell; import org.apache.hadoop.util.StringUtils; -import org.apache.hadoop.util.SubjectUtil; import org.apache.hadoop.util.Time; import org.slf4j.Logger; From 5264f428fb628914a5bf14a465711e08ce3a71d6 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Thu, 21 Aug 2025 13:25:36 +0800 Subject: [PATCH 08/12] fix --- .../authentication/util/SubjectUtil.java | 33 ++- .../authentication/util/TestSubjectUtil.java | 202 +++++++++++++----- 2 files changed, 157 insertions(+), 78 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index 5727382d8847f..4fe64566467f0 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -139,9 +139,7 @@ private static MethodHandle lookupGetContext() { } /** - * Maps to Subject.callAs() if available, otherwise maps to Subject.doAs(). - * It also wraps the Callable so that the Subject is propagated to the new thread - * in all Java versions. + * Map to Subject.callAs() if available, otherwise maps to Subject.doAs(). * * @param subject the subject this action runs as * @param action the action to run @@ -161,19 +159,21 @@ public static T callAs(Subject subject, Callable action) throws Completio } } else { try { - return (T) DO_AS.invoke(subject, callableToPrivilegedAction(action)); + return doAs(subject, callableToPrivilegedAction(action)); } catch (Exception e) { throw new CompletionException(e); - } catch (Throwable t) { - throw sneakyThrow(t); } } } /** - * Maps action to a Callable, and delegates to callAs(). On older JVMs, - * the action may be double wrapped (into Callable, and then back into - * PrivilegedAction). + * Map action to a Callable on Java 18 onwards, and delegates to callAs(). + * Call Subject.doAs directly on older JVM. + *

+ * Note: Exception propagation behavior is different since Java 12, it always + * throw the original exception thrown by action; for lower Java versions, + * throw a PrivilegedActionException that wraps the original exception when + * action throw a checked exception. * * @param subject the subject this action runs as * @param action action the action to run @@ -205,16 +205,16 @@ public static T doAs(Subject subject, PrivilegedAction action) { } /** - * Maps action to a Callable, and delegates to callAs(). On older JVMs, the - * action may be double wrapped (into Callable, and then back into PrivilegedAction). + * Maps action to a Callable on Java 18 onwards, and delegates to callAs(). + * Call Subject.doAs directly on older JVM. * * @param subject the subject this action runs as * @param action action the action to run * @return the result of the action * @param the type of the result - * @throws PrivilegedActionException if {@code action.run()} throws an exception. - * The cause of the {@code PrivilegedActionException} is set to the exception - * thrown by {@code action.run()}. + * @throws PrivilegedActionException if {@code action.run()} throws an checked exception. + * The cause of the {@code PrivilegedActionException} is set to the exception thrown + * by {@code action.run()}. */ @SuppressWarnings("unchecked") public static T doAs( @@ -269,11 +269,6 @@ private static PrivilegedAction callableToPrivilegedAction( }; } - private static PrivilegedExceptionAction callableToPrivilegedExceptionAction( - Callable callable) { - return callable::call; - } - private static Callable privilegedExceptionActionToCallable( PrivilegedExceptionAction action) { return action::run; diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java index 581f4b3ceec85..8a5a5bfdd8195 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java @@ -42,24 +42,72 @@ void testHasCallAs() { @Test void testDoAsPrivilegedActionExceptionPropagation() { - // always throw the original exception thrown by action - Throwable e = assertThrows(IllegalArgumentException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { - @Override - public Object run() { - RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); - throw new IllegalArgumentException("Dummy IllegalArgumentException", innerE); - } - }) + // in Java 12 onwards, always throw the original exception thrown by action; + // in lower Java versions, throw a PrivilegedActionException that wraps the + // original exception when action throws a checked exception + Throwable e = assertThrows(Exception.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction() { + @Override + public Object run() { + RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); + throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException", innerE)); + } + }) ); - assertInstanceOf(IllegalArgumentException.class, e); - assertEquals("Dummy IllegalArgumentException", e.getMessage()); - assertInstanceOf(RuntimeException.class, e.getCause()); - assertEquals("Inner Dummy RuntimeException", e.getCause().getMessage()); - assertNull(e.getCause().getCause()); + if (JAVA_SPEC_VER > 11) { + assertInstanceOf(IOException.class, e); + assertEquals("Dummy IOException", e.getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + } else { + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertInstanceOf(IOException.class, e.getCause()); + assertEquals("Dummy IOException", e.getCause().getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause().getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause()); + } + + // same as above case because PrivilegedActionException is a checked exception + e = assertThrows(PrivilegedActionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction() { + @Override + public Object run() { + throw SubjectUtil.sneakyThrow(new PrivilegedActionException(null)); + } + }) + ); + if (JAVA_SPEC_VER > 11) { + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertNull(e.getCause()); + } else { + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + } + // throw a PrivilegedActionException that wraps the original exception when action throws + // a runtime exception + e = assertThrows(RuntimeException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction() { + @Override + public Object run() { + throw new RuntimeException("Dummy RuntimeException"); + } + }) + ); + assertInstanceOf(RuntimeException.class, e); + assertEquals("Dummy RuntimeException", e.getMessage()); + assertNull(e.getCause()); + + // same as above case because CompletionException is a runtime exception e = assertThrows(CompletionException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction() { @Override public Object run() { throw new CompletionException("Dummy CompletionException", null); @@ -70,8 +118,9 @@ public Object run() { assertEquals("Dummy CompletionException", e.getMessage()); assertNull(e.getCause()); + // throw the original error when action throws an error e = assertThrows(LinkageError.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedAction() { @Override public Object run() { throw new LinkageError("Dummy LinkageError"); @@ -85,10 +134,10 @@ public Object run() { @Test void testDoAsPrivilegedExceptionActionExceptionPropagation() { - // throw PrivilegedActionException that wrap the original exception when action throw + // throw PrivilegedActionException that wraps the original exception when action throws // a checked exception Throwable e = assertThrows(PrivilegedActionException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction() { @Override public Object run() throws Exception { RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); @@ -104,9 +153,24 @@ public Object run() throws Exception { assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); assertNull(e.getCause().getCause().getCause()); - // the original exception when action throw a runtime exception + // same as above because PrivilegedActionException is a checked exception + e = assertThrows(PrivilegedActionException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction() { + @Override + public Object run() throws Exception { + throw new PrivilegedActionException(null); + } + }) + ); + assertInstanceOf(PrivilegedActionException.class, e); + assertNull(e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + + // throw the original exception when action throw a runtime exception e = assertThrows(RuntimeException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction() { @Override public Object run() throws Exception { throw new RuntimeException("Dummy RuntimeException"); @@ -117,9 +181,9 @@ public Object run() throws Exception { assertEquals("Dummy RuntimeException", e.getMessage()); assertNull(e.getCause()); - // CompletionException is subclass of RuntimeException, same as above case + // same as above case because CompletionException is a runtime exception e = assertThrows(CompletionException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction() { @Override public Object run() throws Exception { throw new CompletionException(null); @@ -130,25 +194,9 @@ public Object run() throws Exception { assertNull(e.getMessage()); assertNull(e.getCause()); - // wrap a PrivilegedActionException when action throws a PrivilegedActionException, because - // PrivilegedActionException is a checked exception - e = assertThrows(PrivilegedActionException.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { - @Override - public Object run() throws Exception { - throw new PrivilegedActionException(null); - } - }) - ); - assertInstanceOf(PrivilegedActionException.class, e); - assertNull(e.getMessage()); - assertInstanceOf(PrivilegedActionException.class, e.getCause()); - assertNull(e.getCause().getMessage()); - assertNull(e.getCause().getCause()); - - // throw original error when action throw an error that is not the subclass of Exception + // throw the original error when action throw an error e = assertThrows(LinkageError.class, () -> - SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction<>() { + SubjectUtil.doAs(SubjectUtil.current(), new PrivilegedExceptionAction() { @Override public Object run() throws Exception { throw new LinkageError("Dummy LinkageError"); @@ -162,10 +210,10 @@ public Object run() throws Exception { @Test void testCallAsExceptionPropagation() { - // throw PrivilegedActionException that wrap the original exception when action throws - // an error that is the subclass of Exception. try checked exception + // always throw a CompletionException that wraps the original exception, when action throw + // a checked or runtime exception Throwable e = assertThrows(CompletionException.class, () -> - SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + SubjectUtil.callAs(SubjectUtil.current(), new Callable() { @Override public Object call() throws Exception { RuntimeException innerE = new RuntimeException("Inner Dummy RuntimeException"); @@ -174,16 +222,54 @@ public Object call() throws Exception { }) ); assertInstanceOf(CompletionException.class, e); - assertEquals("java.io.IOException: Dummy IOException", e.getMessage()); - assertInstanceOf(IOException.class, e.getCause()); - assertEquals("Dummy IOException", e.getCause().getMessage()); - assertInstanceOf(RuntimeException.class, e.getCause().getCause()); - assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); - assertNull(e.getCause().getCause().getCause()); + if (JAVA_SPEC_VER > 11) { + assertEquals("java.io.IOException: Dummy IOException", e.getMessage()); + assertInstanceOf(IOException.class, e.getCause()); + assertEquals("Dummy IOException", e.getCause().getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause().getCause()); + assertEquals("Inner Dummy RuntimeException", e.getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause()); + } else { + assertEquals( + "java.security.PrivilegedActionException: java.io.IOException: Dummy IOException", + e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertInstanceOf(IOException.class, e.getCause().getCause()); + assertEquals("Dummy IOException", e.getCause().getCause().getMessage()); + assertInstanceOf(RuntimeException.class, e.getCause().getCause().getCause()); + assertEquals("Inner Dummy RuntimeException", + e.getCause().getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause().getCause()); + } - // same as above, try runtime exception e = assertThrows(CompletionException.class, () -> - SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + SubjectUtil.callAs(SubjectUtil.current(), new Callable() { + @Override + public Object call() throws Exception { + throw new PrivilegedActionException(null); + } + }) + ); + assertInstanceOf(CompletionException.class, e); + if (JAVA_SPEC_VER > 11) { + assertEquals("java.security.PrivilegedActionException", e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertNull(e.getCause().getCause()); + } else { + assertEquals( + "java.security.PrivilegedActionException: java.security.PrivilegedActionException", + e.getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertNull(e.getCause().getMessage()); + assertInstanceOf(PrivilegedActionException.class, e.getCause().getCause()); + assertNull(e.getCause().getCause().getMessage()); + assertNull(e.getCause().getCause().getCause()); + } + + e = assertThrows(CompletionException.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), new Callable() { @Override public Object call() throws Exception { throw new RuntimeException("Dummy RuntimeException"); @@ -196,24 +282,22 @@ public Object call() throws Exception { assertEquals("Dummy RuntimeException", e.getCause().getMessage()); assertNull(e.getCause().getCause()); - // wrap a CompletionException even the action throws a PrivilegedActionException e = assertThrows(CompletionException.class, () -> - SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + SubjectUtil.callAs(SubjectUtil.current(), new Callable() { @Override public Object call() throws Exception { - throw new PrivilegedActionException(null); + throw new CompletionException(null); } }) ); assertInstanceOf(CompletionException.class, e); - assertEquals("java.security.PrivilegedActionException", e.getMessage()); - assertInstanceOf(PrivilegedActionException.class, e.getCause()); + assertEquals("java.util.concurrent.CompletionException", e.getMessage()); + assertInstanceOf(CompletionException.class, e.getCause()); assertNull(e.getCause().getMessage()); - assertNull(e.getCause().getCause()); - // throw original error when action throw an error that is not the subclass of Exception + // throw original error when action throw an error e = assertThrows(LinkageError.class, () -> - SubjectUtil.callAs(SubjectUtil.current(), new Callable<>() { + SubjectUtil.callAs(SubjectUtil.current(), new Callable() { @Override public Object call() throws Exception { throw new LinkageError("Dummy LinkageError"); From 824800f376be1cbb562c8d5ef4b23ff16328a773 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Thu, 21 Aug 2025 13:39:30 +0800 Subject: [PATCH 09/12] nit --- .../hadoop/security/authentication/util/SubjectUtil.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index 4fe64566467f0..7a2876f33b03c 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -176,7 +176,7 @@ public static T callAs(Subject subject, Callable action) throws Completio * action throw a checked exception. * * @param subject the subject this action runs as - * @param action action the action to run + * @param action the action to run * @return the result of the action * @param the type of the result */ @@ -209,7 +209,7 @@ public static T doAs(Subject subject, PrivilegedAction action) { * Call Subject.doAs directly on older JVM. * * @param subject the subject this action runs as - * @param action action the action to run + * @param action the action to run * @return the result of the action * @param the type of the result * @throws PrivilegedActionException if {@code action.run()} throws an checked exception. From e92529b95c47f3c6ffad6725ea02a1b5f05e6155 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Thu, 21 Aug 2025 14:03:50 +0800 Subject: [PATCH 10/12] better exception process --- .../authentication/util/SubjectUtil.java | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index 7a2876f33b03c..9c2d0de5577e3 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -21,7 +21,6 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.invoke.MethodType; -import java.lang.reflect.UndeclaredThrowableException; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; @@ -223,17 +222,14 @@ public static T doAs( try { return callAs(subject, privilegedExceptionActionToCallable(action)); } catch (CompletionException ce) { - try { - Exception cause = (Exception) ce.getCause(); - if (cause instanceof RuntimeException) { - throw (RuntimeException) cause; - } else { - throw new PrivilegedActionException(cause); - } - } catch (ClassCastException castException) { - // This should never happen, as PrivilegedExceptionAction should not wrap - // non-checked exceptions - throw new PrivilegedActionException(new UndeclaredThrowableException(ce.getCause())); + Throwable cause = ce.getCause(); + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else if (cause instanceof Exception) { + throw new PrivilegedActionException((Exception) cause); + } else { + // This should never happen, CompletionException should only wraps an exception + throw sneakyThrow(cause); } } } else { From 9512cb2539173f641ca41b2f6ec97abbb1a76f06 Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Thu, 21 Aug 2025 14:25:40 +0800 Subject: [PATCH 11/12] throw NPE when action is NULL --- .../security/authentication/util/SubjectUtil.java | 7 +++++++ .../authentication/util/TestSubjectUtil.java | 15 +++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index 9c2d0de5577e3..84a57883d9f50 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -24,6 +24,7 @@ import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.concurrent.CompletionException; @@ -144,12 +145,14 @@ private static MethodHandle lookupGetContext() { * @param action the action to run * @return the result of the action * @param the type of the result + * @throws NullPointerException if action is null * @throws CompletionException if {@code action.call()} throws an exception. * The cause of the {@code CompletionException} is set to the exception * thrown by {@code action.call()}. */ @SuppressWarnings("unchecked") public static T callAs(Subject subject, Callable action) throws CompletionException { + Objects.requireNonNull(action); if (HAS_CALL_AS) { try { return (T) CALL_AS.invoke(subject, action); @@ -178,9 +181,11 @@ public static T callAs(Subject subject, Callable action) throws Completio * @param action the action to run * @return the result of the action * @param the type of the result + * @throws NullPointerException if action is null */ @SuppressWarnings("unchecked") public static T doAs(Subject subject, PrivilegedAction action) { + Objects.requireNonNull(action); if (HAS_CALL_AS) { try { return callAs(subject, privilegedActionToCallable(action)); @@ -211,6 +216,7 @@ public static T doAs(Subject subject, PrivilegedAction action) { * @param action the action to run * @return the result of the action * @param the type of the result + * @throws NullPointerException if action is null * @throws PrivilegedActionException if {@code action.run()} throws an checked exception. * The cause of the {@code PrivilegedActionException} is set to the exception thrown * by {@code action.run()}. @@ -218,6 +224,7 @@ public static T doAs(Subject subject, PrivilegedAction action) { @SuppressWarnings("unchecked") public static T doAs( Subject subject, PrivilegedExceptionAction action) throws PrivilegedActionException { + Objects.requireNonNull(action); if (HAS_CALL_AS) { try { return callAs(subject, privilegedExceptionActionToCallable(action)); diff --git a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java index 8a5a5bfdd8195..14ffaae896a07 100644 --- a/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/test/java/org/apache/hadoop/security/authentication/util/TestSubjectUtil.java @@ -130,6 +130,11 @@ public Object run() { assertInstanceOf(LinkageError.class, e); assertEquals("Dummy LinkageError", e.getMessage()); assertNull(e.getCause()); + + // throw NPE when action is NULL + assertThrows(NullPointerException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), (PrivilegedAction) null) + ); } @Test @@ -206,6 +211,11 @@ public Object run() throws Exception { assertInstanceOf(LinkageError.class, e); assertEquals("Dummy LinkageError", e.getMessage()); assertNull(e.getCause()); + + // throw NPE when action is NULL + assertThrows(NullPointerException.class, () -> + SubjectUtil.doAs(SubjectUtil.current(), (PrivilegedExceptionAction) null) + ); } @Test @@ -307,6 +317,11 @@ public Object call() throws Exception { assertInstanceOf(LinkageError.class, e); assertEquals("Dummy LinkageError", e.getMessage()); assertNull(e.getCause()); + + // throw NPE when action is NULL + assertThrows(NullPointerException.class, () -> + SubjectUtil.callAs(SubjectUtil.current(), null) + ); } @Test From 917baf07f148f4ba27eda4b5317965808d2a0a4c Mon Sep 17 00:00:00 2001 From: Cheng Pan Date: Mon, 25 Aug 2025 14:20:09 +0800 Subject: [PATCH 12/12] Udpate Javadoc for SubjectUtil --- .../hadoop/security/authentication/util/SubjectUtil.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java index 84a57883d9f50..faf2d6c7d8131 100644 --- a/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java +++ b/hadoop-common-project/hadoop-auth/src/main/java/org/apache/hadoop/security/authentication/util/SubjectUtil.java @@ -33,7 +33,7 @@ import org.apache.hadoop.classification.InterfaceAudience.Private; /** - * An utility class that adapt the Security Manager and APIs related to it for + * An utility class that adapts the Security Manager and APIs related to it for * JDK 8 and above. *

* In JDK 17, the Security Manager and APIs related to it have been deprecated @@ -43,6 +43,9 @@ *

* In JDK 24, the Security Manager has been permanently disabled. See * JEP 486 for more information. + *

+ * This is derived from Apache Calcite Avatica, which is derived from the Jetty + * implementation. */ @Private public final class SubjectUtil {