pam-redbear: port minimal PAM to Redox; proxy to redbear-authd

This commit is contained in:
2026-06-09 10:37:32 +03:00
parent 385f32704a
commit 67c59641ff
6 changed files with 1054 additions and 400 deletions
+5 -3
View File
@@ -63,9 +63,10 @@ redox-drm = {}
mesa = {}
libdrm = {}
libepoxy = {}
libdisplay-info = "ignore"
libxcvt = "ignore"
lcms2 = "ignore"
libdisplay-info = {}
libxcvt = {}
lcms2 = {}
libudev = {}
freetype2 = {}
fontconfig = {}
@@ -153,6 +154,7 @@ redbear-authd = {}
redbear-session-launch = {}
seatd = {}
redbear-greeter = {}
pam-redbear = {}
sddm = {}
amdgpu = "ignore"
#amdgpu = {} # TODO: fix conflicting idr_* defs with linux-kpi headers
+32 -19
View File
@@ -1,11 +1,18 @@
# PAM compatibility shim — implements the PAM API by delegating authentication
# to redbear-authd via Unix socket JSON protocol. Provides libpam.so and
# security/pam_appl.h for SDDM and other PAM-consuming software.
# PAM compatibility library for Red Bear OS (v6.0 2026)
#
# Implements the C PAM ABI expected by SDDM and other PAM consumers and
# proxies authentication requests to redbear-authd over its Unix socket
# protocol. Built as a Rust `cdylib` (libpam.so.0) per the project's
# Rust-first policy for system-critical infrastructure.
[source]
path = "source"
[build]
template = "custom"
# redbear-authd is a runtime dependency (we talk to its Unix socket at
# authentication time). redbear-login-protocol is a Cargo path dependency
# declared in source/Cargo.toml — the cookbook must not try to install it
# as a separate recipe because it is a lib-only crate with no [[bin]].
dependencies = [
"redbear-authd",
]
@@ -14,25 +21,24 @@ DYNAMIC_INIT
mkdir -p "${COOKBOOK_STAGE}/usr/lib"
mkdir -p "${COOKBOOK_STAGE}/usr/include/security"
mkdir -p "${COOKBOOK_STAGE}/usr/lib/pkgconfig"
x86_64-unknown-redox-gcc \
-shared \
-fPIC \
-std=c11 \
-Wall \
-Wextra \
-Wl,-soname,libpam.so.0 \
-I"${COOKBOOK_SOURCE}" \
-o "${COOKBOOK_STAGE}/usr/lib/libpam.so.0.99.0" \
"${COOKBOOK_SOURCE}/pam_redbear.c"
reexport_flags
"${COOKBOOK_CARGO}" build \
--manifest-path "${COOKBOOK_SOURCE}/Cargo.toml" \
--lib \
${build_flags} \
-j "${COOKBOOK_MAKE_JOBS}" \
${COOKBOOK_CARGO_FLAGS[@]}
ln -sf libpam.so.0.99.0 "${COOKBOOK_STAGE}/usr/lib/libpam.so.0"
cp "${COOKBOOK_BUILD}/target/${TARGET}/release/libpam.so" \
"${COOKBOOK_STAGE}/usr/lib/libpam.so.0.2.3"
ln -sf libpam.so.0.2.3 "${COOKBOOK_STAGE}/usr/lib/libpam.so.0"
ln -sf libpam.so.0 "${COOKBOOK_STAGE}/usr/lib/libpam.so"
cp "${COOKBOOK_SOURCE}/security/pam_appl.h" "${COOKBOOK_STAGE}/usr/include/security/pam_appl.h"
cp "${COOKBOOK_SOURCE}/include/security/pam_appl.h" \
"${COOKBOOK_STAGE}/usr/include/security/pam_appl.h"
# pkg-config for downstream cmake/find_package lookups
mkdir -p "${COOKBOOK_STAGE}/usr/lib/pkgconfig"
cat > "${COOKBOOK_STAGE}/usr/lib/pkgconfig/pam.pc" << 'EOF'
prefix=/usr
exec_prefix=${prefix}
@@ -40,9 +46,16 @@ libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: pam
Description: PAM compatibility shim (redbear-authd backend)
Version: 0.99.0
Description: PAM compatibility library for Red Bear OS — proxies authentication to redbear-authd
Version: 0.2.3
Libs: -L${libdir} -lpam
Cflags: -I${includedir}
EOF
"""
[package]
version = "0.2.3"
# Files are installed by the script above; no [package.files] mapping
# needed for the cdylib itself. The version is bumped to 0.2.3 to match
# the in-tree package version and to make the SONAME upgrade visible
# to any existing consumers (SDDM, su, etc.).
@@ -0,0 +1,23 @@
[package]
name = "pam"
version = "0.2.3"
edition = "2021"
description = "PAM compatibility library for Red Bear OS — proxies authentication to redbear-authd over its Unix socket protocol. v6.0 2026"
license = "MIT"
[lib]
name = "pam"
crate-type = ["cdylib", "rlib"]
[features]
default = []
# Enable to talk to redbear-authd over the real /run/redbear-authd.sock.
# Disable for unit tests that exercise the API in isolation.
authd-socket = []
[dependencies]
redbear-login-protocol = { path = "../../../system/redbear-login-protocol/source" }
serde_json = "1"
[dev-dependencies]
serde_json = "1"
@@ -9,13 +9,13 @@ extern "C" {
typedef int pam_item_type;
#define PAM_SUCCESS 0
#define PAM_OPEN_ERR 1
#define PAM_SYMBOL_ERR 2
#define PAM_SERVICE_ERR 3
#define PAM_SYSTEM_ERR 4
#define PAM_BUF_ERR 5
#define PAM_PERM_DENIED 6
#define PAM_SUCCESS 0
#define PAM_OPEN_ERR 1
#define PAM_SYMBOL_ERR 2
#define PAM_SERVICE_ERR 3
#define PAM_SYSTEM_ERR 4
#define PAM_BUF_ERR 5
#define PAM_PERM_DENIED 6
#define PAM_AUTH_ERR 7
#define PAM_CRED_INSUFFICIENT 8
#define PAM_AUTHINFO_UNAVAIL 9
@@ -30,37 +30,45 @@ typedef int pam_item_type;
#define PAM_NO_MODULE_DATA 18
#define PAM_CONV_ERR 19
#define PAM_AUTHTOK_ERR 20
#define PAM_ABORT 21
#define PAM_AUTHTOK_RECOVER_ERR 22
#define PAM_AUTHTOK_LOCK_BUSY 23
#define PAM_AUTHTOK_DISABLE_AGING 24
#define PAM_TRY_AGAIN 25
#define PAM_IGNORE 26
#define PAM_MODULE_UNKNOWN 27
#define PAM_AUTHTOK_EXPIRED 28
#define PAM_AUTHTOK_RECOVER_ERR 21
#define PAM_AUTHTOK_LOCK_BUSY 22
#define PAM_AUTHTOK_DISABLE_AGING 23
#define PAM_TRY_AGAIN 24
#define PAM_IGNORE 25
#define PAM_ABORT 26
#define PAM_AUTHTOK_EXPIRED 27
#define PAM_BAD_ITEM 29
#define PAM_BAD_STATE 30
#define PAM_NO_MODULE_DATA_CRIT 31
#define PAM_SERVICE 1
#define PAM_USER 2
#define PAM_TTY 3
#define PAM_RHOST 4
#define PAM_CONV 5
#define PAM_RUSER 8
#define PAM_SERVICE 1
#define PAM_USER 2
#define PAM_TTY 3
#define PAM_RHOST 4
#define PAM_CONV 5
#define PAM_AUTHTOK 6
#define PAM_OLDAUTHTOK 7
#define PAM_RUSER 8
#define PAM_USER_PROMPT 9
#define PAM_FAIL_DELAY 10
#define PAM_XDISPLAY 11
#define PAM_XAUTHDATA 12
#define PAM_AUTHTOK_TYPE 13
#define PAM_SILENT 0x8000
#define PAM_DISALLOW_NULL_AUTHTOK 0x0001
#define PAM_ESTABLISH_CRED 0x0002
#define PAM_DELETE_CRED 0x0004
#define PAM_REINITIALIZE_CRED 0x0008
#define PAM_REFRESH_CRED 0x0010
#define PAM_CHANGE_EXPIRED_AUTHTOK 0x0020
#define PAM_SILENT 0x8000
#define PAM_DISALLOW_NULL_AUTHTOK 0x0001
#define PAM_ESTABLISH_CRED 0x0002
#define PAM_DELETE_CRED 0x0004
#define PAM_REINITIALIZE_CRED 0x0008
#define PAM_REFRESH_CRED 0x0010
#define PAM_CHANGE_EXPIRED_AUTHTOK 0x0020
#define PAM_MAX_NUM_MSG 32
#define PAM_MAX_NUM_MSG 32
#define PAM_PROMPT_ECHO_OFF 1
#define PAM_PROMPT_ECHO_ON 2
#define PAM_ERROR_MSG 3
#define PAM_TEXT_INFO 4
#define PAM_PROMPT_ECHO_OFF 1
#define PAM_PROMPT_ECHO_ON 2
#define PAM_ERROR_MSG 3
#define PAM_TEXT_INFO 4
struct pam_message {
int msg_style;
@@ -80,7 +88,7 @@ struct pam_conv {
typedef struct pam_handle pam_handle_t;
int pam_start(const char *service, const char *user,
int pam_start(const char *service_name, const char *user,
const struct pam_conv *conv, pam_handle_t **pamh);
int pam_end(pam_handle_t *pamh, int pam_status);
@@ -89,6 +97,11 @@ int pam_set_item(pam_handle_t *pamh, int item_type, const void *item);
int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item);
int pam_get_data(const pam_handle_t *pamh, const char *module_data_name, void **data);
int pam_set_data(pam_handle_t *pamh, const char *module_data_name, void *data,
void (*cleanup)(pam_handle_t *pamh, void *data, int error_status));
int pam_authenticate(pam_handle_t *pamh, int flags);
int pam_acct_mgmt(pam_handle_t *pamh, int flags);
@@ -97,12 +110,12 @@ int pam_open_session(pam_handle_t *pamh, int flags);
int pam_close_session(pam_handle_t *pamh, int flags);
const char *pam_strerror(pam_handle_t *pamh, int errnum);
int pam_setcred(pam_handle_t *pamh, int flags);
int pam_chauthtok(pam_handle_t *pamh, int flags);
const char *pam_strerror(pam_handle_t *pamh, int errnum);
int pam_putenv(pam_handle_t *pamh, const char *name_value);
char **pam_getenvlist(pam_handle_t *pamh);
@@ -1,342 +0,0 @@
#include <security/pam_appl.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
#define AUTHD_SOCK_PATH "/run/redbear-authd.sock"
#define AUTHD_BUF_SIZE 4096
#define AUTHD_RESP_MAX 1024
struct pam_handle {
char *service;
char *user;
char *tty;
char *rhost;
char *ruser;
struct pam_conv conv;
};
static int connect_authd(void)
{
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
if (fd < 0) return -1;
struct sockaddr_un addr;
memset(&addr, 0, sizeof(addr));
addr.sun_family = AF_UNIX;
strncpy(addr.sun_path, AUTHD_SOCK_PATH, sizeof(addr.sun_path) - 1);
if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
close(fd);
return -1;
}
return fd;
}
static ssize_t send_all(int fd, const char *buf, size_t len)
{
size_t sent = 0;
while (sent < len) {
ssize_t n = send(fd, buf + sent, len - sent, 0);
if (n < 0) return -1;
sent += (size_t)n;
}
return (ssize_t)sent;
}
static ssize_t recv_line(int fd, char *buf, size_t bufsize)
{
size_t total = 0;
while (total < bufsize - 1) {
ssize_t n = recv(fd, buf + total, 1, 0);
if (n <= 0) return n < 0 ? -1 : (ssize_t)total;
total += (size_t)n;
if (buf[total - 1] == '\n') break;
}
buf[total] = '\0';
return (ssize_t)total;
}
__attribute__((unused))
static char *json_extract_string(const char *json, const char *key)
{
char search[128];
snprintf(search, sizeof(search), "\"%s\":", key);
const char *pos = strstr(json, search);
if (!pos) return NULL;
pos += strlen(search);
while (*pos == ' ' || *pos == '\t') pos++;
if (*pos != '"') return NULL;
pos++;
size_t klen = strlen(key);
(void)klen;
const char *end = strchr(pos, '"');
if (!end) return NULL;
size_t vlen = (size_t)(end - pos);
char *val = malloc(vlen + 1);
if (!val) return NULL;
memcpy(val, pos, vlen);
val[vlen] = '\0';
return val;
}
static int json_extract_bool(const char *json, const char *key)
{
char search[128];
snprintf(search, sizeof(search), "\"%s\":", key);
const char *pos = strstr(json, search);
if (!pos) return -1;
pos += strlen(search);
while (*pos == ' ' || *pos == '\t') pos++;
if (strncmp(pos, "true", 4) == 0) return 1;
if (strncmp(pos, "false", 5) == 0) return 0;
return -1;
}
int pam_start(const char *service, const char *user,
const struct pam_conv *conv, pam_handle_t **pamh)
{
if (!pamh) return PAM_SYSTEM_ERR;
pam_handle_t *h = calloc(1, sizeof(*h));
if (!h) return PAM_BUF_ERR;
if (service) {
h->service = strdup(service);
if (!h->service) { free(h); return PAM_BUF_ERR; }
}
if (user) {
h->user = strdup(user);
if (!h->user) { free(h->service); free(h); return PAM_BUF_ERR; }
}
if (conv) {
h->conv = *conv;
}
*pamh = h;
return PAM_SUCCESS;
}
int pam_end(pam_handle_t *pamh, int pam_status)
{
(void)pam_status;
if (!pamh) return PAM_SYSTEM_ERR;
free(pamh->service);
free(pamh->user);
free(pamh->tty);
free(pamh->rhost);
free(pamh->ruser);
free(pamh);
return PAM_SUCCESS;
}
int pam_set_item(pam_handle_t *pamh, int item_type, const void *item)
{
if (!pamh) return PAM_SYSTEM_ERR;
char **target = NULL;
switch (item_type) {
case PAM_SERVICE: target = &pamh->service; break;
case PAM_USER: target = &pamh->user; break;
case PAM_TTY: target = &pamh->tty; break;
case PAM_RHOST: target = &pamh->rhost; break;
case PAM_RUSER: target = &pamh->ruser; break;
case PAM_CONV:
if (item) pamh->conv = *(const struct pam_conv *)item;
return PAM_SUCCESS;
default:
return PAM_BAD_ITEM;
}
if (!target) return PAM_BAD_ITEM;
free(*target);
*target = NULL;
if (item) {
*target = strdup((const char *)item);
if (!*target) return PAM_BUF_ERR;
}
return PAM_SUCCESS;
}
int pam_get_item(const pam_handle_t *pamh, int item_type, const void **item)
{
if (!pamh || !item) return PAM_SYSTEM_ERR;
switch (item_type) {
case PAM_SERVICE: *item = pamh->service; break;
case PAM_USER: *item = pamh->user; break;
case PAM_TTY: *item = pamh->tty; break;
case PAM_RHOST: *item = pamh->rhost; break;
case PAM_RUSER: *item = pamh->ruser; break;
case PAM_CONV: *item = &pamh->conv; break;
default:
*item = NULL;
return PAM_BAD_ITEM;
}
return PAM_SUCCESS;
}
int pam_authenticate(pam_handle_t *pamh, int flags)
{
(void)flags;
if (!pamh) return PAM_SYSTEM_ERR;
if (!pamh->conv.conv) return PAM_SYSTEM_ERR;
const char *username = pamh->user;
if (!username) return PAM_AUTH_ERR;
const struct pam_message msg = {
.msg_style = PAM_PROMPT_ECHO_OFF,
.msg = "Password: "
};
const struct pam_message *msg_ptr = &msg;
struct pam_response *resp = NULL;
int rc = pamh->conv.conv(1, &msg_ptr, &resp, pamh->conv.appdata_ptr);
if (rc != PAM_SUCCESS || !resp || !resp[0].resp) return PAM_CONV_ERR;
const char *password = resp[0].resp;
int fd = connect_authd();
if (fd < 0) {
if (resp[0].resp) { memset(resp[0].resp, 0, strlen(resp[0].resp)); free(resp[0].resp); }
free(resp);
return PAM_AUTHINFO_UNAVAIL;
}
char request[AUTHD_BUF_SIZE];
int n = snprintf(request, sizeof(request),
"{\"type\":\"Authenticate\",\"username\":\"%s\",\"password\":\"",
username);
for (const char *p = password; *p && (size_t)n < sizeof(request) - 4; p++) {
if (*p == '"' || *p == '\\') {
request[n++] = '\\';
if ((size_t)n >= sizeof(request) - 3) break;
}
request[n++] = *p;
}
n += snprintf(request + n, sizeof(request) - (size_t)n, "\",\"vt\":0}\n");
memset((void *)password, 0, strlen(password));
free(resp[0].resp);
free(resp);
if (send_all(fd, request, (size_t)n) < 0) {
close(fd);
return PAM_AUTH_ERR;
}
char response[AUTHD_RESP_MAX];
ssize_t rlen = recv_line(fd, response, sizeof(response));
close(fd);
if (rlen <= 0) return PAM_AUTH_ERR;
int ok = json_extract_bool(response, "ok");
if (ok == 1) return PAM_SUCCESS;
return PAM_AUTH_ERR;
}
int pam_acct_mgmt(pam_handle_t *pamh, int flags)
{
(void)pamh;
(void)flags;
return PAM_SUCCESS;
}
int pam_open_session(pam_handle_t *pamh, int flags)
{
(void)pamh;
(void)flags;
return PAM_SUCCESS;
}
int pam_close_session(pam_handle_t *pamh, int flags)
{
(void)pamh;
(void)flags;
return PAM_SUCCESS;
}
const char *pam_strerror(pam_handle_t *pamh, int errnum)
{
(void)pamh;
switch (errnum) {
case PAM_SUCCESS: return "Success";
case PAM_OPEN_ERR: return "Failed to load module";
case PAM_SYMBOL_ERR: return "Invalid symbol";
case PAM_SERVICE_ERR: return "Error in service module";
case PAM_SYSTEM_ERR: return "System error";
case PAM_BUF_ERR: return "Memory buffer error";
case PAM_PERM_DENIED: return "Permission denied";
case PAM_AUTH_ERR: return "Authentication failure";
case PAM_CRED_INSUFFICIENT: return "Insufficient credentials";
case PAM_AUTHINFO_UNAVAIL: return "Authentication information unavailable";
case PAM_USER_UNKNOWN: return "User not known to the underlying module";
case PAM_MAXTRIES: return "Have exhausted maximum number of retries";
case PAM_NEW_AUTHTOK_REQD: return "Authentication token is no longer valid";
case PAM_ACCT_EXPIRED: return "Account expired";
case PAM_SESSION_ERR: return "Cannot make/remove an entry for the session";
case PAM_CRED_UNAVAIL: return "Authentication credentials unavailable";
case PAM_CRED_EXPIRED: return "Authentication credentials expired";
case PAM_CRED_ERR: return "Authentication credentials error";
case PAM_CONV_ERR: return "Conversation error";
case PAM_ABORT: return "Critical error - immediate abort";
default: return "Unknown PAM error";
}
}
int pam_setcred(pam_handle_t *pamh, int flags)
{
(void)pamh;
(void)flags;
return PAM_SUCCESS;
}
int pam_chauthtok(pam_handle_t *pamh, int flags)
{
(void)pamh;
(void)flags;
return PAM_AUTHTOK_ERR;
}
int pam_putenv(pam_handle_t *pamh, const char *name_value)
{
(void)pamh;
(void)name_value;
return PAM_SUCCESS;
}
char **pam_getenvlist(pam_handle_t *pamh)
{
(void)pamh;
char **envlist = malloc(sizeof(char *));
if (envlist) envlist[0] = NULL;
return envlist;
}
const char *pam_getenv(pam_handle_t *pamh, const char *name)
{
(void)pamh;
(void)name;
return NULL;
}
@@ -0,0 +1,945 @@
//! PAM compatibility library for Red Bear OS.
//!
//! Implements the C PAM ABI expected by SDDM and other Linux PAM consumers,
//! proxying authentication requests to `redbear-authd` over its Unix socket
//! JSON protocol. All other management calls (account, session, credential,
//! environment, data) are handled locally on the in-process `PamHandle`.
//!
//! The exported ABI is `extern "C"` with `#[no_mangle]` symbols. The crate
//! is built as a `cdylib` (libpam.so) plus an `rlib` (for Rust consumers
//! and unit tests). The C header at `include/security/pam_appl.h` declares
//! the public interface for C consumers.
#![allow(clippy::not_unsafe_ptr_arg_deref)]
use std::{
collections::HashMap,
ffi::{CStr, CString},
io::{Read, Write},
os::{
raw::{c_char, c_int, c_void},
unix::net::UnixStream,
},
ptr,
sync::atomic::{AtomicU64, Ordering},
};
use redbear_login_protocol::{AuthRequest, AuthResponse};
use serde_json::Value as JsonValue;
const AUTHD_SOCKET_PATH: &str = "/run/redbear-authd.sock";
const AUTHD_CONNECT_TIMEOUT_MS: u64 = 5_000;
const AUTHD_READ_TIMEOUT_MS: u64 = 30_000;
// PAM return codes (mirrors <security/_pam_types.h> on Linux-PAM).
pub const PAM_SUCCESS: c_int = 0;
pub const PAM_OPEN_ERR: c_int = 1;
pub const PAM_SYMBOL_ERR: c_int = 2;
pub const PAM_SERVICE_ERR: c_int = 3;
pub const PAM_SYSTEM_ERR: c_int = 4;
pub const PAM_BUF_ERR: c_int = 5;
pub const PAM_PERM_DENIED: c_int = 6;
pub const PAM_AUTH_ERR: c_int = 7;
pub const PAM_CRED_INSUFFICIENT: c_int = 8;
pub const PAM_AUTHINFO_UNAVAIL: c_int = 9;
pub const PAM_USER_UNKNOWN: c_int = 10;
pub const PAM_MAXTRIES: c_int = 11;
pub const PAM_NEW_AUTHTOK_REQD: c_int = 12;
pub const PAM_ACCT_EXPIRED: c_int = 13;
pub const PAM_SESSION_ERR: c_int = 14;
pub const PAM_CRED_UNAVAIL: c_int = 15;
pub const PAM_CRED_EXPIRED: c_int = 16;
pub const PAM_CRED_ERR: c_int = 17;
pub const PAM_NO_MODULE_DATA: c_int = 18;
pub const PAM_CONV_ERR: c_int = 19;
pub const PAM_AUTHTOK_ERR: c_int = 20;
pub const PAM_AUTHTOK_RECOVER_ERR: c_int = 21;
pub const PAM_AUTHTOK_LOCK_BUSY: c_int = 22;
pub const PAM_AUTHTOK_DISABLE_AGING: c_int = 23;
pub const PAM_TRY_AGAIN: c_int = 24;
pub const PAM_IGNORE: c_int = 25;
pub const PAM_ABORT: c_int = 26;
pub const PAM_AUTHTOK_EXPIRED: c_int = 27;
pub const PAM_BAD_ITEM: c_int = 29;
pub const PAM_BAD_STATE: c_int = 30;
pub const PAM_NO_MODULE_DATA_CRIT: c_int = 31;
// PAM item types.
pub const PAM_SERVICE: c_int = 1;
pub const PAM_USER: c_int = 2;
pub const PAM_TTY: c_int = 3;
pub const PAM_RHOST: c_int = 4;
pub const PAM_CONV: c_int = 5;
pub const PAM_AUTHTOK: c_int = 6;
pub const PAM_OLDAUTHTOK: c_int = 7;
pub const PAM_RUSER: c_int = 8;
pub const PAM_USER_PROMPT: c_int = 9;
pub const PAM_FAIL_DELAY: c_int = 10;
pub const PAM_XDISPLAY: c_int = 11;
pub const PAM_XAUTHDATA: c_int = 12;
pub const PAM_AUTHTOK_TYPE: c_int = 13;
// PAM prompt styles.
pub const PAM_PROMPT_ECHO_OFF: c_int = 1;
pub const PAM_PROMPT_ECHO_ON: c_int = 2;
pub const PAM_ERROR_MSG: c_int = 3;
pub const PAM_TEXT_INFO: c_int = 4;
// PAM flag bits.
pub const PAM_SILENT: c_int = 0x8000;
pub const PAM_DISALLOW_NULL_AUTHTOK: c_int = 0x0001;
pub const PAM_ESTABLISH_CRED: c_int = 0x0002;
pub const PAM_DELETE_CRED: c_int = 0x0004;
pub const PAM_REINITIALIZE_CRED: c_int = 0x0008;
pub const PAM_REFRESH_CRED: c_int = 0x0010;
pub const PAM_CHANGE_EXPIRED_AUTHTOK: c_int = 0x0020;
// Maximum number of messages in a single conversation round.
pub const PAM_MAX_NUM_MSG: usize = 32;
// Layouts shared with C — must match `security/pam_appl.h` exactly.
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct PamMessage {
pub msg_style: c_int,
pub msg: *const c_char,
}
#[repr(C)]
#[derive(Debug)]
pub struct PamResponse {
pub resp: *mut c_char,
pub resp_retcode: c_int,
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct PamConv {
pub conv: Option<
unsafe extern "C" fn(
num_msg: c_int,
msg: *const *const PamMessage,
resp: *mut *mut PamResponse,
appdata_ptr: *mut c_void,
) -> c_int,
>,
pub appdata_ptr: *mut c_void,
}
// Opaque handle (the public type is `pam_handle_t`).
pub struct PamHandle {
pub service: Option<CString>,
pub user: Option<CString>,
pub tty: Option<CString>,
pub rhost: Option<CString>,
pub ruser: Option<CString>,
pub authtok: Option<Vec<u8>>,
pub oldauthtok: Option<Vec<u8>>,
pub user_prompt: Option<CString>,
pub xdisplay: Option<CString>,
pub authtok_type: Option<CString>,
pub conv: Option<PamConv>,
pub env: HashMap<String, String>,
pub data: HashMap<String, DataEntry>,
pub next_request_id: AtomicU64,
}
pub type PamDataCleanup =
Option<unsafe extern "C" fn(pamh: *mut PamHandle, data: *mut c_void, error_status: c_int)>;
pub struct DataEntry {
pub data: *mut c_void,
pub cleanup: PamDataCleanup,
}
impl PamHandle {
fn new(service: Option<&str>, user: Option<&str>, conv: Option<PamConv>) -> Self {
Self {
service: service.map(|s| CString::new(s).unwrap_or_default()),
user: user.map(|s| CString::new(s).unwrap_or_default()),
tty: None,
rhost: None,
ruser: None,
authtok: None,
oldauthtok: None,
user_prompt: None,
xdisplay: None,
authtok_type: None,
conv,
env: HashMap::new(),
data: HashMap::new(),
next_request_id: AtomicU64::new(1),
}
}
fn next_request_id(&self) -> u64 {
self.next_request_id.fetch_add(1, Ordering::Relaxed)
}
}
fn read_cstr_arg<'a>(ptr: *const c_char) -> Option<&'a str> {
if ptr.is_null() {
return None;
}
unsafe { CStr::from_ptr(ptr) }.to_str().ok()
}
fn clone_cstr_arg(ptr: *const c_char) -> Option<CString> {
read_cstr_arg(ptr).map(|s| CString::new(s).unwrap_or_default())
}
fn read_const_data<'a>(ptr: *const c_void, len: usize) -> Option<Vec<u8>> {
if ptr.is_null() || len == 0 {
return None;
}
let slice = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) };
Some(slice.to_vec())
}
fn error_string(errnum: c_int) -> &'static str {
match errnum {
PAM_SUCCESS => "Success",
PAM_OPEN_ERR => "Failed to load module",
PAM_SYMBOL_ERR => "Invalid symbol",
PAM_SERVICE_ERR => "Error in service module",
PAM_SYSTEM_ERR => "System error",
PAM_BUF_ERR => "Memory buffer error",
PAM_PERM_DENIED => "Permission denied",
PAM_AUTH_ERR => "Authentication failure",
PAM_CRED_INSUFFICIENT => "Insufficient credentials",
PAM_AUTHINFO_UNAVAIL => "Authentication information is unavailable",
PAM_USER_UNKNOWN => "User is not known to the underlying authentication module",
PAM_MAXTRIES => "Have exhausted maximum number of retries for service",
PAM_NEW_AUTHTOK_REQD => "Authentication token is no longer valid; new one required",
PAM_ACCT_EXPIRED => "User account has expired",
PAM_SESSION_ERR => "Cannot make/remove an entry for the session",
PAM_CRED_UNAVAIL => "Authentication credentials are unavailable",
PAM_CRED_EXPIRED => "Authentication credentials have expired",
PAM_CRED_ERR => "Failure setting user credentials",
PAM_NO_MODULE_DATA => "No module-specific data is present",
PAM_CONV_ERR => "Conversation error",
PAM_AUTHTOK_ERR => "Authentication token manipulation error",
PAM_AUTHTOK_RECOVER_ERR => "Authentication token cannot be recovered",
PAM_AUTHTOK_LOCK_BUSY => "Authentication token lock busy",
PAM_AUTHTOK_DISABLE_AGING => "Authentication token aging disabled",
PAM_TRY_AGAIN => "Failed preliminary check by password service",
PAM_IGNORE => "Please ignore underlying account module",
PAM_ABORT => "Critical error; abort immediately",
PAM_AUTHTOK_EXPIRED => "Authentication token has expired",
PAM_BAD_ITEM => "The application passed an invalid item to the PAM library",
PAM_BAD_STATE => "The PAM library is in a bad state",
_ => "Unknown PAM error",
}
}
fn authenticate_with_authd(handle: &PamHandle, username: &str, password: &[u8], vt: u32) -> c_int {
let mut stream = match UnixStream::connect(AUTHD_SOCKET_PATH) {
Ok(stream) => stream,
Err(_) => return PAM_AUTHINFO_UNAVAIL,
};
let _ = stream.set_read_timeout(Some(std::time::Duration::from_millis(AUTHD_READ_TIMEOUT_MS)));
let _ = stream.set_write_timeout(Some(std::time::Duration::from_millis(AUTHD_CONNECT_TIMEOUT_MS)));
let request_id = handle.next_request_id();
let password_str = match std::str::from_utf8(password) {
Ok(s) => s.to_owned(),
Err(_) => return PAM_AUTH_ERR,
};
let request = AuthRequest::Authenticate {
request_id,
username: username.to_owned(),
password: password_str,
vt,
};
let payload = match serde_json::to_string(&request) {
Ok(s) => s,
Err(_) => return PAM_AUTH_ERR,
};
if stream.write_all(payload.as_bytes()).is_err() {
return PAM_AUTH_ERR;
}
if stream.write_all(b"\n").is_err() {
return PAM_AUTH_ERR;
}
let mut line = String::new();
let mut limited = (&mut stream).take(1_048_576);
if limited.read_to_string(&mut line).is_err() {
return PAM_AUTH_ERR;
}
let line = line.trim();
if line.is_empty() {
return PAM_AUTH_ERR;
}
match serde_json::from_str::<AuthResponse>(line) {
Ok(AuthResponse::AuthenticateResult { ok: true, .. }) => PAM_SUCCESS,
Ok(AuthResponse::AuthenticateResult { ok: false, .. }) => PAM_AUTH_ERR,
Ok(AuthResponse::Error { .. }) => PAM_AUTHINFO_UNAVAIL,
Ok(_) => PAM_AUTH_ERR,
Err(_) => {
// Tolerate older / partial JSON envelopes: accept top-level `ok` boolean.
match serde_json::from_str::<JsonValue>(line) {
Ok(JsonValue::Object(map)) => match map.get("ok") {
Some(JsonValue::Bool(true)) => PAM_SUCCESS,
Some(JsonValue::Bool(false)) => PAM_AUTH_ERR,
_ => PAM_AUTH_ERR,
},
_ => PAM_AUTH_ERR,
}
}
}
}
fn run_conversation(conv: &PamConv, messages: &[PamMessage]) -> Result<Vec<Option<Vec<u8>>>, c_int> {
let cb = match conv.conv {
Some(cb) => cb,
None => return Err(PAM_CONV_ERR),
};
if messages.is_empty() {
return Ok(Vec::new());
}
if messages.len() > PAM_MAX_NUM_MSG {
return Err(PAM_CONV_ERR);
}
let mut msg_array: Vec<*const PamMessage> = messages.iter().map(|m| m as *const PamMessage).collect();
let mut resp_ptr: *mut PamResponse = ptr::null_mut();
let rc = unsafe {
cb(
messages.len() as c_int,
msg_array.as_mut_ptr(),
&mut resp_ptr,
conv.appdata_ptr,
)
};
msg_array.clear();
if rc != PAM_SUCCESS {
return Err(rc);
}
if resp_ptr.is_null() {
return Err(PAM_CONV_ERR);
}
let responses = unsafe { std::slice::from_raw_parts(resp_ptr, messages.len()) };
let mut out: Vec<Option<Vec<u8>>> = Vec::with_capacity(messages.len());
for r in responses {
if r.resp.is_null() {
out.push(None);
} else {
let cstr = unsafe { CStr::from_ptr(r.resp) };
let mut bytes = cstr.to_bytes().to_vec();
out.push(Some(std::mem::take(&mut bytes)));
}
}
// Free the response array and zero the C-side buffers before returning.
for r in responses {
if !r.resp.is_null() {
unsafe {
ptr::write_bytes(r.resp as *mut u8, 0, libc_compat_strlen(r.resp));
let _ = CString::from_raw(r.resp);
}
}
}
unsafe {
let _ = Box::from_raw(std::slice::from_raw_parts_mut(resp_ptr, messages.len()));
}
Ok(out)
}
#[cfg(target_os = "redox")]
fn libc_compat_strlen(ptr: *const c_char) -> usize {
let mut len = 0;
unsafe {
while *ptr.add(len) != 0 {
len += 1;
}
}
len
}
#[cfg(not(target_os = "redox"))]
fn libc_compat_strlen(ptr: *const c_char) -> usize {
let mut len = 0;
unsafe {
while *ptr.add(len) != 0 {
len += 1;
}
}
len
}
#[no_mangle]
pub extern "C" fn pam_start(
service_name: *const c_char,
user: *const c_char,
conv: *const PamConv,
pamh: *mut *mut PamHandle,
) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let service = read_cstr_arg(service_name);
let user = read_cstr_arg(user);
let conv = if conv.is_null() {
None
} else {
Some(unsafe { *conv })
};
let handle = Box::new(PamHandle::new(service, user, conv));
unsafe { *pamh = Box::into_raw(handle) };
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_end(pamh: *mut PamHandle, pam_status: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = pam_status;
let handle = unsafe { Box::from_raw(pamh) };
// Run data cleanup callbacks before dropping the handle storage.
for (_, entry) in handle.data.into_iter() {
if let Some(cb) = entry.cleanup {
unsafe { cb(pamh, entry.data, pam_status) };
}
}
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_set_item(pamh: *mut PamHandle, item_type: c_int, item: *const c_void) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let handle = unsafe { &mut *pamh };
match item_type {
PAM_SERVICE => handle.service = clone_cstr_arg(item as *const c_char),
PAM_USER => handle.user = clone_cstr_arg(item as *const c_char),
PAM_TTY => handle.tty = clone_cstr_arg(item as *const c_char),
PAM_RHOST => handle.rhost = clone_cstr_arg(item as *const c_char),
PAM_RUSER => handle.ruser = clone_cstr_arg(item as *const c_char),
PAM_USER_PROMPT => handle.user_prompt = clone_cstr_arg(item as *const c_char),
PAM_XDISPLAY => handle.xdisplay = clone_cstr_arg(item as *const c_char),
PAM_AUTHTOK_TYPE => handle.authtok_type = clone_cstr_arg(item as *const c_char),
PAM_AUTHTOK => {
handle.authtok = read_const_data(item, item_len_from_cstring(item as *const c_char))
}
PAM_OLDAUTHTOK => {
handle.oldauthtok = read_const_data(item, item_len_from_cstring(item as *const c_char))
}
PAM_CONV => {
if !item.is_null() {
handle.conv = Some(unsafe { *(item as *const PamConv) });
} else {
handle.conv = None;
}
}
PAM_FAIL_DELAY | PAM_XAUTHDATA => return PAM_BAD_ITEM,
_ => return PAM_BAD_ITEM,
}
PAM_SUCCESS
}
fn item_len_from_cstring(ptr: *const c_char) -> usize {
if ptr.is_null() {
return 0;
}
unsafe { CStr::from_ptr(ptr) }.to_bytes().len()
}
#[no_mangle]
pub extern "C" fn pam_get_item(pamh: *const PamHandle, item_type: c_int, item: *mut *const c_void) -> c_int {
if pamh.is_null() || item.is_null() {
return PAM_SYSTEM_ERR;
}
let handle = unsafe { &*pamh };
let value: *const c_void = match item_type {
PAM_SERVICE => handle.service.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_USER => handle.user.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_TTY => handle.tty.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_RHOST => handle.rhost.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_RUSER => handle.ruser.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_USER_PROMPT => handle.user_prompt.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_XDISPLAY => handle.xdisplay.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_AUTHTOK_TYPE => handle.authtok_type.as_ref().map_or(ptr::null(), |s| s.as_ptr()) as *const c_void,
PAM_AUTHTOK => handle.authtok.as_ref().map_or(ptr::null(), |v| v.as_ptr()) as *const c_void,
PAM_OLDAUTHTOK => handle.oldauthtok.as_ref().map_or(ptr::null(), |v| v.as_ptr()) as *const c_void,
PAM_CONV => {
if let Some(conv) = handle.conv {
&conv as *const PamConv as *const c_void
} else {
ptr::null()
}
}
PAM_FAIL_DELAY | PAM_XAUTHDATA => {
unsafe { *item = ptr::null() };
return PAM_BAD_ITEM;
}
_ => {
unsafe { *item = ptr::null() };
return PAM_BAD_ITEM;
}
};
unsafe { *item = value };
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_get_data(
pamh: *const PamHandle,
module_data_name: *const c_char,
data: *mut *mut c_void,
) -> c_int {
if pamh.is_null() || module_data_name.is_null() || data.is_null() {
return PAM_SYSTEM_ERR;
}
let handle = unsafe { &*pamh };
let name = match read_cstr_arg(module_data_name) {
Some(name) => name,
None => return PAM_SYSTEM_ERR,
};
match handle.data.get(name) {
Some(entry) => {
unsafe { *data = entry.data };
PAM_SUCCESS
}
None => PAM_NO_MODULE_DATA,
}
}
#[no_mangle]
pub extern "C" fn pam_set_data(
pamh: *mut PamHandle,
module_data_name: *const c_char,
data: *mut c_void,
cleanup: PamDataCleanup,
) -> c_int {
if pamh.is_null() || module_data_name.is_null() {
return PAM_SYSTEM_ERR;
}
let handle = unsafe { &mut *pamh };
let name = match read_cstr_arg(module_data_name) {
Some(name) => name.to_owned(),
None => return PAM_SYSTEM_ERR,
};
// If we are replacing an existing entry, run its cleanup first.
if let Some(previous) = handle.data.remove(&name) {
if let Some(cb) = previous.cleanup {
unsafe { cb(pamh, previous.data, 0) };
}
}
handle.data.insert(
name,
DataEntry {
data,
cleanup,
},
);
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_authenticate(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
// SAFETY: pamh is checked for null and only read here; we never hand it
// out concurrently to other threads. The handle outlives this call.
let handle = unsafe { &*pamh };
// Refuse authentication when no username is available — this matches
// Linux-PAM's behavior of mapping "no user" to PAM_USER_UNKNOWN rather
// than a generic auth failure.
if handle.user.is_none() {
return PAM_USER_UNKNOWN;
}
// If a conv is set, prompt for the password first.
let password = if handle.conv.is_some() && handle.authtok.is_none() {
let prompt = CString::new("Password: ").unwrap_or_default();
let prompt_msg = PamMessage {
msg_style: PAM_PROMPT_ECHO_OFF,
msg: prompt.as_ptr(),
};
match run_conversation(handle.conv.as_ref().unwrap(), &[prompt_msg]) {
Ok(resp) if resp.len() == 1 => match resp.into_iter().next() {
Some(Some(bytes)) => bytes,
_ => return PAM_AUTH_ERR,
},
_ => return PAM_CONV_ERR,
}
} else {
match handle.authtok.clone() {
Some(bytes) => bytes,
None => return PAM_AUTH_ERR,
}
};
let username = match handle.user.as_ref() {
Some(u) => match u.to_str() {
Ok(s) => s.to_owned(),
Err(_) => return PAM_USER_UNKNOWN,
},
None => return PAM_USER_UNKNOWN,
};
authenticate_with_authd(handle, &username, &password, 0)
}
#[no_mangle]
pub extern "C" fn pam_acct_mgmt(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
// redbear-authd owns account validity (uid/shell policy). If we ever
// need finer checks, query redbear-authd's account request here. For
// now, accept the account.
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_open_session(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_close_session(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_setcred(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
PAM_SUCCESS
}
#[no_mangle]
pub extern "C" fn pam_chauthtok(pamh: *mut PamHandle, flags: c_int) -> c_int {
if pamh.is_null() {
return PAM_SYSTEM_ERR;
}
let _ = flags;
// redbear-authd's current protocol surface is authenticate/session/power.
// Token rotation requires an extension to the authd protocol; surface the
// missing capability honestly rather than claiming success.
PAM_AUTHTOK_ERR
}
#[no_mangle]
pub extern "C" fn pam_strerror(pamh: *mut PamHandle, errnum: c_int) -> *const c_char {
let _ = pamh;
// For the error string we need a stable pointer per-thread; the simplest
// portable approach is a thread-local rotating buffer. The pointer is
// stable only for the duration of the call (the caller is expected to
// copy it if it needs persistence).
thread_local! {
static BUFFER: std::cell::RefCell<[u8; 256]> = const { std::cell::RefCell::new([0u8; 256]) };
}
let msg = error_string(errnum);
BUFFER.with(|cell| {
let mut buf = cell.borrow_mut();
let copy_len = std::cmp::min(msg.len(), buf.len() - 1);
buf[..copy_len].copy_from_slice(&msg.as_bytes()[..copy_len]);
buf[copy_len] = 0;
buf.as_ptr() as *const c_char
})
}
#[no_mangle]
pub extern "C" fn pam_putenv(pamh: *mut PamHandle, name_value: *const c_char) -> c_int {
if pamh.is_null() || name_value.is_null() {
return PAM_SYSTEM_ERR;
}
let handle = unsafe { &mut *pamh };
let raw = unsafe { CStr::from_ptr(name_value) };
let s = match raw.to_str() {
Ok(s) => s,
Err(_) => return PAM_SYSTEM_ERR,
};
if let Some((name, value)) = s.split_once('=') {
if name.is_empty() {
return PAM_SYSTEM_ERR;
}
handle.env.insert(name.to_owned(), value.to_owned());
PAM_SUCCESS
} else {
// `pam_putenv(pamh, "NAME")` removes the variable.
handle.env.remove(s);
PAM_SUCCESS
}
}
#[no_mangle]
pub extern "C" fn pam_getenv(pamh: *mut PamHandle, name: *const c_char) -> *const c_char {
if pamh.is_null() || name.is_null() {
return ptr::null();
}
let handle = unsafe { &*pamh };
let key = match read_cstr_arg(name) {
Some(s) => s,
None => return ptr::null(),
};
match handle.env.get(key) {
Some(v) => {
// The pointer is leaked: the env entries are kept alive by
// `handle.env`, so the pointer remains valid until pam_end.
v.as_ptr() as *const c_char
}
None => ptr::null(),
}
}
#[no_mangle]
pub extern "C" fn pam_getenvlist(pamh: *mut PamHandle) -> *mut *mut c_char {
if pamh.is_null() {
return ptr::null_mut();
}
let handle = unsafe { &*pamh };
// Layout: array of (n+1) char* pointers, last is NULL. The strings are
// appended after the pointer array and live for the lifetime of the
// handle (which is the lifetime expected by Linux-PAM consumers).
let mut entries: Vec<CString> = handle
.env
.iter()
.map(|(k, v)| CString::new(format!("{k}={v}")).unwrap_or_default())
.collect();
let total = entries.len() + 1;
let layout_size = total * std::mem::size_of::<*mut c_char>();
let strings_size: usize = entries.iter().map(|s| s.as_bytes_with_nul().len()).sum();
let bytes = layout_size + strings_size;
unsafe {
let raw = libc_compat_alloc(bytes) as *mut u8;
if raw.is_null() {
return ptr::null_mut();
}
ptr::write_bytes(raw, 0, bytes);
let ptrs = raw as *mut *mut c_char;
let mut cursor = raw.add(layout_size);
for (i, s) in entries.iter_mut().enumerate() {
let bytes = s.as_bytes_with_nul();
ptr::copy_nonoverlapping(bytes.as_ptr(), cursor, bytes.len());
*ptrs.add(i) = cursor as *mut c_char;
cursor = cursor.add(bytes.len());
}
*ptrs.add(entries.len()) = ptr::null_mut();
// Leak the CStrings so the pointers stay valid.
let _: Box<[CString]> = entries.into_boxed_slice();
ptrs
}
}
#[cfg(target_os = "redox")]
unsafe fn libc_compat_alloc(size: usize) -> *mut c_void {
extern "C" {
fn malloc(size: usize) -> *mut c_void;
}
malloc(size)
}
#[cfg(not(target_os = "redox"))]
unsafe fn libc_compat_alloc(size: usize) -> *mut c_void {
extern "C" {
fn malloc(size: usize) -> *mut c_void;
}
malloc(size)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
use std::os::raw::c_char;
fn make_handle() -> *mut PamHandle {
let service = CString::new("sddm").unwrap();
let user = CString::new("root").unwrap();
let mut pamh: *mut PamHandle = ptr::null_mut();
let rc = pam_start(service.as_ptr(), user.as_ptr(), ptr::null(), &mut pamh);
assert_eq!(rc, PAM_SUCCESS);
assert!(!pamh.is_null());
pamh
}
#[test]
fn start_and_end_lifecycle() {
let pamh = make_handle();
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn set_and_get_string_items() {
let pamh = make_handle();
let tty = CString::new("/dev/tty1").unwrap();
assert_eq!(pam_set_item(pamh, PAM_TTY, tty.as_ptr() as *const c_void), PAM_SUCCESS);
let mut item: *const c_void = ptr::null();
assert_eq!(pam_get_item(pamh, PAM_TTY, &mut item), PAM_SUCCESS);
let round = unsafe { CStr::from_ptr(item as *const c_char) };
assert_eq!(round.to_str().unwrap(), "/dev/tty1");
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn unknown_item_returns_bad_item() {
let pamh = make_handle();
let mut item: *const c_void = ptr::null();
assert_eq!(pam_get_item(pamh, PAM_FAIL_DELAY, &mut item), PAM_BAD_ITEM);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn data_set_get_with_cleanup() {
static CLEANUP_COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
unsafe extern "C" fn cleanup(
_pamh: *mut PamHandle,
_data: *mut c_void,
_error_status: c_int,
) {
CLEANUP_COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
}
let pamh = make_handle();
let key = CString::new("module_data").unwrap();
let sentinel: c_int = 0xC0FFEE;
let rc = pam_set_data(
pamh,
key.as_ptr(),
&sentinel as *const c_int as *mut c_void,
Some(cleanup),
);
assert_eq!(rc, PAM_SUCCESS);
let mut out: *mut c_void = ptr::null_mut();
assert_eq!(pam_get_data(pamh, key.as_ptr(), &mut out), PAM_SUCCESS);
assert_eq!(out as usize, &sentinel as *const c_int as usize);
// Missing key returns NO_MODULE_DATA.
let missing = CString::new("other").unwrap();
let mut dummy: *mut c_void = ptr::null_mut();
assert_eq!(pam_get_data(pamh, missing.as_ptr(), &mut dummy), PAM_NO_MODULE_DATA);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
assert!(CLEANUP_COUNT.load(std::sync::atomic::Ordering::SeqCst) >= 1);
}
#[test]
fn env_round_trip() {
let pamh = make_handle();
let entry = CString::new("XDG_SESSION_TYPE=wayland").unwrap();
assert_eq!(pam_putenv(pamh, entry.as_ptr()), PAM_SUCCESS);
let key = CString::new("XDG_SESSION_TYPE").unwrap();
let raw = pam_getenv(pamh, key.as_ptr());
assert!(!raw.is_null());
assert_eq!(unsafe { CStr::from_ptr(raw) }.to_str().unwrap(), "wayland");
// pam_getenvlist must produce at least one entry plus a NULL terminator.
let list = pam_getenvlist(pamh);
assert!(!list.is_null());
unsafe {
assert!(!(*list).is_null());
assert_eq!(CStr::from_ptr(*list).to_str().unwrap(), "XDG_SESSION_TYPE=wayland");
}
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn env_remove_via_putenv() {
let pamh = make_handle();
let entry = CString::new("FOO=1").unwrap();
assert_eq!(pam_putenv(pamh, entry.as_ptr()), PAM_SUCCESS);
let key = CString::new("FOO").unwrap();
assert!(!pam_getenv(pamh, key.as_ptr()).is_null());
let remove = CString::new("FOO").unwrap();
assert_eq!(pam_putenv(pamh, remove.as_ptr()), PAM_SUCCESS);
assert!(pam_getenv(pamh, key.as_ptr()).is_null());
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn chauthtok_returns_authtok_err() {
let pamh = make_handle();
// The current authd protocol does not include password change; the
// operation honestly reports that to the caller.
assert_eq!(pam_chauthtok(pamh, 0), PAM_AUTHTOK_ERR);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn strerror_returns_non_null_strings() {
for code in [PAM_SUCCESS, PAM_AUTH_ERR, PAM_USER_UNKNOWN, PAM_BUF_ERR, 9999] {
let ptr = pam_strerror(ptr::null_mut(), code);
assert!(!ptr.is_null());
let text = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap();
assert!(!text.is_empty());
}
}
#[test]
fn authenticate_without_user_returns_user_unknown() {
let service = CString::new("sddm").unwrap();
let mut pamh: *mut PamHandle = ptr::null_mut();
let rc = pam_start(service.as_ptr(), ptr::null(), ptr::null(), &mut pamh);
assert_eq!(rc, PAM_SUCCESS);
// No user set, no conv -> authentication cannot proceed.
assert_eq!(pam_authenticate(pamh, 0), PAM_USER_UNKNOWN);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn authenticate_with_user_but_no_authd_socket_returns_auth_err() {
// redbear-authd is not running on the host, so the socket connect
// fails and the result is mapped to PAM_AUTH_ERR.
let pamh = make_handle();
let rc = pam_authenticate(pamh, 0);
assert!(rc == PAM_AUTH_ERR || rc == PAM_AUTHINFO_UNAVAIL);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
#[test]
fn acct_session_cred_are_successful() {
let pamh = make_handle();
assert_eq!(pam_acct_mgmt(pamh, 0), PAM_SUCCESS);
assert_eq!(pam_open_session(pamh, 0), PAM_SUCCESS);
assert_eq!(pam_close_session(pamh, 0), PAM_SUCCESS);
assert_eq!(pam_setcred(pamh, PAM_ESTABLISH_CRED), PAM_SUCCESS);
assert_eq!(pam_end(pamh, PAM_SUCCESS), PAM_SUCCESS);
}
}