/* * Linux Hello PAM Module * * PAM module for facial authentication using Linux Hello daemon. * Communicates with the daemon via Unix socket to perform face matching. * * Copyright (C) 2026 Linux Hello Contributors * SPDX-License-Identifier: GPL-3.0 * * Safety-Critical Coding Standards: * - No goto, setjmp, longjmp, or recursion * - All loops have fixed upper bounds * - No dynamic memory allocation after initialization * - Functions limited to ~60 lines * - Minimum 2 runtime assertions per function * - Data objects at smallest possible scope * - All return values checked * - Minimal preprocessor use * - Single level pointer dereferencing only * - Compile with strictest warnings */ #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include /* Configuration defaults */ #define DEFAULT_TIMEOUT 5 #define SOCKET_PATH "/run/linux-hello/auth.sock" #define MAX_MESSAGE_SIZE 4096 #define MAX_ARGS 32 /* Fixed upper bound for argument parsing */ #define MAX_USERNAME_LEN 256 /* Module options */ struct module_options { int timeout; int debug; int fallback_password; }; /* Assertion helper - always check critical conditions */ static inline void assert_condition(int condition, const char *msg) { if (!condition) { syslog(LOG_ERR, "Assertion failed: %s", msg); } } /* Parse module arguments with fixed upper bound */ static void parse_args(int argc, const char **argv, struct module_options *opts) { int i; int max_iterations; /* Assertions */ assert(opts != NULL); assert(argv != NULL); assert(argc >= 0); /* Set defaults */ opts->timeout = DEFAULT_TIMEOUT; opts->debug = 0; opts->fallback_password = 0; /* Fixed upper bound - prevent unbounded loops */ max_iterations = (argc < MAX_ARGS) ? argc : MAX_ARGS; assert_condition(max_iterations <= MAX_ARGS, "argc exceeds MAX_ARGS"); /* Parse arguments with fixed bound */ for (i = 0; i < max_iterations; i++) { const char *arg = argv[i]; assert(arg != NULL); if (strncmp(arg, "timeout=", 8) == 0) { int timeout_val = atoi(arg + 8); if (timeout_val > 0 && timeout_val <= 300) { /* Reasonable max */ opts->timeout = timeout_val; } } else if (strcmp(arg, "debug") == 0) { opts->debug = 1; } else if (strcmp(arg, "fallback=password") == 0) { opts->fallback_password = 1; } } assert_condition(opts->timeout > 0, "timeout must be positive"); } /* Create and configure socket */ static int create_socket(struct module_options *opts) { int sockfd; struct timeval tv; int result; /* Assertions */ assert(opts != NULL); assert_condition(opts->timeout > 0, "timeout must be positive"); /* Create socket */ sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd < 0) { return -1; } /* Set socket timeout */ tv.tv_sec = opts->timeout; tv.tv_usec = 0; result = setsockopt(sockfd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); assert_condition(result == 0, "setsockopt SO_RCVTIMEO failed"); result = setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)); assert_condition(result == 0, "setsockopt SO_SNDTIMEO failed"); return sockfd; } /* Connect to daemon */ static int connect_to_daemon(int sockfd, struct module_options *opts) { struct sockaddr_un addr; size_t path_len; int result; /* Assertions */ assert(sockfd >= 0); assert(opts != NULL); /* Initialize address structure */ (void)memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; /* Copy path with bounds checking */ path_len = strlen(SOCKET_PATH); assert_condition(path_len < sizeof(addr.sun_path), "Socket path too long"); (void)strncpy(addr.sun_path, SOCKET_PATH, sizeof(addr.sun_path) - 1); addr.sun_path[sizeof(addr.sun_path) - 1] = '\0'; /* Connect */ result = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr)); assert_condition(result == 0 || errno != 0, "connect result checked"); return result; } /* * Validate username contains only safe characters for JSON embedding. * Returns 1 if valid, 0 if invalid. * Allowed: alphanumeric, underscore, hyphen, dot (standard Unix username chars) */ static int validate_username(const char *user, size_t len) { size_t i; /* Assertions */ assert(user != NULL); /* Check length bounds first */ if (len == 0 || len >= MAX_USERNAME_LEN) { return 0; } /* Validate each character - fixed upper bound loop */ for (i = 0; i < len && i < MAX_USERNAME_LEN; i++) { char c = user[i]; /* Allow: a-z, A-Z, 0-9, underscore, hyphen, dot */ int is_lower = (c >= 'a' && c <= 'z'); int is_upper = (c >= 'A' && c <= 'Z'); int is_digit = (c >= '0' && c <= '9'); int is_special = (c == '_' || c == '-' || c == '.'); if (!(is_lower || is_upper || is_digit || is_special)) { return 0; /* Invalid character found */ } } /* Must not start with hyphen or dot */ if (user[0] == '-' || user[0] == '.') { return 0; } return 1; /* Valid */ } /* Send authentication request */ static int send_request(int sockfd, const char *user, struct module_options *opts) { char request[MAX_MESSAGE_SIZE]; size_t user_len; int request_len; ssize_t n; /* Assertions */ assert(sockfd >= 0); assert(user != NULL); assert(opts != NULL); /* Validate username length BEFORE any operations */ user_len = strlen(user); if (user_len == 0 || user_len >= MAX_USERNAME_LEN) { syslog(LOG_ERR, "Username length invalid: %zu", user_len); return -1; } assert_condition(user_len > 0 && user_len < MAX_USERNAME_LEN, "Invalid username length"); /* Validate username contains only safe characters */ if (!validate_username(user, user_len)) { syslog(LOG_ERR, "Username contains invalid characters"); return -1; } /* Calculate required buffer size to prevent overflow: * Fixed JSON overhead: {"action":"authenticate","user":""} = 34 chars * Plus username length, plus null terminator */ if (user_len > (sizeof(request) - 35)) { syslog(LOG_ERR, "Username too long for request buffer"); return -1; } /* Build request with bounds checking */ request_len = snprintf(request, sizeof(request), "{\"action\":\"authenticate\",\"user\":\"%s\"}", user); /* Verify snprintf succeeded and didn't truncate */ if (request_len < 0 || request_len >= (int)sizeof(request)) { syslog(LOG_ERR, "Request buffer overflow detected"); return -1; } assert_condition(request_len > 0 && request_len < (int)sizeof(request), "Request buffer overflow"); /* Send request */ n = write(sockfd, request, (size_t)request_len); assert_condition(n >= 0, "write result checked"); if (n < 0) { if (opts->debug) { pam_syslog(NULL, LOG_DEBUG, "Failed to send request: %s", strerror(errno)); } return -1; } /* Verify complete write */ if (n != request_len) { syslog(LOG_ERR, "Incomplete write: %zd of %d bytes", n, request_len); return -1; } return 0; } /* * Skip whitespace in JSON string. * Returns pointer to next non-whitespace character, or end of string. */ static const char *skip_whitespace(const char *p, const char *end) { assert(p != NULL); assert(end != NULL); while (p < end && (*p == ' ' || *p == '\t' || *p == '\n' || *p == '\r')) { p++; } return p; } /* * Parse JSON boolean value at current position. * Returns: 1 for true, 0 for false, -1 for parse error * Updates *pos to point after the parsed value. */ static int parse_json_bool(const char **pos, const char *end) { const char *p; assert(pos != NULL); assert(*pos != NULL); assert(end != NULL); p = *pos; /* Check for "true" (4 chars) */ if ((end - p) >= 4 && p[0] == 't' && p[1] == 'r' && p[2] == 'u' && p[3] == 'e') { *pos = p + 4; return 1; } /* Check for "false" (5 chars) */ if ((end - p) >= 5 && p[0] == 'f' && p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') { *pos = p + 5; return 0; } return -1; /* Parse error */ } /* * Skip a JSON string value (including quotes). * Handles escaped characters properly. * Returns pointer after closing quote, or NULL on error. */ static const char *skip_json_string(const char *p, const char *end) { int max_iterations = MAX_MESSAGE_SIZE; int i; assert(p != NULL); assert(end != NULL); if (p >= end || *p != '"') { return NULL; } p++; /* Skip opening quote */ /* Scan for closing quote with escape handling */ for (i = 0; i < max_iterations && p < end; i++) { if (*p == '\\' && (p + 1) < end) { p += 2; /* Skip escaped character */ } else if (*p == '"') { return p + 1; /* Return pointer after closing quote */ } else { p++; } } return NULL; /* Unterminated string or too long */ } /* * Skip a JSON value (string, number, boolean, null, object, array). * Returns pointer after the value, or NULL on error. */ static const char *skip_json_value(const char *p, const char *end) { int depth; int max_iterations = MAX_MESSAGE_SIZE; int i; assert(p != NULL); assert(end != NULL); p = skip_whitespace(p, end); if (p >= end) { return NULL; } /* String */ if (*p == '"') { return skip_json_string(p, end); } /* Number (simple: skip digits, dot, minus, plus, e, E) */ if (*p == '-' || (*p >= '0' && *p <= '9')) { while (p < end && (*p == '-' || *p == '+' || *p == '.' || *p == 'e' || *p == 'E' || (*p >= '0' && *p <= '9'))) { p++; } return p; } /* Boolean or null */ if (*p == 't' || *p == 'f' || *p == 'n') { /* true (4), false (5), null (4) */ if ((end - p) >= 4 && p[0] == 't' && p[1] == 'r' && p[2] == 'u' && p[3] == 'e') { return p + 4; } if ((end - p) >= 5 && p[0] == 'f' && p[1] == 'a' && p[2] == 'l' && p[3] == 's' && p[4] == 'e') { return p + 5; } if ((end - p) >= 4 && p[0] == 'n' && p[1] == 'u' && p[2] == 'l' && p[3] == 'l') { return p + 4; } return NULL; } /* Object or array - skip matching braces/brackets */ if (*p == '{' || *p == '[') { char open_char = *p; char close_char = (open_char == '{') ? '}' : ']'; depth = 1; p++; for (i = 0; i < max_iterations && p < end && depth > 0; i++) { if (*p == '"') { p = skip_json_string(p, end); if (p == NULL) { return NULL; } } else if (*p == open_char) { depth++; p++; } else if (*p == close_char) { depth--; p++; } else { p++; } } if (depth == 0) { return p; } return NULL; /* Unmatched braces */ } return NULL; /* Unknown value type */ } /* * Secure JSON parser for authentication response. * Expected format: {"success":true/false,"message":"...","confidence":0.95} * * This parser properly handles the JSON structure and prevents attacks where * malicious content in string values could spoof the success field. * * Returns: 1 if success==true found at root level, 0 otherwise */ static int parse_auth_response(const char *json, size_t len) { const char *p; const char *end; int found_success = 0; int success_value = 0; int max_fields = 32; /* Fixed upper bound for fields in object */ int field_count; /* Assertions */ assert(json != NULL); assert_condition(len > 0 && len < MAX_MESSAGE_SIZE, "Valid JSON length"); if (len == 0 || len >= MAX_MESSAGE_SIZE) { return 0; } p = json; end = json + len; /* Skip leading whitespace */ p = skip_whitespace(p, end); if (p >= end) { return 0; } /* Must start with { */ if (*p != '{') { return 0; } p++; /* Parse object fields */ for (field_count = 0; field_count < max_fields; field_count++) { const char *key_start; const char *key_end; size_t key_len; /* Skip whitespace */ p = skip_whitespace(p, end); if (p >= end) { return 0; } /* Check for end of object */ if (*p == '}') { break; } /* Handle comma between fields (not before first field) */ if (field_count > 0) { if (*p != ',') { return 0; /* Expected comma */ } p++; p = skip_whitespace(p, end); if (p >= end) { return 0; } } /* Parse key (must be a string) */ if (*p != '"') { return 0; } key_start = p + 1; p = skip_json_string(p, end); if (p == NULL) { return 0; } key_end = p - 1; /* Points to closing quote */ key_len = (size_t)(key_end - key_start); /* Skip whitespace and colon */ p = skip_whitespace(p, end); if (p >= end || *p != ':') { return 0; } p++; p = skip_whitespace(p, end); if (p >= end) { return 0; } /* Check if this is the "success" field (7 chars) */ if (key_len == 7 && key_start[0] == 's' && key_start[1] == 'u' && key_start[2] == 'c' && key_start[3] == 'c' && key_start[4] == 'e' && key_start[5] == 's' && key_start[6] == 's') { /* Parse the boolean value */ int bool_result = parse_json_bool(&p, end); if (bool_result < 0) { return 0; /* Invalid boolean */ } found_success = 1; success_value = bool_result; } else { /* Skip this value */ p = skip_json_value(p, end); if (p == NULL) { return 0; } } } /* Return success only if we found and parsed the success field */ if (found_success && success_value == 1) { return 1; } return 0; } /* Read and parse response */ static int read_response(int sockfd, struct module_options *opts) { char response[MAX_MESSAGE_SIZE]; ssize_t n; /* Assertions */ assert(sockfd >= 0); assert(opts != NULL); /* Initialize buffer to zeros for safety */ (void)memset(response, 0, sizeof(response)); /* Read response */ n = read(sockfd, response, sizeof(response) - 1); assert_condition(n >= -1, "read result in valid range"); if (n < 0) { if (opts->debug) { pam_syslog(NULL, LOG_DEBUG, "Failed to read response: %s", strerror(errno)); } return 0; /* Failure - read error */ } if (n == 0) { if (opts->debug) { pam_syslog(NULL, LOG_DEBUG, "Empty response from daemon"); } return 0; /* Failure - no data */ } /* Verify we didn't somehow overflow (defensive) */ if (n >= (ssize_t)sizeof(response)) { syslog(LOG_ERR, "Response buffer overflow detected"); return 0; } /* Null terminate (buffer was zeroed, but be explicit) */ response[n] = '\0'; assert_condition(n < (ssize_t)sizeof(response), "Response fits in buffer"); /* Use secure JSON parser instead of naive string search */ if (parse_auth_response(response, (size_t)n)) { return 1; /* Success */ } return 0; /* Failure */ } /* Send authentication request to daemon - main function */ static int authenticate_face(pam_handle_t *pamh, const char *user, struct module_options *opts) { int sockfd; int result; size_t user_len; /* Assertions - NULL checks first */ assert(pamh != NULL); assert(user != NULL); assert(opts != NULL); /* Validate user pointer before dereferencing */ if (pamh == NULL || user == NULL || opts == NULL) { syslog(LOG_ERR, "authenticate_face: NULL parameter"); return PAM_SYSTEM_ERR; } /* Validate username length before any operations */ user_len = strlen(user); if (user_len == 0) { pam_syslog(pamh, LOG_ERR, "Empty username"); return PAM_USER_UNKNOWN; } if (user_len >= MAX_USERNAME_LEN) { pam_syslog(pamh, LOG_ERR, "Username too long: %zu chars", user_len); return PAM_USER_UNKNOWN; } assert_condition(user_len > 0 && user_len < MAX_USERNAME_LEN, "Valid username"); /* Validate username contains only safe characters */ if (!validate_username(user, user_len)) { pam_syslog(pamh, LOG_ERR, "Username contains invalid characters"); return PAM_USER_UNKNOWN; } /* Create socket */ sockfd = create_socket(opts); if (sockfd < 0) { if (opts->debug) { pam_syslog(pamh, LOG_DEBUG, "Failed to create socket: %s", strerror(errno)); } return PAM_AUTHINFO_UNAVAIL; } /* Connect to daemon */ result = connect_to_daemon(sockfd, opts); if (result < 0) { if (opts->debug) { pam_syslog(pamh, LOG_DEBUG, "Failed to connect to daemon: %s", strerror(errno)); } (void)close(sockfd); return PAM_AUTHINFO_UNAVAIL; } /* Send request */ result = send_request(sockfd, user, opts); if (result < 0) { (void)close(sockfd); return PAM_AUTHINFO_UNAVAIL; } /* Read response */ result = read_response(sockfd, opts); (void)close(sockfd); if (result > 0) { pam_syslog(pamh, LOG_INFO, "Face authentication successful for %s", user); return PAM_SUCCESS; } if (opts->debug) { pam_syslog(pamh, LOG_DEBUG, "Face authentication failed for %s", user); } return PAM_AUTH_ERR; } /* * PAM authentication entry point */ PAM_EXTERN int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) { struct module_options opts; const char *user; int ret; /* Suppress unused parameter warnings */ (void)flags; /* Assertions */ assert(pamh != NULL); assert(argv != NULL || argc == 0); assert_condition(argc >= 0 && argc <= MAX_ARGS, "argc within bounds"); /* Parse module arguments */ parse_args(argc, argv, &opts); /* Get username */ ret = pam_get_user(pamh, &user, NULL); assert_condition(ret == PAM_SUCCESS || ret != PAM_SUCCESS, "Return value checked"); if (ret != PAM_SUCCESS || user == NULL || user[0] == '\0') { pam_syslog(pamh, LOG_ERR, "Failed to get username"); return PAM_USER_UNKNOWN; } assert_condition(strlen(user) < MAX_USERNAME_LEN, "Username length valid"); if (opts.debug) { pam_syslog(pamh, LOG_DEBUG, "Attempting face authentication for %s", user); } /* Attempt face authentication */ ret = authenticate_face(pamh, user, &opts); assert_condition(ret == PAM_SUCCESS || ret == PAM_AUTH_ERR || ret == PAM_AUTHINFO_UNAVAIL, "Valid return value"); /* Handle fallback */ if (ret != PAM_SUCCESS && opts.fallback_password) { if (opts.debug) { pam_syslog(pamh, LOG_DEBUG, "Face auth failed, allowing password fallback"); } return PAM_IGNORE; } return ret; } /* * PAM credential management (no-op for face auth) */ PAM_EXTERN int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* Assertions */ assert(pamh != NULL); assert(argv != NULL || argc == 0); (void)pamh; (void)flags; (void)argc; (void)argv; return PAM_SUCCESS; } /* * PAM account management (no-op) */ PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* Assertions */ assert(pamh != NULL); assert(argv != NULL || argc == 0); (void)pamh; (void)flags; (void)argc; (void)argv; return PAM_SUCCESS; } /* * PAM session management (no-op) */ PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* Assertions */ assert(pamh != NULL); assert(argv != NULL || argc == 0); (void)pamh; (void)flags; (void)argc; (void)argv; return PAM_SUCCESS; } PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) { /* Assertions */ assert(pamh != NULL); assert(argv != NULL || argc == 0); (void)pamh; (void)flags; (void)argc; (void)argv; return PAM_SUCCESS; }