You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kyuubi.apache.org by ch...@apache.org on 2026/07/10 07:05:09 UTC

(kyuubi) branch master updated: [KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility

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

pan3793 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new fe81544f64 [KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility
fe81544f64 is described below

commit fe81544f64a495e4092933a487e96e8f8936b49a
Author: Fei Wang <fw...@ebay.com>
AuthorDate: Fri Jul 10 15:04:59 2026 +0800

    [KYUUBI #7549] Fix Subject.getSubject() in Kyuubi JDBC driver for JDK 25 compatibility
    
    ## Why are the changes needed?
    
    JDK 24 permanently disabled the Security Manager (JEP 486), making `AccessController.getContext()` throw `UnsupportedOperationException`. JDK 25 extended this to `Subject.getSubject()`. These APIs are called in the Kyuubi JDBC driver during Kerberos authentication, causing connections to fail:
    
    ```
    java.lang.UnsupportedOperationException: getSubject is not supported
        at java.base/javax.security.auth.Subject.getSubject(Subject.java)
    ```
    
    Additionally, `Subject.doAs()` is deprecated for removal (JEP 411) and will break in a future JDK release.
    
    Fixes #7549.
    
    ## What changed
    
    Added `SubjectUtil` to `kyuubi-hive-jdbc`, ported from Apache Hadoop (HADOOP-19212, HADOOP-19906). It bridges the deprecated/removed Security Manager APIs to their JDK 18+ replacements using `MethodHandle` lookups resolved once at class load time:
    
    - `Subject.getSubject(AccessController.getContext())` → `Subject.current()` (JDK 18+)
    - `Subject.doAs()` → `Subject.callAs()` (JDK 18+)
    - On JDK 22+, maintains an `InheritableThreadLocal<Subject>` to restore pre-JDK 22 Subject propagation into newly constructed threads (HADOOP-19906)
    - Falls back to the old APIs on JDK < 18 transparently
    
    Updated all affected callsites in the JDBC driver:
    
    | File | Change |
    |------|--------|
    | `KyuubiConnection.java` | `Subject.getSubject()` × 2 → `SubjectUtil.current()` |
    | `HttpAuthUtils.java` | `Subject.doAs()` → `SubjectUtil.doAs()` |
    | `TSubjectTransport.java` | `Subject.doAs()` → `SubjectUtil.doAs()` |
    
    ## How was this patch tested?
    
    Ported `TestSubjectUtil` from Apache Hadoop, covering exception propagation semantics of `callAs()`, `doAs(PrivilegedAction)`, `doAs(PrivilegedExceptionAction)`, and `sneakyThrow()` across JDK versions. Full `kyuubi-hive-jdbc` unit suite (43 tests) passes.
    
    ## Was this patch authored or co-authored using generative AI tooling?
    
    Assisted-by: Claude:claude-sonnet-4-6
    
    Closes #7550 from turboFei/jdk25.
    
    Closes #7549
    
    5baf2c974 [Fei Wang] refactor: move SubjectUtil to kyuubi-util; fix Javadoc and Kyuubi-managed comments
    17d810697 [Fei Wang] test: replace SubjectUtilTest with full Hadoop TestSubjectUtil port
    c01508255 [Fei Wang] fix: replace Subject.doAs() with SubjectUtil.doAs() in HttpAuthUtils and TSubjectTransport
    727aa53d5 [Fei Wang] feat: replace SubjectUtil with full Hadoop implementation for JDK 25
    fbfb88689 [Fei Wang] chore: remove docs/superpowers
    27a02e8b2 [Fei Wang] style: reformat after JDK 25 Subject fix
    492273c67 [Fei Wang] fix: replace Subject.getSubject() with SubjectUtil.current() for JDK 25 compat
    78ba2f172 [Fei Wang] feat: add SubjectUtil to bridge Subject.getSubject() for JDK 25
    fe909f976 [Fei Wang] docs: add JDK 25 Subject.getSubject fix implementation plan
    23864a37e [Fei Wang] docs: add JDK 25 Subject.getSubject fix design spec
    
    Authored-by: Fei Wang <fw...@ebay.com>
    Signed-off-by: Cheng Pan <ch...@apache.org>
---
 .../apache/kyuubi/jdbc/hive/KyuubiConnection.java  |   6 +-
 .../kyuubi/jdbc/hive/auth/HttpAuthUtils.java       |   3 +-
 .../kyuubi/jdbc/hive/auth/TSubjectTransport.java   |   3 +-
 .../java/org/apache/kyuubi/util/SubjectUtil.java   | 303 ++++++++++++++++
 .../org/apache/kyuubi/util/SubjectUtilTest.java    | 394 +++++++++++++++++++++
 5 files changed, 704 insertions(+), 5 deletions(-)

diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
index dc2b7afee3..50df116b02 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiConnection.java
@@ -71,6 +71,7 @@ import org.apache.kyuubi.shaded.thrift.protocol.TBinaryProtocol;
 import org.apache.kyuubi.shaded.thrift.transport.THttpClient;
 import org.apache.kyuubi.shaded.thrift.transport.TTransport;
 import org.apache.kyuubi.shaded.thrift.transport.TTransportException;
+import org.apache.kyuubi.util.SubjectUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -922,7 +923,7 @@ public class KyuubiConnection implements SQLConnection, KyuubiLoggable {
       @SuppressWarnings("unchecked")
       Class<? extends Principal> HadoopUserClz =
           (Class<? extends Principal>) ClassUtils.getClass("org.apache.hadoop.security.User");
-      Subject subject = Subject.getSubject(AccessController.getContext());
+      Subject subject = SubjectUtil.current();
       return subject != null && !subject.getPrincipals(HadoopUserClz).isEmpty();
     } catch (ClassNotFoundException e) {
       return false;
@@ -1019,8 +1020,7 @@ public class KyuubiConnection implements SQLConnection, KyuubiLoggable {
       String keytab = sessConfMap.get(AUTH_KYUUBI_CLIENT_KEYTAB);
       return KerberosAuthenticationManager.getKeytabAuthentication(principal, keytab).getSubject();
     } else if (isFromSubjectAuthMode()) {
-      AccessControlContext context = AccessController.getContext();
-      return Subject.getSubject(context);
+      return SubjectUtil.current();
     } else if (isTgtCacheAuthMode()) {
       String ticketCache = sessConfMap.getOrDefault(AUTH_KYUUBI_CLIENT_TICKET_CACHE, "");
       return KerberosAuthenticationManager.getTgtCacheAuthentication(ticketCache).getSubject();
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
index 0802e8effd..368bc9dadd 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/HttpAuthUtils.java
@@ -20,6 +20,7 @@ package org.apache.kyuubi.jdbc.hive.auth;
 import java.security.PrivilegedExceptionAction;
 import java.util.Base64;
 import javax.security.auth.Subject;
+import org.apache.kyuubi.util.SubjectUtil;
 import org.ietf.jgss.GSSContext;
 import org.ietf.jgss.GSSManager;
 import org.ietf.jgss.GSSName;
@@ -35,7 +36,7 @@ public final class HttpAuthUtils {
   public static String getKerberosServiceTicket(
       String serverPrinciple, String host, Subject loggedInSubject) throws Exception {
     String spn = KerberosUtils.canonicalPrincipal(serverPrinciple, host);
-    return Subject.doAs(loggedInSubject, new HttpKerberosClientAction(spn));
+    return SubjectUtil.doAs(loggedInSubject, new HttpKerberosClientAction(spn));
   }
 
   private HttpAuthUtils() {
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
index be7e581eef..e72f5f6c2f 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/auth/TSubjectTransport.java
@@ -22,6 +22,7 @@ import java.security.PrivilegedExceptionAction;
 import javax.security.auth.Subject;
 import org.apache.kyuubi.shaded.thrift.transport.TTransport;
 import org.apache.kyuubi.shaded.thrift.transport.TTransportException;
+import org.apache.kyuubi.util.SubjectUtil;
 
 /**
  * This is used on the client side, where the API explicitly opens transport to the server using the
@@ -39,7 +40,7 @@ public class TSubjectTransport extends TFilterTransport {
   @Override
   public void open() throws TTransportException {
     try {
-      Subject.doAs(
+      SubjectUtil.doAs(
           subject,
           (PrivilegedExceptionAction<Void>)
               () -> {
diff --git a/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java b/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java
new file mode 100644
index 0000000000..e29359c317
--- /dev/null
+++ b/kyuubi-util/src/main/java/org/apache/kyuubi/util/SubjectUtil.java
@@ -0,0 +1,303 @@
+/*
+ * 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.kyuubi.util;
+
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+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;
+import javax.security.auth.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Utility class that adapts Security Manager APIs across JDK versions (8 through 25+).
+ *
+ * <p>In JDK 17 the Security Manager and related APIs were deprecated for removal (JEP 411). In JDK
+ * 24 the Security Manager was permanently disabled (JEP 486). This class bridges {@code
+ * Subject.getSubject()}, {@code Subject.doAs()}, and {@code AccessController.getContext()} to their
+ * JDK 18+ replacements ({@code Subject.current()} and {@code Subject.callAs()}) using {@code
+ * MethodHandle} lookups resolved once at class load time.
+ *
+ * <p>Ported from Apache Hadoop 3.5.1 ({@code
+ * org.apache.hadoop.security.authentication.util.SubjectUtil}), HADOOP-19212 and HADOOP-19906,
+ * which is itself derived from Apache Calcite Avatica and the Jetty implementation.
+ */
+public final class SubjectUtil {
+  private static final Logger LOG = LoggerFactory.getLogger(SubjectUtil.class);
+
+  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();
+
+  // "1.8" -> 8, "9" -> 9, "17" -> 17, etc.
+  private static final int JAVA_SPEC_VER =
+      Math.max(
+          8, Integer.parseInt(System.getProperty("java.specification.version").split("\\.")[0]));
+
+  /** True if the current JVM copies the current JAAS subject into new threads automatically. */
+  public static final boolean THREAD_INHERITS_SUBJECT = checkThreadInheritsSubject();
+
+  /**
+   * Kyuubi-managed {@link InheritableThreadLocal} mirroring the active Subject for the duration of
+   * {@link #callAs} / {@link #doAs}. On JDK 22+ the JVM stopped propagating the Subject to newly
+   * constructed threads via {@code AccessControlContext} (JEP 411 / 486), and the new {@code
+   * ScopedValue}-based mechanism behind {@code Subject.current()} also does not inherit across
+   * {@code Thread.<init>}. Only consulted when {@link #THREAD_INHERITS_SUBJECT} is {@code false}.
+   */
+  private static final InheritableThreadLocal<Subject> CURRENT_SUBJECT_TL =
+      THREAD_INHERITS_SUBJECT ? null : new InheritableThreadLocal<>();
+
+  private static MethodHandle lookupCallAs() {
+    try {
+      return MethodHandles.lookup()
+          .findStatic(
+              Subject.class,
+              "callAs",
+              MethodType.methodType(Object.class, Subject.class, Callable.class));
+    } catch (NoSuchMethodException x) {
+      return null;
+    } catch (IllegalAccessException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private static boolean checkThreadInheritsSubject() {
+    // JDK 22+ stopped propagating Subject to new threads via AccessControlContext.
+    // We accept the minor overhead for EOL 22/23 to avoid SecurityManager console warnings.
+    return JAVA_SPEC_VER <= 21;
+  }
+
+  private static MethodHandle lookupDoAs() {
+    try {
+      return MethodHandles.lookup()
+          .findStatic(
+              Subject.class,
+              "doAs",
+              MethodType.methodType(Object.class, Subject.class, PrivilegedAction.class));
+    } catch (IllegalAccessException | NoSuchMethodException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private static MethodHandle lookupDoAsThrowException() {
+    try {
+      return MethodHandles.lookup()
+          .findStatic(
+              Subject.class,
+              "doAs",
+              MethodType.methodType(Object.class, Subject.class, PrivilegedExceptionAction.class));
+    } catch (IllegalAccessException | NoSuchMethodException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private static MethodHandle lookupCurrent() {
+    try {
+      return MethodHandles.lookup()
+          .findStatic(Subject.class, "current", MethodType.methodType(Subject.class));
+    } catch (NoSuchMethodException e) {
+      // JDK < 18: compose AccessController.getContext() -> Subject.getSubject()
+      MethodHandle getContext = lookupGetContext();
+      MethodHandle getSubject = lookupGetSubject();
+      return MethodHandles.filterReturnValue(getContext, getSubject);
+    } catch (IllegalAccessException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private static MethodHandle lookupGetSubject() {
+    try {
+      Class<?> contextKlass =
+          ClassLoader.getSystemClassLoader().loadClass("java.security.AccessControlContext");
+      return MethodHandles.lookup()
+          .findStatic(
+              Subject.class, "getSubject", MethodType.methodType(Subject.class, contextKlass));
+    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  private static MethodHandle lookupGetContext() {
+    try {
+      Class<?> controllerKlass =
+          ClassLoader.getSystemClassLoader().loadClass("java.security.AccessController");
+      Class<?> contextKlass =
+          ClassLoader.getSystemClassLoader().loadClass("java.security.AccessControlContext");
+      return MethodHandles.lookup()
+          .findStatic(controllerKlass, "getContext", MethodType.methodType(contextKlass));
+    } catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException e) {
+      throw new ExceptionInInitializerError(e);
+    }
+  }
+
+  /**
+   * Runs {@code action} as {@code subject}, bridging to {@code Subject.callAs()} on JDK 18+ and
+   * falling back to {@code Subject.doAs()} on older JDKs. On JDK 22+ also maintains {@link
+   * #CURRENT_SUBJECT_TL} so platform threads constructed inside {@code action} inherit the subject.
+   *
+   * @throws CompletionException wrapping any exception thrown by {@code action}
+   */
+  public static <T> T callAs(Subject subject, Callable<T> action) throws CompletionException {
+    Objects.requireNonNull(action);
+    if (THREAD_INHERITS_SUBJECT) {
+      return invokeCallAs(subject, action);
+    }
+    final Subject prev = CURRENT_SUBJECT_TL.get();
+    if (subject == null) {
+      CURRENT_SUBJECT_TL.remove();
+    } else if (subject != prev) {
+      CURRENT_SUBJECT_TL.set(subject);
+    }
+    try {
+      return invokeCallAs(subject, action);
+    } finally {
+      if (prev == null) {
+        CURRENT_SUBJECT_TL.remove();
+      } else if (prev != subject) {
+        CURRENT_SUBJECT_TL.set(prev);
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> T invokeCallAs(Subject subject, Callable<T> action)
+      throws CompletionException {
+    if (HAS_CALL_AS) {
+      try {
+        return (T) CALL_AS.invoke(subject, action);
+      } catch (Throwable t) {
+        throw sneakyThrow(t);
+      }
+    } else {
+      try {
+        return doAs(subject, callableToPrivilegedAction(action));
+      } catch (Exception e) {
+        throw new CompletionException(e);
+      }
+    }
+  }
+
+  /** Bridges to {@code Subject.callAs()} on JDK 18+ or {@code Subject.doAs()} on older JDKs. */
+  @SuppressWarnings("unchecked")
+  public static <T> T doAs(Subject subject, PrivilegedAction<T> action) {
+    Objects.requireNonNull(action);
+    if (HAS_CALL_AS) {
+      try {
+        return callAs(subject, privilegedActionToCallable(action));
+      } catch (CompletionException ce) {
+        Throwable cause = ce.getCause();
+        throw sneakyThrow(cause != null ? cause : ce);
+      }
+    } else {
+      try {
+        return (T) DO_AS.invoke(subject, action);
+      } catch (Throwable t) {
+        throw sneakyThrow(t);
+      }
+    }
+  }
+
+  /** Bridges to {@code Subject.callAs()} on JDK 18+ or {@code Subject.doAs()} on older JDKs. */
+  @SuppressWarnings("unchecked")
+  public static <T> T doAs(Subject subject, PrivilegedExceptionAction<T> action)
+      throws PrivilegedActionException {
+    Objects.requireNonNull(action);
+    if (HAS_CALL_AS) {
+      try {
+        return callAs(subject, privilegedExceptionActionToCallable(action));
+      } catch (CompletionException ce) {
+        Throwable cause = ce.getCause();
+        if (cause instanceof RuntimeException) {
+          throw (RuntimeException) cause;
+        } else if (cause instanceof Exception) {
+          throw new PrivilegedActionException((Exception) cause);
+        } else {
+          throw sneakyThrow(cause);
+        }
+      }
+    } else {
+      try {
+        return (T) DO_AS_THROW_EXCEPTION.invoke(subject, action);
+      } catch (Throwable t) {
+        throw sneakyThrow(t);
+      }
+    }
+  }
+
+  /**
+   * Returns the {@link Subject} associated with the current execution context, or {@code null} if
+   * there is none. On JDK 18+ delegates to {@code Subject.current()}; on older JDKs delegates to
+   * {@code Subject.getSubject(AccessController.getContext())}. On JDK 22+ also consults {@link
+   * #CURRENT_SUBJECT_TL} to restore subject visibility for inherited platform threads.
+   */
+  public static Subject current() {
+    if (!THREAD_INHERITS_SUBJECT) {
+      Subject fromJdk = invokeJdkCurrent();
+      if (fromJdk != null) {
+        LOG.trace("Get current Subject from JDK API directly");
+        return fromJdk;
+      }
+      LOG.trace("Get current Subject from Kyuubi-managed InheritableThreadLocal");
+      return CURRENT_SUBJECT_TL.get();
+    }
+    return invokeJdkCurrent();
+  }
+
+  private static Subject invokeJdkCurrent() {
+    try {
+      return (Subject) CURRENT.invoke();
+    } catch (Throwable t) {
+      throw sneakyThrow(t);
+    }
+  }
+
+  private static <T> PrivilegedAction<T> callableToPrivilegedAction(Callable<T> callable) {
+    return () -> {
+      try {
+        return callable.call();
+      } catch (Exception e) {
+        throw sneakyThrow(e);
+      }
+    };
+  }
+
+  private static <T> Callable<T> privilegedExceptionActionToCallable(
+      PrivilegedExceptionAction<T> action) {
+    return action::run;
+  }
+
+  private static <T> Callable<T> privilegedActionToCallable(PrivilegedAction<T> action) {
+    return action::run;
+  }
+
+  @SuppressWarnings("unchecked")
+  static <E extends Throwable> RuntimeException sneakyThrow(Throwable e) throws E {
+    throw (E) e;
+  }
+
+  private SubjectUtil() {}
+}
diff --git a/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java b/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java
new file mode 100644
index 0000000000..db23aa7346
--- /dev/null
+++ b/kyuubi-util/src/test/java/org/apache/kyuubi/util/SubjectUtilTest.java
@@ -0,0 +1,394 @@
+/*
+ * 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.kyuubi.util;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+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;
+import org.junit.jupiter.api.Test;
+
+public class SubjectUtilTest {
+
+  // "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() {
+    // 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<Object>() {
+                      @Override
+                      public Object run() {
+                        RuntimeException innerE =
+                            new RuntimeException("Inner Dummy RuntimeException");
+                        throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException", innerE));
+                      }
+                    }));
+    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<Object>() {
+                      @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 RuntimeException directly when action throws a runtime exception
+    e =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                SubjectUtil.doAs(
+                    SubjectUtil.current(),
+                    new PrivilegedAction<Object>() {
+                      @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<Object>() {
+                      @Override
+                      public Object run() {
+                        throw new CompletionException("Dummy CompletionException", null);
+                      }
+                    }));
+    assertInstanceOf(CompletionException.class, e);
+    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<Object>() {
+                      @Override
+                      public Object run() {
+                        throw new LinkageError("Dummy LinkageError");
+                      }
+                    }));
+    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<Object>) null));
+  }
+
+  @Test
+  void testDoAsPrivilegedExceptionActionExceptionPropagation() {
+    // throw PrivilegedActionException that wraps the original exception when action throws
+    // a checked exception
+    Throwable e =
+        assertThrows(
+            PrivilegedActionException.class,
+            () ->
+                SubjectUtil.doAs(
+                    SubjectUtil.current(),
+                    new PrivilegedExceptionAction<Object>() {
+                      @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());
+
+    // same as above because PrivilegedActionException is a checked exception
+    e =
+        assertThrows(
+            PrivilegedActionException.class,
+            () ->
+                SubjectUtil.doAs(
+                    SubjectUtil.current(),
+                    new PrivilegedExceptionAction<Object>() {
+                      @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 throws a runtime exception
+    e =
+        assertThrows(
+            RuntimeException.class,
+            () ->
+                SubjectUtil.doAs(
+                    SubjectUtil.current(),
+                    new PrivilegedExceptionAction<Object>() {
+                      @Override
+                      public Object run() throws Exception {
+                        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 PrivilegedExceptionAction<Object>() {
+                      @Override
+                      public Object run() throws Exception {
+                        throw new CompletionException(null);
+                      }
+                    }));
+    assertInstanceOf(CompletionException.class, e);
+    assertNull(e.getMessage());
+    assertNull(e.getCause());
+
+    // throw the original error when action throws an error
+    e =
+        assertThrows(
+            LinkageError.class,
+            () ->
+                SubjectUtil.doAs(
+                    SubjectUtil.current(),
+                    new PrivilegedExceptionAction<Object>() {
+                      @Override
+                      public Object run() throws Exception {
+                        throw new LinkageError("Dummy LinkageError");
+                      }
+                    }));
+    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<Object>) null));
+  }
+
+  @Test
+  void testCallAsExceptionPropagation() {
+    // always throw a CompletionException that wraps the original exception, when action throws
+    // a checked or runtime exception
+    Throwable e =
+        assertThrows(
+            CompletionException.class,
+            () ->
+                SubjectUtil.callAs(
+                    SubjectUtil.current(),
+                    new Callable<Object>() {
+                      @Override
+                      public Object call() throws Exception {
+                        RuntimeException innerE =
+                            new RuntimeException("Inner Dummy RuntimeException");
+                        throw new IOException("Dummy IOException", innerE);
+                      }
+                    }));
+    assertInstanceOf(CompletionException.class, e);
+    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());
+    }
+
+    e =
+        assertThrows(
+            CompletionException.class,
+            () ->
+                SubjectUtil.callAs(
+                    SubjectUtil.current(),
+                    new Callable<Object>() {
+                      @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<Object>() {
+                      @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());
+
+    e =
+        assertThrows(
+            CompletionException.class,
+            () ->
+                SubjectUtil.callAs(
+                    SubjectUtil.current(),
+                    new Callable<Object>() {
+                      @Override
+                      public Object call() throws Exception {
+                        throw new CompletionException(null);
+                      }
+                    }));
+    assertInstanceOf(CompletionException.class, e);
+    assertEquals("java.util.concurrent.CompletionException", e.getMessage());
+    assertInstanceOf(CompletionException.class, e.getCause());
+    assertNull(e.getCause().getMessage());
+
+    // throw original error when action throws an error
+    e =
+        assertThrows(
+            LinkageError.class,
+            () ->
+                SubjectUtil.callAs(
+                    SubjectUtil.current(),
+                    new Callable<Object>() {
+                      @Override
+                      public Object call() throws Exception {
+                        throw new LinkageError("Dummy LinkageError");
+                      }
+                    }));
+    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
+  void testSneakyThrow() {
+    IOException e = assertThrows(IOException.class, this::throwCheckedException);
+    assertEquals("Dummy IOException", e.getMessage());
+  }
+
+  private void throwCheckedException() {
+    throw SubjectUtil.sneakyThrow(new IOException("Dummy IOException"));
+  }
+}