From 67c59641ff0b10b3bb19ad77eeb437a162dfaca3 Mon Sep 17 00:00:00 2001 From: Admin Pupkin Date: Tue, 9 Jun 2026 10:37:32 +0300 Subject: [PATCH] pam-redbear: port minimal PAM to Redox; proxy to redbear-authd --- config/redbear-full.toml | 8 +- local/recipes/libs/pam-redbear/recipe.toml | 51 +- .../libs/pam-redbear/source/Cargo.toml | 23 + .../source/{ => include}/security/pam_appl.h | 85 +- .../libs/pam-redbear/source/pam_redbear.c | 342 ------- .../libs/pam-redbear/source/src/lib.rs | 945 ++++++++++++++++++ 6 files changed, 1054 insertions(+), 400 deletions(-) create mode 100644 local/recipes/libs/pam-redbear/source/Cargo.toml rename local/recipes/libs/pam-redbear/source/{ => include}/security/pam_appl.h (52%) delete mode 100644 local/recipes/libs/pam-redbear/source/pam_redbear.c create mode 100644 local/recipes/libs/pam-redbear/source/src/lib.rs diff --git a/config/redbear-full.toml b/config/redbear-full.toml index e76cbe7336..1101bab211 100644 --- a/config/redbear-full.toml +++ b/config/redbear-full.toml @@ -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 diff --git a/local/recipes/libs/pam-redbear/recipe.toml b/local/recipes/libs/pam-redbear/recipe.toml index 34a69731b5..f5d0bed6ad 100644 --- a/local/recipes/libs/pam-redbear/recipe.toml +++ b/local/recipes/libs/pam-redbear/recipe.toml @@ -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.). diff --git a/local/recipes/libs/pam-redbear/source/Cargo.toml b/local/recipes/libs/pam-redbear/source/Cargo.toml new file mode 100644 index 0000000000..6fab7f15e4 --- /dev/null +++ b/local/recipes/libs/pam-redbear/source/Cargo.toml @@ -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" diff --git a/local/recipes/libs/pam-redbear/source/security/pam_appl.h b/local/recipes/libs/pam-redbear/source/include/security/pam_appl.h similarity index 52% rename from local/recipes/libs/pam-redbear/source/security/pam_appl.h rename to local/recipes/libs/pam-redbear/source/include/security/pam_appl.h index 0ff0ee067f..396b97cb54 100644 --- a/local/recipes/libs/pam-redbear/source/security/pam_appl.h +++ b/local/recipes/libs/pam-redbear/source/include/security/pam_appl.h @@ -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); diff --git a/local/recipes/libs/pam-redbear/source/pam_redbear.c b/local/recipes/libs/pam-redbear/source/pam_redbear.c deleted file mode 100644 index 817c468677..0000000000 --- a/local/recipes/libs/pam-redbear/source/pam_redbear.c +++ /dev/null @@ -1,342 +0,0 @@ -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#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; -} diff --git a/local/recipes/libs/pam-redbear/source/src/lib.rs b/local/recipes/libs/pam-redbear/source/src/lib.rs new file mode 100644 index 0000000000..b51b283e80 --- /dev/null +++ b/local/recipes/libs/pam-redbear/source/src/lib.rs @@ -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 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, + pub user: Option, + pub tty: Option, + pub rhost: Option, + pub ruser: Option, + pub authtok: Option>, + pub oldauthtok: Option>, + pub user_prompt: Option, + pub xdisplay: Option, + pub authtok_type: Option, + pub conv: Option, + pub env: HashMap, + pub data: HashMap, + pub next_request_id: AtomicU64, +} + +pub type PamDataCleanup = + Option; + +pub struct DataEntry { + pub data: *mut c_void, + pub cleanup: PamDataCleanup, +} + +impl PamHandle { + fn new(service: Option<&str>, user: Option<&str>, conv: Option) -> 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 { + read_cstr_arg(ptr).map(|s| CString::new(s).unwrap_or_default()) +} + +fn read_const_data<'a>(ptr: *const c_void, len: usize) -> Option> { + 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::(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::(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>>, 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>> = 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 = 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); + } +}