8277489: Rewrite JAAS UnixLoginModule with FFM

Co-authored-by: Martin Doerr <mdoerr@openjdk.org>
Reviewed-by: mdoerr, ascarpino, erikj
This commit is contained in:
Weijun Wang 2026-01-22 12:16:09 +00:00
parent 0d1d4d07b9
commit eda15aa19c
6 changed files with 220 additions and 165 deletions

View File

@ -31,6 +31,7 @@ include LibCommon.gmk
## Build libjaas
################################################################################
ifeq ($(call isTargetOs, windows), true)
$(eval $(call SetupJdkLibrary, BUILD_LIBJAAS, \
NAME := jaas, \
OPTIMIZATION := LOW, \
@ -39,5 +40,5 @@ $(eval $(call SetupJdkLibrary, BUILD_LIBJAAS, \
))
TARGETS += $(BUILD_LIBJAAS)
endif
################################################################################

View File

@ -274,7 +274,8 @@ module java.base {
jdk.httpserver,
jdk.jlink,
jdk.jpackage,
jdk.net;
jdk.net,
jdk.security.auth;
exports sun.net to
java.net.http,
jdk.naming.dns;

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2000, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,7 +26,6 @@
package com.sun.security.auth.module;
import java.util.*;
import java.io.IOException;
import javax.security.auth.*;
import javax.security.auth.callback.*;
import javax.security.auth.login.*;
@ -34,6 +33,7 @@ import javax.security.auth.spi.*;
import com.sun.security.auth.UnixPrincipal;
import com.sun.security.auth.UnixNumericUserPrincipal;
import com.sun.security.auth.UnixNumericGroupPrincipal;
import jdk.internal.util.OperatingSystem;
/**
* This {@code LoginModule} imports a user's Unix
@ -121,20 +121,34 @@ public class UnixLoginModule implements LoginModule {
*/
public boolean login() throws LoginException {
long[] unixGroups = null;
try {
ss = new UnixSystem();
} catch (UnsatisfiedLinkError ule) {
succeeded = false;
// Fail immediately on Windows to avoid cygwin-like functions
// being loaded, which are not supported.
if (OperatingSystem.isWindows()) {
throw new FailedLoginException
("Failed in attempt to import " +
"the underlying system identity information" +
" on " + System.getProperty("os.name"));
}
try {
ss = new UnixSystem();
} catch (ExceptionInInitializerError | UnsatisfiedLinkError ule) {
// Errors could happen in either static blocks or the constructor,
// both have a cause.
succeeded = false;
var error = new FailedLoginException
("Failed in attempt to import " +
"the underlying system identity information" +
" on " + System.getProperty("os.name"));
if (ule.getCause() != null) {
error.initCause(ule.getCause());
}
throw error;
}
userPrincipal = new UnixPrincipal(ss.getUsername());
UIDPrincipal = new UnixNumericUserPrincipal(ss.getUid());
GIDPrincipal = new UnixNumericGroupPrincipal(ss.getGid(), true);
long[] unixGroups = null;
if (ss.getGroups() != null && ss.getGroups().length > 0) {
unixGroups = ss.getGroups();
for (int i = 0; i < unixGroups.length; i++) {
@ -150,11 +164,13 @@ public class UnixLoginModule implements LoginModule {
"succeeded importing info: ");
System.out.println("\t\t\tuid = " + ss.getUid());
System.out.println("\t\t\tgid = " + ss.getGid());
unixGroups = ss.getGroups();
System.out.println("\t\t\tusername = " + ss.getUsername());
if (unixGroups != null) {
for (int i = 0; i < unixGroups.length; i++) {
System.out.println("\t\t\tsupp gid = " + unixGroups[i]);
}
}
}
succeeded = true;
return true;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2000, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2000, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,38 +25,194 @@
package com.sun.security.auth.module;
import jdk.internal.util.Architecture;
import jdk.internal.util.OperatingSystem;
import java.lang.foreign.AddressLayout;
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.GroupLayout;
import java.lang.foreign.Linker;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.StructLayout;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.VarHandle;
import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
/**
* This class implementation retrieves and makes available Unix
* UID/GID/groups information for the current user.
*
* @since 1.4
*/
@SuppressWarnings("restricted")
public class UnixSystem {
private native void getUnixInfo();
// Warning: the following 4 fields are used by Unix.c
/** The current username. */
/**
* The current username.
*/
protected String username;
/** The current user ID. */
/**
* The current user ID.
*/
protected long uid;
/** The current group ID. */
/**
* The current group ID.
*/
protected long gid;
/** The current list of groups. */
/**
* The current list of groups.
*/
protected long[] groups;
private static final Linker LINKER = Linker.nativeLinker();
private static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.loaderLookup()
.or(LINKER.defaultLookup());
private static final ValueLayout.OfByte C_CHAR
= (ValueLayout.OfByte) LINKER.canonicalLayouts().get("char");
private static final ValueLayout.OfInt C_INT
= (ValueLayout.OfInt) LINKER.canonicalLayouts().get("int");
private static final ValueLayout.OfLong C_LONG
= (ValueLayout.OfLong) LINKER.canonicalLayouts().get("long");
private static final AddressLayout C_POINTER
= ((AddressLayout) LINKER.canonicalLayouts().get("void*"))
.withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
private static final ValueLayout C_SIZE_T
= (ValueLayout) LINKER.canonicalLayouts().get("size_t");
private static final StructLayout CAPTURE_STATE_LAYOUT
= Linker.Option.captureStateLayout();
private static final VarHandle VH_errno = CAPTURE_STATE_LAYOUT.varHandle(
MemoryLayout.PathElement.groupElement("errno"));
private static final MethodHandle MH_strerror
= LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow("strerror"),
FunctionDescriptor.of(C_POINTER, C_INT));
private static final MethodHandle MH_getgroups
= LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow("getgroups"),
FunctionDescriptor.of(C_INT, C_INT, C_POINTER),
Linker.Option.captureCallState("errno"));
private static final MethodHandle MH_getuid
= LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow("getuid"),
FunctionDescriptor.of(C_INT));
// Some architectures require appropriate zero or sign extension to 64 bit.
// Use long directly before https://bugs.openjdk.org/browse/JDK-8336664 is resolved.
private static final boolean calling_convention_requires_int_as_long
= Architecture.isPPC64() || Architecture.isPPC64LE() || Architecture.isS390();
// getpwuid_r does not work on AIX, instead we use another similar function
// extern int _posix_getpwuid_r(uid_t, struct passwd *, char *, int, struct passwd **)
private static final MethodHandle MH_getpwuid_r
= LINKER.downcallHandle(SYMBOL_LOOKUP.findOrThrow(
OperatingSystem.isAix() ? "_posix_getpwuid_r" : "getpwuid_r"),
FunctionDescriptor.of(C_INT,
calling_convention_requires_int_as_long ? C_LONG : C_INT,
C_POINTER, C_POINTER,
OperatingSystem.isAix() ? C_INT : C_SIZE_T,
C_POINTER));
private static final GroupLayout ML_passwd = MemoryLayout.structLayout(
C_POINTER.withName("pw_name"),
C_POINTER.withName("pw_passwd"),
C_INT.withName("pw_uid"),
C_INT.withName("pw_gid"),
// Different platforms have different fields in `struct passwd`.
// While we don't need those fields here, the struct needs to be
// big enough to avoid buffer overflow when `getpwuid_r` is called.
MemoryLayout.paddingLayout(100));
private static final VarHandle VH_pw_uid
= ML_passwd.varHandle(groupElement("pw_uid"));
private static final VarHandle VH_pw_gid
= ML_passwd.varHandle(groupElement("pw_gid"));
private static final VarHandle VH_pw_name
= ML_passwd.varHandle(groupElement("pw_name"));
// The buffer size for the getpwuid_r function:
// 1. sysconf(_SC_GETPW_R_SIZE_MAX) on macOS is 4096 and 1024 on Linux,
// so we choose a bigger one.
// 2. We do not call sysconf() here because even _SC_GETPW_R_SIZE_MAX
// could be different on different platforms.
// 3. We choose int instead of long because the buffer_size argument
// might be `int` or `long` and converting from `long` to `int`
// requires an explicit cast.
private static final int GETPW_R_SIZE_MAX = 4096;
/**
* Instantiate a {@code UnixSystem} and load
* the native library to access the underlying system information.
*/
@SuppressWarnings("restricted")
public UnixSystem() {
System.loadLibrary("jaas");
getUnixInfo();
// The FFM code has only been tested on multiple platforms
// (including macOS, Linux, AIX, etc) and might fail on other
// *nix systems. Especially, the `passwd` struct could be defined
// differently. I've checked several and an extra 100 chars at the
// end seems enough.
try (Arena scope = Arena.ofConfined()) {
MemorySegment capturedState = scope.allocate(CAPTURE_STATE_LAYOUT);
int groupnum = (int) MH_getgroups.invokeExact(capturedState, 0, MemorySegment.NULL);
if (groupnum == -1) {
throw new RuntimeException("getgroups returns " + groupnum);
}
var gs = scope.allocate(C_INT, groupnum);
groupnum = (int) MH_getgroups.invokeExact(capturedState, groupnum, gs);
if (groupnum == -1) {
var errno = (int) VH_errno.get(capturedState, 0L);
var errMsg = (MemorySegment) MH_strerror.invokeExact(errno);
throw new RuntimeException("getgroups returns " + groupnum
+ ". Reason: " + errMsg.reinterpret(Long.MAX_VALUE).getString(0));
}
groups = new long[groupnum];
for (int i = 0; i < groupnum; i++) {
groups[i] = Integer.toUnsignedLong(gs.getAtIndex(C_INT, i));
}
var pwd = scope.allocate(ML_passwd);
var result = scope.allocate(C_POINTER);
var buffer = scope.allocate(GETPW_R_SIZE_MAX);
long tmpUid = Integer.toUnsignedLong((int) MH_getuid.invokeExact());
// Do not call invokeExact because the type of buffer_size is not
// always long in the underlying system.
int out;
if (calling_convention_requires_int_as_long) {
out = (int) MH_getpwuid_r.invoke(
tmpUid, pwd, buffer, GETPW_R_SIZE_MAX, result);
} else {
out = (int) MH_getpwuid_r.invoke(
(int) tmpUid, pwd, buffer, GETPW_R_SIZE_MAX, result);
}
if (out != 0) {
// If ERANGE (Result too large) is detected in a new platform,
// consider adjusting GETPW_R_SIZE_MAX.
var err = (MemorySegment) MH_strerror.invokeExact(out);
throw new RuntimeException(err.reinterpret(Long.MAX_VALUE).getString(0));
} else if (result.get(ValueLayout.ADDRESS, 0).equals(MemorySegment.NULL)) {
throw new RuntimeException("the requested entry is not found");
} else {
// uid_t and gid_t were defined unsigned.
uid = Integer.toUnsignedLong((int) VH_pw_uid.get(pwd, 0L));
gid = Integer.toUnsignedLong((int) VH_pw_gid.get(pwd, 0L));
username = ((MemorySegment) VH_pw_name.get(pwd, 0L)).getString(0);
}
} catch (Throwable t) {
var error = new UnsatisfiedLinkError("FFM calls failed");
error.initCause(t);
throw error;
}
}
/**

View File

@ -1,130 +0,0 @@
/*
* Copyright (c) 2000, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
#include <jni.h>
#include "jni_util.h"
#include "com_sun_security_auth_module_UnixSystem.h"
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pwd.h>
/*
* Declare library specific JNI_Onload entry if static build
*/
DEF_STATIC_JNI_OnLoad
JNIEXPORT void JNICALL
Java_com_sun_security_auth_module_UnixSystem_getUnixInfo
(JNIEnv *env, jobject obj) {
int i;
char pwd_buf[1024];
struct passwd *pwd = NULL;
struct passwd resbuf;
jfieldID userNameID;
jfieldID userID;
jfieldID groupID;
jfieldID supplementaryGroupID;
jstring jstr;
jlongArray jgroups;
jlong *jgroupsAsArray;
jsize numSuppGroups;
gid_t *groups;
jclass cls;
numSuppGroups = getgroups(0, NULL);
if (numSuppGroups == -1) {
return;
}
groups = (gid_t *)calloc(numSuppGroups, sizeof(gid_t));
if (groups == NULL) {
jclass cls = (*env)->FindClass(env,"java/lang/OutOfMemoryError");
if (cls != NULL) {
(*env)->ThrowNew(env, cls, NULL);
}
return;
}
cls = (*env)->GetObjectClass(env, obj);
supplementaryGroupID = (*env)->GetFieldID(env, cls, "groups", "[J");
if (supplementaryGroupID == 0) {
goto cleanUpAndReturn;
}
if (getgroups(numSuppGroups, groups) != -1) {
jgroups = (*env)->NewLongArray(env, numSuppGroups);
if (jgroups == NULL) {
goto cleanUpAndReturn;
}
jgroupsAsArray = (*env)->GetLongArrayElements(env, jgroups, 0);
if (jgroupsAsArray == NULL) {
goto cleanUpAndReturn;
}
for (i = 0; i < numSuppGroups; i++) {
jgroupsAsArray[i] = groups[i];
}
(*env)->ReleaseLongArrayElements(env, jgroups, jgroupsAsArray, 0);
(*env)->SetObjectField(env, obj, supplementaryGroupID, jgroups);
}
userNameID = (*env)->GetFieldID(env, cls, "username", "Ljava/lang/String;");
if (userNameID == 0) {
goto cleanUpAndReturn;
}
userID = (*env)->GetFieldID(env, cls, "uid", "J");
if (userID == 0) {
goto cleanUpAndReturn;
}
groupID = (*env)->GetFieldID(env, cls, "gid", "J");
if (groupID == 0) {
goto cleanUpAndReturn;
}
memset(pwd_buf, 0, sizeof(pwd_buf));
if (getpwuid_r(getuid(), &resbuf, pwd_buf, sizeof(pwd_buf), &pwd) == 0 &&
pwd != NULL) {
(*env)->SetLongField(env, obj, userID, pwd->pw_uid);
(*env)->SetLongField(env, obj, groupID, pwd->pw_gid);
jstr = (*env)->NewStringUTF(env, pwd->pw_name);
if (jstr == NULL) {
goto cleanUpAndReturn;
}
(*env)->SetObjectField(env, obj, userNameID, jstr);
} else {
(*env)->SetLongField(env, obj, userID, getuid());
(*env)->SetLongField(env, obj, groupID, getgid());
}
cleanUpAndReturn:
free(groups);
return;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2014, 2022, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,8 +25,11 @@
* @test
* @bug 8039951
* @summary com.sun.security.auth.module missing classes on some platforms
* @modules java.base/jdk.internal.util
* @run main/othervm AllPlatforms
*/
import jdk.internal.util.OperatingSystem;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
import java.nio.file.Files;
@ -39,14 +42,14 @@ public class AllPlatforms {
private static final String NT_MODULE = "NTLoginModule";
public static void main(String[] args) throws Exception {
login("cross-platform",
login(true, "cross-platform",
UNIX_MODULE, "optional",
NT_MODULE, "optional");
login("windows", NT_MODULE, "required");
login("unix", UNIX_MODULE, "required");
login(OperatingSystem.isWindows(), "windows", NT_MODULE, "required");
login(!OperatingSystem.isWindows(), "unix", UNIX_MODULE, "required");
}
static void login(String test, String... conf) throws Exception {
static void login(boolean shouldSucceed, String test, String... conf) throws Exception {
System.out.println("Testing " + test + "...");
StringBuilder sb = new StringBuilder();
@ -68,11 +71,19 @@ public class AllPlatforms {
lc.login();
System.out.println(lc.getSubject());
lc.logout();
if (!shouldSucceed) {
throw new RuntimeException("Should not succeed");
}
} catch (FailedLoginException e) {
if (shouldSucceed) {
throw new RuntimeException("Should succeed");
}
// This exception can occur in other platform module than the running one.
if (e.getMessage().startsWith("Failed in attempt to import")) {
System.out.println("Expected Exception found.");
e.printStackTrace(System.out);
} else {
throw new RuntimeException("Unexpected error", e);
}
}
}