diff --git a/pam-1.3.1-pam-namespace-rebase.patch b/pam-1.3.1-pam-namespace-rebase.patch index 356f54e..a6cb4dc 100644 --- a/pam-1.3.1-pam-namespace-rebase.patch +++ b/pam-1.3.1-pam-namespace-rebase.patch @@ -1,21 +1,73 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/namespace.init.pam-namespace-rebase Linux-PAM-1.3.1/modules/pam_namespace/namespace.init --- Linux-PAM-1.3.1/modules/pam_namespace/namespace.init.pam-namespace-rebase 2017-02-10 11:10:15.000000000 +0100 -+++ Linux-PAM-1.3.1/modules/pam_namespace/namespace.init 2025-06-17 12:50:04.962542096 +0200 -@@ -15,8 +15,8 @@ if [ "$3" = 1 ]; then - gid=$(echo "$passwd" | cut -f4 -d":") - cp -rT /etc/skel "$homedir" - chown -R "$user":"$gid" "$homedir" ++++ Linux-PAM-1.3.1/modules/pam_namespace/namespace.init 2025-08-06 10:54:56.591532134 +0200 +@@ -1,25 +1,43 @@ + #!/bin/sh +-# It receives polydir path as $1, the instance path as $2, +-# a flag whether the instance dir was newly created (0 - no, 1 - yes) in $3, +-# and user name in $4. ++# It receives as arguments: ++# - $1 polydir path (see WARNING below) ++# - $2 instance path (see WARNING below) ++# - $3 flag whether the instance dir was newly created (0 - no, 1 - yes) ++# - $4 user name ++# - $5 flag whether the polydir path ($1) is safe (0 - unsafe, 1 -safe) ++# - $6 flag whether the instance path ($2) is safe (0 - unsafe, 1 - safe) ++# ++# WARNING: This script is invoked with full root privileges. Accessing ++# the polydir ($1) and the instance ($2) directories in this context may be ++# extremely dangerous as those can be under user control. The flags $5 and $6 ++# are provided to let you know if all the segments part of the path (except the ++# last one) are owned by root and are writable by root only. If the path does ++# not meet these criteria, you expose yourself to possible symlink attacks when ++# accessing these path. ++# However, even if the path components are safe, the content of the ++# directories may still be owned/writable by a user, so care must be taken! + # + # The following section will copy the contents of /etc/skel if this is a + # newly created home directory. +-if [ "$3" = 1 ]; then +- # This line will fix the labeling on all newly created directories +- [ -x /sbin/restorecon ] && /sbin/restorecon "$1" +- user="$4" +- passwd=$(getent passwd "$user") +- homedir=$(echo "$passwd" | cut -f6 -d":") +- if [ "$1" = "$homedir" ]; then +- gid=$(echo "$passwd" | cut -f4 -d":") +- cp -rT /etc/skel "$homedir" +- chown -R "$user":"$gid" "$homedir" - mask=$(awk '/^UMASK/{gsub("#.*$", "", $2); print $2; exit}' /etc/login.defs) - mode=$(printf "%o" $((0777 & ~$mask))) -+ mask=$(sed -E -n 's/^UMASK[[:space:]]+([^#[:space:]]+).*/\1/p' /etc/login.defs) -+ mode=$(printf "%o" $((0777 & ~mask))) - chmod ${mode:-700} "$homedir" - [ -x /sbin/restorecon ] && /sbin/restorecon -R "$homedir" - fi +- chmod ${mode:-700} "$homedir" +- [ -x /sbin/restorecon ] && /sbin/restorecon -R "$homedir" +- fi +-fi + ++# Executes only if the polydir path is safe ++if [ "$5" = 1 ]; then ++ ++ if [ "$3" = 1 ]; then ++ # This line will fix the labeling on all newly created directories ++ [ -x /sbin/restorecon ] && /sbin/restorecon "$1" ++ user="$4" ++ passwd=$(getent passwd "$user") ++ homedir=$(echo "$passwd" | cut -f6 -d":") ++ if [ "$1" = "$homedir" ]; then ++ gid=$(echo "$passwd" | cut -f4 -d":") ++ cp -rT /etc/skel "$homedir" ++ chown -R "$user":"$gid" "$homedir" ++ mask=$(sed -E -n 's/^UMASK[[:space:]]+([^#[:space:]]+).*/\1/p' /etc/login.defs) ++ mode=$(printf "%o" $((0777 & ~mask))) ++ chmod ${mode:-700} "$homedir" ++ [ -x /sbin/restorecon ] && /sbin/restorecon -R "$homedir" ++ fi ++ fi ++fi + exit 0 diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-rebase Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c ---- Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-rebase 2025-06-17 12:50:04.954195973 +0200 -+++ Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c 2025-06-17 12:51:08.445616548 +0200 -@@ -34,9 +34,250 @@ +--- Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-rebase 2025-08-06 10:54:56.579491315 +0200 ++++ Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c 2025-08-06 11:01:58.518495367 +0200 +@@ -34,9 +34,380 @@ #define _ATFILE_SOURCE @@ -26,6 +78,8 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb #include "pam_namespace.h" #include "argv_parse.h" ++#define MAGIC_LNK_FD_SIZE 64 ++ +/* --- evaluating all files in VENDORDIR/security/namespace.d and /etc/security/namespace.d --- */ +static const char *base_name(const char *path) +{ @@ -60,7 +114,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb +static int protect_mount(int dfd, const char *path, struct instance_data *idata) +{ + struct protect_dir_s *dir = idata->protect_dirs; -+ char tmpbuf[64]; ++ char tmpbuf[MAGIC_LNK_FD_SIZE]; + + while (dir != NULL) { + if (strcmp(path, dir->dir) == 0) { @@ -106,56 +160,106 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + return 0; +} + -+static int protect_dir(const char *path, mode_t mode, int do_mkdir, ++/* ++ * Returns a fd to the given absolute path, acquired securely. This means: ++ * - iterating on each segment of the path, ++ * - not following user symlinks, ++ * - using race-free operations. ++ * ++ * Takes a bit mask to specify the operation mode: ++ * - SECURE_OPENDIR_PROTECT: call protect_mount() on each unsafe segment of path ++ * - SECURE_OPENDIR_MKDIR: create last segment of path if does not exist ++ * - SECURE_OPENDIR_FULL_FD: open the directory with O_RDONLY instead of O_PATH, ++ * allowing more operations to be done with the returned fd ++ * ++ * Be aware that using SECURE_OPENDIR_PROTECT: ++ * - will modify some external state (global structure...) and should not be ++ * called in cleanup code paths. See wrapper secure_opendir_stateless() ++ * - need a non-NULL idata to call protect_mount() ++ */ ++static int secure_opendir(const char *path, int opm, mode_t mode, + struct instance_data *idata) +{ -+ char *p = strdup(path); ++ char *p; + char *d; -+ char *dir = p; -+ int dfd = AT_FDCWD; ++ char *dir; ++ int dfd = -1; + int dfd_next; + int save_errno; -+ int flags = O_RDONLY | O_DIRECTORY; ++ int flags = O_DIRECTORY | O_CLOEXEC; + int rv = -1; + struct stat st; + -+ if (p == NULL) { -+ return -1; -+ } ++ if (opm & SECURE_OPENDIR_FULL_FD) ++ flags |= O_RDONLY; ++ else ++ flags |= O_PATH; + -+ if (*dir == '/') { -+ dfd = open("/", flags); -+ if (dfd == -1) { -+ goto error; -+ } -+ dir++; /* assume / is safe */ ++ /* Check for args consistency */ ++ if ((opm & SECURE_OPENDIR_PROTECT) && idata == NULL) ++ return -1; ++ ++ /* Accept only absolute paths */ ++ if (*path != '/') ++ return -1; ++ ++ dir = p = strdup(path); ++ if (p == NULL) ++ return -1; ++ ++ /* Assume '/' is safe */ ++ dfd = open("/", flags); ++ if (dfd == -1) ++ goto error; ++ ++ /* Needed to not loop too far and call openat() on NULL */ ++ strip_trailing_slashes(p); ++ ++ dir++; ++ ++ /* In case path is '/' */ ++ if (*dir == '\0') { ++ free(p); ++ return dfd; + } + + while ((d=strchr(dir, '/')) != NULL) { + *d = '\0'; ++ + dfd_next = openat(dfd, dir, flags); -+ if (dfd_next == -1) { ++ if (dfd_next == -1) ++ goto error; ++ ++ if (fstat(dfd_next, &st) != 0) { ++ close(dfd_next); + goto error; + } + -+ if (dfd != AT_FDCWD) -+ close(dfd); -+ dfd = dfd_next; -+ -+ if (fstat(dfd, &st) != 0) { -+ goto error; -+ } -+ -+ if (flags & O_NOFOLLOW) { ++ if ((flags & O_NOFOLLOW) && (opm & SECURE_OPENDIR_PROTECT)) { + /* we are inside user-owned dir - protect */ -+ if (protect_mount(dfd, p, idata) == -1) ++ if (protect_mount(dfd_next, p, idata) == -1) { ++ close(dfd_next); + goto error; -+ } else if (st.st_uid != 0 || st.st_gid != 0 || -+ (st.st_mode & S_IWOTH)) { ++ } ++ /* ++ * Reopen the directory to obtain a new descriptor ++ * after protect_mount(), this is necessary in cases ++ * when another directory is going to be mounted over ++ * the given path. ++ */ ++ close(dfd_next); ++ dfd_next = openat(dfd, dir, flags); ++ if (dfd_next == -1) ++ goto error; ++ } else if (st.st_uid != 0 ++ || (st.st_mode & (S_IWGRP|S_IWOTH))) { + /* do not follow symlinks on subdirectories */ + flags |= O_NOFOLLOW; + } + ++ close(dfd); ++ dfd = dfd_next; ++ + *d = '/'; + dir = d + 1; + } @@ -163,13 +267,14 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + rv = openat(dfd, dir, flags); + + if (rv == -1) { -+ if (!do_mkdir || mkdirat(dfd, dir, mode) != 0) { ++ if ((opm & SECURE_OPENDIR_MKDIR) && mkdirat(dfd, dir, mode) == 0) ++ rv = openat(dfd, dir, flags); ++ ++ if (rv == -1) + goto error; -+ } -+ rv = openat(dfd, dir, flags); + } + -+ if (flags & O_NOFOLLOW) { ++ if ((flags & O_NOFOLLOW) && (opm & SECURE_OPENDIR_PROTECT)) { + /* we are inside user-owned dir - protect */ + if (protect_mount(rv, p, idata) == -1) { + save_errno = errno; @@ -177,18 +282,95 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + rv = -1; + errno = save_errno; + } ++ /* ++ * Reopen the directory to obtain a new descriptor after ++ * protect_mount(), this is necessary in cases when another ++ * directory is going to be mounted over the given path. ++ */ ++ close(rv); ++ rv = openat(dfd, dir, flags); + } + +error: + save_errno = errno; + free(p); -+ if (dfd != AT_FDCWD && dfd >= 0) ++ if (dfd >= 0) + close(dfd); + errno = save_errno; + + return rv; +} + ++/* ++ * Returns a fd to the given path, acquired securely. ++ * It can be called in all situations, including in cleanup code paths, as ++ * it does not modify external state (no access to global structures...). ++ */ ++static int secure_opendir_stateless(const char *path) ++{ ++ return secure_opendir(path, 0, 0, NULL); ++} ++ ++/* ++ * Umount securely the given path, even if the directories along ++ * the path are under user control. It should protect against ++ * symlinks attacks and race conditions. ++ */ ++static int secure_umount(const char *path) ++{ ++ int save_errno; ++ int rv = -1; ++ int dfd = -1; ++ char s_path[MAGIC_LNK_FD_SIZE]; ++ ++ dfd = secure_opendir_stateless(path); ++ if (dfd == -1) ++ return rv; ++ ++ if (pam_sprintf(s_path, "/proc/self/fd/%d", dfd) < 0) ++ goto error; ++ ++ /* ++ * We still have a fd open to path itself, ++ * so we need to do a lazy umount. ++ */ ++ rv = umount2(s_path, MNT_DETACH); ++ ++error: ++ save_errno = errno; ++ close(dfd); ++ errno = save_errno; ++ return rv; ++} ++ ++/* ++ * Rmdir the given path securely, protecting against symlinks attacks ++ * and race conditions. ++ * This function is currently called only in cleanup code paths where ++ * any errors returned are not handled, so do not handle them either. ++ * Basically, try to rmdir the path on a best-effort basis. ++ */ ++static void secure_try_rmdir(const char *path) ++{ ++ int dfd; ++ char *buf; ++ char *parent; ++ ++ buf = strdup(path); ++ if (buf == NULL) ++ return; ++ ++ parent = dirname(buf); ++ ++ dfd = secure_opendir_stateless(parent); ++ if (dfd >= 0) { ++ unlinkat(dfd, base_name(path), AT_REMOVEDIR); ++ close(dfd); ++ } ++ ++ free(buf); ++} ++ +/* Evaluating a list of files which have to be parsed in the right order: + * + * - If etc/security/namespace.d/@filename@.conf exists, then @@ -266,7 +448,16 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb /* * Adds an entry for a polyinstantiated directory to the linked list of * polyinstantiated directories. It is called from process_line() while -@@ -106,7 +347,7 @@ static void cleanup_protect_data(pam_han +@@ -88,7 +459,7 @@ static void unprotect_dirs(struct protec + struct protect_dir_s *next; + + while (dir != NULL) { +- umount(dir->dir); ++ secure_umount(dir->dir); + free(dir->dir); + next = dir->next; + free(dir); +@@ -106,7 +477,7 @@ static void cleanup_protect_data(pam_han unprotect_dirs(data); } @@ -275,7 +466,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb { const char *src = orig; char *dst; -@@ -117,7 +358,7 @@ static char *expand_variables(const char +@@ -117,7 +488,7 @@ static char *expand_variables(const char if (*src == '$') { int i; for (i = 0; var_names[i]; i++) { @@ -284,7 +475,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (strncmp(var_names[i], src+1, namelen) == 0) { dstlen += strlen(var_values[i]) - 1; /* $ */ src += namelen; -@@ -135,7 +376,7 @@ static char *expand_variables(const char +@@ -135,7 +506,7 @@ static char *expand_variables(const char if (c == '$') { int i; for (i = 0; var_names[i]; i++) { @@ -293,7 +484,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (strncmp(var_names[i], src+1, namelen) == 0) { dst = stpcpy(dst, var_values[i]); --dst; -@@ -219,8 +460,7 @@ static int parse_iscript_params(char *pa +@@ -219,8 +590,7 @@ static int parse_iscript_params(char *pa if (*params != '\0') { if (*params != '/') { /* path is relative to NAMESPACE_D_DIR */ @@ -303,7 +494,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb } else { poly->init_script = strdup(params); } -@@ -259,7 +499,7 @@ static int filter_mntopts(const char *op +@@ -259,7 +629,7 @@ static int filter_mntopts(const char *op do { size_t len; @@ -312,7 +503,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb end = strchr(opts, ','); if (end == NULL) { -@@ -268,7 +508,7 @@ static int filter_mntopts(const char *op +@@ -268,7 +638,7 @@ static int filter_mntopts(const char *op len = end - opts; } @@ -321,7 +512,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (mntflags[i].len != len) continue; if (memcmp(mntflags[i].name, opts, len) == 0) { -@@ -302,9 +542,9 @@ static int parse_method(char *method, st +@@ -302,9 +672,9 @@ static int parse_method(char *method, st { enum polymethod pm; char *sptr = NULL; @@ -333,7 +524,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb "shared", "mntopts", NULL }; static const unsigned int flag_values[] = { POLYDIR_CREATE, POLYDIR_NOINIT, POLYDIR_ISCRIPT, POLYDIR_SHARED, POLYDIR_MNTOPTS }; -@@ -329,7 +569,7 @@ static int parse_method(char *method, st +@@ -329,7 +699,7 @@ static int parse_method(char *method, st while ((flag=strtok_r(NULL, ":", &sptr)) != NULL) { for (i = 0; flag_names[i]; i++) { @@ -342,7 +533,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (strncmp(flag, flag_names[i], namelen) == 0) { poly->flags |= flag_values[i]; -@@ -375,27 +615,27 @@ static int parse_method(char *method, st +@@ -375,27 +745,27 @@ static int parse_method(char *method, st * of the namespace configuration file. It skips over comments and incomplete * or malformed lines. It processes a valid line with information on * polyinstantiating a directory by populating appropriate fields of a @@ -374,7 +565,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb line++; /* -@@ -431,22 +671,19 @@ static int process_line(char *line, cons +@@ -431,22 +801,19 @@ static int process_line(char *line, cons goto erralloc; } @@ -401,7 +592,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb goto skipping; } -@@ -461,19 +698,16 @@ static int process_line(char *line, cons +@@ -461,19 +828,16 @@ static int process_line(char *line, cons /* * Expand $HOME and $USER in poly dir and instance dir prefix */ @@ -425,7 +616,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb goto erralloc; } -@@ -483,15 +717,8 @@ static int process_line(char *line, cons +@@ -483,15 +847,8 @@ static int process_line(char *line, cons pam_syslog(idata->pamh, LOG_DEBUG, "Expanded instance prefix: '%s'", instance_prefix); } @@ -443,7 +634,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (dir[0] == '\0' || rdir[0] == '\0') { pam_syslog(idata->pamh, LOG_NOTICE, "Invalid polydir"); -@@ -502,26 +729,19 @@ static int process_line(char *line, cons +@@ -502,26 +859,15 @@ static int process_line(char *line, cons * Populate polyinstantiated directory structure with appropriate * pathnames and the method with which to polyinstantiate. */ @@ -467,19 +658,15 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - goto skipping; - } - strcat(poly->instance_prefix, "XXXXXX"); -+#define COPY_STR(dst, src, apd) \ -+ pam_sprintf((dst), "%s%s", (src), (apd)) -+ -+ if (COPY_STR(poly->dir, dir, "") < 0 -+ || COPY_STR(poly->rdir, rdir, "") < 0 -+ || COPY_STR(poly->instance_prefix, instance_prefix, -+ poly->method == TMPDIR ? "XXXXXX" : "") < 0) { ++ if (pam_sprintf(poly->dir, "%s", dir) < 0 ++ || pam_sprintf(poly->rdir, "%s", rdir) < 0 ++ || pam_sprintf(poly->instance_prefix, "%s", instance_prefix) < 0) { + pam_syslog(idata->pamh, LOG_NOTICE, "Pathnames too long"); + goto skipping; } /* -@@ -545,7 +765,7 @@ static int process_line(char *line, cons +@@ -545,7 +891,7 @@ static int process_line(char *line, cons if (uids) { uid_t *uidptr; const char *ustr, *sstr; @@ -488,7 +675,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (*uids == '~') { poly->flags |= POLYDIR_EXCLUSIVE; -@@ -554,8 +774,13 @@ static int process_line(char *line, cons +@@ -554,8 +900,13 @@ static int process_line(char *line, cons for (count = 0, ustr = sstr = uids; sstr; ustr = sstr + 1, count++) sstr = strchr(ustr, ','); @@ -503,7 +690,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb uidptr = poly->uid; if (uidptr == NULL) { goto erralloc; -@@ -622,8 +847,6 @@ static int parse_config_file(struct inst +@@ -622,8 +973,6 @@ static int parse_config_file(struct inst char *line; int retval; size_t len = 0; @@ -512,7 +699,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb size_t n; /* -@@ -662,13 +885,16 @@ static int parse_config_file(struct inst +@@ -662,13 +1011,16 @@ static int parse_config_file(struct inst * process_line to process each line. */ @@ -535,7 +722,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb n = 0; for (;;) { if (idata->flags & PAMNS_DEBUG) -@@ -678,7 +904,6 @@ static int parse_config_file(struct inst +@@ -678,7 +1030,6 @@ static int parse_config_file(struct inst if (fil == NULL) { pam_syslog(idata->pamh, LOG_ERR, "Error opening config file %s", confname); @@ -543,7 +730,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb free(rhome); free(home); return PAM_SERVICE_ERR; -@@ -696,7 +921,6 @@ static int parse_config_file(struct inst +@@ -696,7 +1047,6 @@ static int parse_config_file(struct inst "Error processing conf file %s line %s", confname, line); fclose(fil); free(line); @@ -551,7 +738,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb free(rhome); free(home); return PAM_SERVICE_ERR; -@@ -705,14 +929,18 @@ static int parse_config_file(struct inst +@@ -705,14 +1055,18 @@ static int parse_config_file(struct inst fclose(fil); free(line); @@ -574,7 +761,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb free(rhome); free(home); -@@ -738,7 +966,7 @@ static int parse_config_file(struct inst +@@ -738,7 +1092,7 @@ static int parse_config_file(struct inst /* @@ -583,9 +770,27 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb * directory's list of override uids. If the uid is one of the override * uids for the polyinstantiated directory, polyinstantiation is not * performed for that user for that directory. -@@ -795,11 +1023,11 @@ static char *md5hash(const char *instnam +@@ -794,23 +1148,37 @@ static char *md5hash(const char *instnam + } #ifdef WITH_SELINUX ++static char *secure_getfilecon(pam_handle_t *pamh, const char *dir) ++{ ++ char *ctx = NULL; ++ int dfd = secure_opendir(dir, SECURE_OPENDIR_FULL_FD, 0, NULL); ++ if (dfd < 0) { ++ pam_syslog(pamh, LOG_ERR, "Error getting fd to %s: %m", dir); ++ return NULL; ++ } ++ if (fgetfilecon(dfd, &ctx) < 0) ++ ctx = NULL; ++ if (ctx == NULL) ++ pam_syslog(pamh, LOG_ERR, ++ "Error getting poly dir context for %s: %m", dir); ++ close(dfd); ++ return ctx; ++} ++ static int form_context(const struct polydir_s *polyptr, - security_context_t *i_context, security_context_t *origcon, + char **i_context, char **origcon, @@ -597,7 +802,20 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb security_class_t tclass; /* -@@ -842,6 +1070,12 @@ static int form_context(const struct pol + * Get the security context of the directory to polyinstantiate. + */ +- rc = getfilecon(polyptr->dir, origcon); +- if (rc < 0 || *origcon == NULL) { +- pam_syslog(idata->pamh, LOG_ERR, +- "Error getting poly dir context, %m"); ++ *origcon = secure_getfilecon(idata->pamh, polyptr->dir); ++ if (*origcon == NULL) + return PAM_SESSION_ERR; +- } + + if (polyptr->method == USER) return PAM_SUCCESS; + +@@ -842,6 +1210,12 @@ static int form_context(const struct pol if (polyptr->method == CONTEXT) { tclass = string_to_security_class("dir"); @@ -610,7 +828,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (security_compute_member(scon, *origcon, tclass, i_context) < 0) { -@@ -878,7 +1112,7 @@ static int form_context(const struct pol +@@ -878,7 +1252,7 @@ static int form_context(const struct pol goto fail; } if (context_range_set(fcontext, context_range_get(scontext)) != 0) { @@ -619,7 +837,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb goto fail; } *i_context=strdup(context_str(fcontext)); -@@ -895,6 +1129,7 @@ static int form_context(const struct pol +@@ -895,34 +1269,58 @@ static int form_context(const struct pol return rc; } /* Should never get here */ @@ -627,16 +845,54 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb return PAM_SUCCESS; } #endif -@@ -908,7 +1143,7 @@ static int form_context(const struct pol + + /* +- * poly_name returns the name of the polyinstantiated instance directory ++ * From the instance differentiation string, set in the polyptr structure: ++ * - the absolute path to the instance dir, ++ * - the absolute path to the previous dir (parent), ++ * - the instance name (may be different than the instance differentiation string) ++ */ ++static int set_polydir_paths(struct polydir_s *polyptr, const char *inst_differentiation) ++{ ++ char *tmp; ++ ++ if (pam_sprintf(polyptr->instance_absolute, "%s%s", ++ polyptr->instance_prefix, inst_differentiation) < 0) ++ return -1; ++ ++ polyptr->instname = strrchr(polyptr->instance_absolute, '/') + 1; ++ ++ if (pam_sprintf(polyptr->instance_parent, "%s", polyptr->instance_absolute) < 0) ++ return -1; ++ ++ tmp = strrchr(polyptr->instance_parent, '/') + 1; ++ *tmp = '\0'; ++ ++ return 0; ++} ++ ++/* ++ * Set the name of the polyinstantiated instance directory + * based on the method used for polyinstantiation (user, context or level) + * In addition, the function also returns the security contexts of the + * original directory to polyinstantiate and the polyinstantiated instance + * directory. */ #ifdef WITH_SELINUX - static int poly_name(const struct polydir_s *polyptr, char **i_name, +-static int poly_name(const struct polydir_s *polyptr, char **i_name, - security_context_t *i_context, security_context_t *origcon, -+ char **i_context, char **origcon, - struct instance_data *idata) +- struct instance_data *idata) ++static int poly_name(struct polydir_s *polyptr, char **i_context, ++ char **origcon, struct instance_data *idata) #else - static int poly_name(const struct polydir_s *polyptr, char **i_name, -@@ -919,7 +1154,7 @@ static int poly_name(const struct polydi +-static int poly_name(const struct polydir_s *polyptr, char **i_name, +- struct instance_data *idata) ++static int poly_name(struct polydir_s *polyptr, struct instance_data *idata) + #endif + { + int rc; ++ char *inst_differentiation = NULL; char *hash = NULL; enum polymethod pm; #ifdef WITH_SELINUX @@ -644,20 +900,23 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + char *rawcon = NULL; #endif - *i_name = NULL; -@@ -956,10 +1191,8 @@ static int poly_name(const struct polydi +- *i_name = NULL; + #ifdef WITH_SELINUX + *i_context = NULL; + *origcon = NULL; +@@ -956,10 +1354,8 @@ static int poly_name(const struct polydi switch (pm) { case USER: - if (asprintf(i_name, "%s", idata->user) < 0) { - *i_name = NULL; -+ if ((*i_name = strdup(idata->user)) == NULL) ++ if ((inst_differentiation = strdup(idata->user)) == NULL) goto fail; - } break; #ifdef WITH_SELINUX -@@ -969,17 +1202,12 @@ static int poly_name(const struct polydi +@@ -969,26 +1365,25 @@ static int poly_name(const struct polydi pam_syslog(idata->pamh, LOG_ERR, "Error translating directory context"); goto fail; } @@ -673,16 +932,48 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - } - } + if (polyptr->flags & POLYDIR_SHARED) -+ *i_name = strdup(rawcon); ++ inst_differentiation = strdup(rawcon); + else -+ *i_name = pam_asprintf("%s_%s", rawcon, idata->user); -+ if (*i_name == NULL) ++ inst_differentiation = pam_asprintf("%s_%s", rawcon, idata->user); ++ if (inst_differentiation == NULL) + goto fail; break; #endif /* WITH_SELINUX */ -@@ -1009,11 +1237,12 @@ static int poly_name(const struct polydi - *i_name = hash; + + case TMPDIR: ++ if ((inst_differentiation = strdup("XXXXXX")) == NULL) ++ goto fail; ++ goto success; ++ + case TMPFS: +- if ((*i_name=strdup("")) == NULL) ++ if ((inst_differentiation=strdup("")) == NULL) + goto fail; +- return PAM_SUCCESS; ++ goto success; + + default: + if (idata->flags & PAMNS_DEBUG) +@@ -997,31 +1392,37 @@ static int poly_name(const struct polydi + } + + if (idata->flags & PAMNS_DEBUG) +- pam_syslog(idata->pamh, LOG_DEBUG, "poly_name %s", *i_name); ++ pam_syslog(idata->pamh, LOG_DEBUG, "poly_name %s", inst_differentiation); + +- if ((idata->flags & PAMNS_GEN_HASH) || strlen(*i_name) > NAMESPACE_MAX_DIR_LEN) { +- hash = md5hash(*i_name, idata); ++ if ((idata->flags & PAMNS_GEN_HASH) || strlen(inst_differentiation) > NAMESPACE_MAX_DIR_LEN) { ++ hash = md5hash(inst_differentiation, idata); + if (hash == NULL) { + goto fail; + } + if (idata->flags & PAMNS_GEN_HASH) { +- free(*i_name); +- *i_name = hash; ++ free(inst_differentiation); ++ inst_differentiation = hash; hash = NULL; } else { - char *newname; @@ -691,19 +982,42 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + char *newname = + pam_asprintf("%.*s_%s", + NAMESPACE_MAX_DIR_LEN - 1 - (int)strlen(hash), -+ *i_name, hash); ++ inst_differentiation, hash); + if (newname == NULL) goto fail; - } - free(*i_name); - *i_name = newname; +- free(*i_name); +- *i_name = newname; ++ free(inst_differentiation); ++ inst_differentiation = newname; } -@@ -1038,137 +1267,6 @@ fail: + } +- rc = PAM_SUCCESS; + ++success: ++ if (set_polydir_paths(polyptr, inst_differentiation) == -1) ++ goto fail; ++ ++ rc = PAM_SUCCESS; + fail: + free(hash); ++ free(inst_differentiation); + #ifdef WITH_SELINUX + freecon(rawcon); + #endif +@@ -1032,188 +1433,109 @@ fail: + freecon(*origcon); + *origcon = NULL; + #endif +- free(*i_name); +- *i_name = NULL; + } return rc; } -static int protect_mount(int dfd, const char *path, struct instance_data *idata) --{ ++static int check_inst_parent(int dfd, struct instance_data *idata) + { - struct protect_dir_s *dir = idata->protect_dirs; - char tmpbuf[64]; - @@ -721,38 +1035,63 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - } - - dir->dir = strdup(path); -- ++ struct stat instpbuf; + - if (dir->dir == NULL) { - free(dir); - return -1; - } -- ++ /* ++ * Stat the instance parent directory to make sure it's writable by ++ * root only (unless the admin explicitly instructs to ignore the ++ * instance parent mode by the "ignore_instance_parent_mode" argument). ++ */ + - snprintf(tmpbuf, sizeof(tmpbuf), "/proc/self/fd/%d", dfd); -- ++ if (idata->flags & PAMNS_IGN_INST_PARENT_MODE) ++ return PAM_SUCCESS; + - if (idata->flags & PAMNS_DEBUG) { - pam_syslog(idata->pamh, LOG_INFO, - "Protect mount of %s over itself", path); -- } -- ++ if (fstat(dfd, &instpbuf) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, ++ "Error accessing instance parent, %m"); ++ return PAM_SESSION_ERR; + } + - if (mount(tmpbuf, tmpbuf, NULL, MS_BIND, NULL) != 0) { - int save_errno = errno; -- pam_syslog(idata->pamh, LOG_ERR, ++ if ((instpbuf.st_mode & (S_IRWXU|S_IRWXG|S_IRWXO)) || instpbuf.st_uid != 0) { + pam_syslog(idata->pamh, LOG_ERR, - "Protect mount of %s failed: %m", tmpbuf); - free(dir->dir); - free(dir); - errno = save_errno; - return -1; -- } -- ++ "Mode of inst parent not 000 or owner not root"); ++ return PAM_SESSION_ERR; + } + - dir->next = idata->protect_dirs; - idata->protect_dirs = dir; - - return 0; --} -- ++ return PAM_SUCCESS; + } + -static int protect_dir(const char *path, mode_t mode, int do_mkdir, - struct instance_data *idata) --{ ++/* ++ * Check for a given absolute path that all segments except the last one are: ++ * 1. a directory owned by root and not writable by group or others ++ * 2. a symlink owned by root and referencing a directory respecting 1. ++ * Returns 0 if safe, -1 is unsafe. ++ * If the path is not accessible (does not exist, hidden under a mount...), ++ * returns -1 (unsafe). ++ */ ++static int check_safe_path(const char *path, struct instance_data *idata) + { - char *p = strdup(path); - char *d; - char *dir = p; @@ -774,7 +1113,11 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - } - dir++; /* assume / is safe */ - } -- ++ char *p = strdup(path); ++ char *d; ++ char *dir = p; ++ struct stat st; + - while ((d=strchr(dir, '/')) != NULL) { - *d = '\0'; - dfd_next = openat(dfd, dir, flags); @@ -785,11 +1128,36 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - if (dfd != AT_FDCWD) - close(dfd); - dfd = dfd_next; -- ++ if (p == NULL) ++ return -1; + - if (fstat(dfd, &st) != 0) { - goto error; - } -- ++ /* Check path is absolute */ ++ if (p[0] != '/') ++ goto error; ++ ++ strip_trailing_slashes(p); ++ ++ /* Last segment of the path may be owned by the user */ ++ if ((d = strrchr(dir, '/')) != NULL) ++ *d = '\0'; ++ ++ while ((d=strrchr(dir, '/')) != NULL) { ++ ++ /* Do not follow symlinks */ ++ if (lstat(dir, &st) != 0) ++ goto error; ++ ++ if (S_ISLNK(st.st_mode)) { ++ if (st.st_uid != 0) { ++ if (idata->flags & PAMNS_DEBUG) ++ pam_syslog(idata->pamh, LOG_DEBUG, ++ "Path deemed unsafe: Symlink %s should be owned by root", dir); ++ goto error; ++ } + - if (flags & O_NOFOLLOW) { - /* we are inside user-owned dir - protect */ - if (protect_mount(dfd, p, idata) == -1) @@ -799,20 +1167,39 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - /* do not follow symlinks on subdirectories */ - flags |= O_NOFOLLOW; - } -- ++ /* Follow symlinks */ ++ if (stat(dir, &st) != 0) ++ goto error; ++ } + - *d = '/'; - dir = d + 1; - } -- ++ if (!S_ISDIR(st.st_mode)) { ++ if (idata->flags & PAMNS_DEBUG) ++ pam_syslog(idata->pamh, LOG_DEBUG, ++ "Path deemed unsafe: %s is expected to be a directory", dir); ++ goto error; ++ } + - rv = openat(dfd, dir, flags); -- ++ if (st.st_uid != 0 || ++ ((st.st_mode & (S_IWGRP|S_IWOTH)) && !(st.st_mode & S_ISVTX))) { ++ if (idata->flags & PAMNS_DEBUG) ++ pam_syslog(idata->pamh, LOG_DEBUG, ++ "Path deemed unsafe: %s should be owned by root, and not be writable by group or others", dir); ++ goto error; ++ } + - if (rv == -1) { - if (!do_mkdir || mkdirat(dfd, dir, mode) != 0) { - goto error; - } - rv = openat(dfd, dir, flags); - } -- ++ *d = '\0'; ++ } + - if (flags & O_NOFOLLOW) { - /* we are inside user-owned dir - protect */ - if (protect_mount(rv, p, idata) == -1) { @@ -822,8 +1209,10 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - errno = save_errno; - } - } -- --error: ++ free(p); ++ return 0; + + error: - save_errno = errno; - free(p); - if (dfd != AT_FDCWD && dfd >= 0) @@ -833,25 +1222,57 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - return rv; -} - - static int check_inst_parent(char *ipath, struct instance_data *idata) - { - struct stat instpbuf; -@@ -1180,13 +1278,12 @@ static int check_inst_parent(char *ipath - * admin explicitly instructs to ignore the instance parent - * mode by the "ignore_instance_parent_mode" argument). - */ +-static int check_inst_parent(char *ipath, struct instance_data *idata) +-{ +- struct stat instpbuf; +- char *inst_parent, *trailing_slash; +- int dfd; +- /* +- * stat the instance parent path to make sure it exists +- * and is a directory. Check that its mode is 000 (unless the +- * admin explicitly instructs to ignore the instance parent +- * mode by the "ignore_instance_parent_mode" argument). +- */ - inst_parent = (char *) malloc(strlen(ipath)+1); -+ inst_parent = strdup(ipath); - if (!inst_parent) { - pam_syslog(idata->pamh, LOG_CRIT, "Error allocating pathname string"); - return PAM_SESSION_ERR; - } - +- if (!inst_parent) { +- pam_syslog(idata->pamh, LOG_CRIT, "Error allocating pathname string"); +- return PAM_SESSION_ERR; +- } +- - strcpy(inst_parent, ipath); - trailing_slash = strrchr(inst_parent, '/'); - if (trailing_slash) - *trailing_slash = '\0'; -@@ -1226,72 +1323,83 @@ static int inst_init(const struct polydi +- trailing_slash = strrchr(inst_parent, '/'); +- if (trailing_slash) +- *trailing_slash = '\0'; +- +- dfd = protect_dir(inst_parent, 0, 1, idata); +- +- if (dfd == -1 || fstat(dfd, &instpbuf) < 0) { +- pam_syslog(idata->pamh, LOG_ERR, +- "Error creating or accessing instance parent %s, %m", inst_parent); +- if (dfd != -1) +- close(dfd); +- free(inst_parent); +- return PAM_SESSION_ERR; +- } +- +- if ((idata->flags & PAMNS_IGN_INST_PARENT_MODE) == 0) { +- if ((instpbuf.st_mode & (S_IRWXU|S_IRWXG|S_IRWXO)) || instpbuf.st_uid != 0) { +- pam_syslog(idata->pamh, LOG_ERR, "Mode of inst parent %s not 000 or owner not root", +- inst_parent); +- close(dfd); +- free(inst_parent); +- return PAM_SESSION_ERR; +- } +- } +- close(dfd); +- free(inst_parent); +- return PAM_SUCCESS; ++ free(p); ++ return -1; + } + + /* +@@ -1226,72 +1548,87 @@ static int inst_init(const struct polydi struct instance_data *idata, int newdir) { pid_t rc, pid; @@ -916,7 +1337,11 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb + close_fds_pre_exec(idata); + + execle(init_script, init_script, -+ polyptr->dir, ipath, newdir?"1":"0", idata->user, NULL, envp); ++ polyptr->dir, ipath, ++ newdir ? "1":"0", idata->user, ++ (check_safe_path(polyptr->dir, idata) == -1) ? "0":"1", ++ (check_safe_path(ipath, idata) == -1) ? "0":"1", ++ NULL, envp); + _exit(1); + } else if (pid > 0) { + while (((rc = waitpid(pid, &status, 0)) == (pid_t)-1) && @@ -986,17 +1411,211 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb } static int create_polydir(struct polydir_s *polyptr, -@@ -1395,7 +1503,7 @@ static int create_polydir(struct polydir +@@ -1331,7 +1668,9 @@ static int create_polydir(struct polydir + } + #endif + +- rc = protect_dir(dir, mode, 1, idata); ++ rc = secure_opendir(dir, ++ SECURE_OPENDIR_PROTECT | SECURE_OPENDIR_MKDIR | SECURE_OPENDIR_FULL_FD, ++ mode, idata); + if (rc == -1) { + pam_syslog(idata->pamh, LOG_ERR, + "Error creating directory %s: %m", dir); +@@ -1356,9 +1695,9 @@ static int create_polydir(struct polydir + pam_syslog(idata->pamh, LOG_ERR, + "Error changing mode of directory %s: %m", dir); + close(rc); +- umount(dir); /* undo the eventual protection bind mount */ +- rmdir(dir); +- return PAM_SESSION_ERR; ++ secure_umount(dir); /* undo the eventual protection bind mount */ ++ secure_try_rmdir(dir); ++ return -1; + } + } + +@@ -1376,42 +1715,38 @@ static int create_polydir(struct polydir + pam_syslog(idata->pamh, LOG_ERR, + "Unable to change owner on directory %s: %m", dir); + close(rc); +- umount(dir); /* undo the eventual protection bind mount */ +- rmdir(dir); +- return PAM_SESSION_ERR; ++ secure_umount(dir); /* undo the eventual protection bind mount */ ++ secure_try_rmdir(dir); ++ return -1; + } + +- close(rc); +- + if (idata->flags & PAMNS_DEBUG) + pam_syslog(idata->pamh, LOG_DEBUG, + "Polydir owner %u group %u", uid, gid); + +- return PAM_SUCCESS; ++ return rc; + } + + /* +- * Create polyinstantiated instance directory (ipath). ++ * Create polyinstantiated instance directory. ++ * To protect against races, changes are done on a fd to the parent of the ++ * instance directory (dfd_iparent) and a relative path (polyptr->instname). ++ * The absolute path (polyptr->instance_absolute) is only updated when creating ++ * a tmpdir and used for logging purposes. */ #ifdef WITH_SELINUX - static int create_instance(struct polydir_s *polyptr, char *ipath, struct stat *statbuf, +-static int create_instance(struct polydir_s *polyptr, char *ipath, struct stat *statbuf, - security_context_t icontext, security_context_t ocontext, -+ const char *icontext, const char *ocontext, - struct instance_data *idata) +- struct instance_data *idata) ++static int create_instance(struct polydir_s *polyptr, int dfd_iparent, ++ struct stat *statbuf, const char *icontext, const char *ocontext, ++ struct instance_data *idata) #else - static int create_instance(struct polydir_s *polyptr, char *ipath, struct stat *statbuf, -@@ -1513,7 +1621,7 @@ static int ns_setup(struct polydir_s *po - char *instname = NULL; +-static int create_instance(struct polydir_s *polyptr, char *ipath, struct stat *statbuf, +- struct instance_data *idata) ++static int create_instance(struct polydir_s *polyptr, int dfd_iparent, ++ struct stat *statbuf, struct instance_data *idata) + #endif + { + struct stat newstatbuf; + int fd; + + /* +- * Check to make sure instance parent is valid. +- */ +- if (check_inst_parent(ipath, idata)) +- return PAM_SESSION_ERR; +- +- /* + * Create instance directory and set its security context to the context + * returned by the security policy. Set its mode and ownership + * attributes to match that of the original directory that is being +@@ -1419,29 +1754,39 @@ static int create_instance(struct polydi + */ + + if (polyptr->method == TMPDIR) { +- if (mkdtemp(polyptr->instance_prefix) == NULL) { +- pam_syslog(idata->pamh, LOG_ERR, "Error creating temporary instance %s, %m", +- polyptr->instance_prefix); +- polyptr->method = NONE; /* do not clean up! */ +- return PAM_SESSION_ERR; +- } +- /* copy the actual directory name to ipath */ +- strcpy(ipath, polyptr->instance_prefix); +- } else if (mkdir(ipath, S_IRUSR) < 0) { ++ char s_path[PATH_MAX]; ++ /* ++ * Create the template for mkdtemp() as a magic link based on ++ * our existing fd to avoid symlink attacks and races. ++ */ ++ if (pam_sprintf(s_path, "/proc/self/fd/%d/%s", dfd_iparent, polyptr->instname) < 0 ++ || mkdtemp(s_path) == NULL) { ++ pam_syslog(idata->pamh, LOG_ERR, ++ "Error creating temporary instance dir %s, %m", ++ polyptr->instance_absolute); ++ polyptr->method = NONE; /* do not clean up! */ ++ return PAM_SESSION_ERR; ++ } ++ ++ /* Copy the actual directory name to polyptr->instname */ ++ strcpy(polyptr->instname, base_name(s_path)); ++ } else if (mkdirat(dfd_iparent, polyptr->instname, S_IRUSR) < 0) { + if (errno == EEXIST) + return PAM_IGNORE; + else { + pam_syslog(idata->pamh, LOG_ERR, "Error creating %s, %m", +- ipath); ++ polyptr->instance_absolute); + return PAM_SESSION_ERR; + } + } + +- /* Open a descriptor to it to prevent races */ +- fd = open(ipath, O_DIRECTORY | O_RDONLY); ++ /* Open a descriptor to prevent races, based on our existing fd. */ ++ fd = openat(dfd_iparent, polyptr->instname, ++ O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (fd < 0) { +- pam_syslog(idata->pamh, LOG_ERR, "Error opening %s, %m", ipath); +- rmdir(ipath); ++ pam_syslog(idata->pamh, LOG_ERR, "Error opening %s, %m", ++ polyptr->instance_absolute); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + #ifdef WITH_SELINUX +@@ -1451,17 +1796,19 @@ static int create_instance(struct polydi + if (icontext) { + if (fsetfilecon(fd, icontext) < 0) { + pam_syslog(idata->pamh, LOG_ERR, +- "Error setting context of %s to %s", ipath, icontext); ++ "Error setting context of %s to %s", ++ polyptr->instance_absolute, icontext); + close(fd); +- rmdir(ipath); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + } else { + if (fsetfilecon(fd, ocontext) < 0) { + pam_syslog(idata->pamh, LOG_ERR, +- "Error setting context of %s to %s", ipath, ocontext); ++ "Error setting context of %s to %s", ++ polyptr->instance_absolute, ocontext); + close(fd); +- rmdir(ipath); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + } +@@ -1469,9 +1816,9 @@ static int create_instance(struct polydi + #endif + if (fstat(fd, &newstatbuf) < 0) { + pam_syslog(idata->pamh, LOG_ERR, "Error stating %s, %m", +- ipath); ++ polyptr->instance_absolute); + close(fd); +- rmdir(ipath); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + if (newstatbuf.st_uid != statbuf->st_uid || +@@ -1479,17 +1826,17 @@ static int create_instance(struct polydi + if (fchown(fd, statbuf->st_uid, statbuf->st_gid) < 0) { + pam_syslog(idata->pamh, LOG_ERR, + "Error changing owner for %s, %m", +- ipath); ++ polyptr->instance_absolute); + close(fd); +- rmdir(ipath); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + } + if (fchmod(fd, statbuf->st_mode & 07777) < 0) { + pam_syslog(idata->pamh, LOG_ERR, "Error changing mode for %s, %m", +- ipath); ++ polyptr->instance_absolute); + close(fd); +- rmdir(ipath); ++ unlinkat(dfd_iparent, polyptr->instname, AT_REMOVEDIR); + return PAM_SESSION_ERR; + } + close(fd); +@@ -1508,51 +1855,63 @@ static int ns_setup(struct polydir_s *po + struct instance_data *idata) + { + int retval; ++ int dfd_iparent = -1; ++ int dfd_ipath = -1; ++ int dfd_pptrdir = -1; + int newdir = 1; +- char *inst_dir = NULL; +- char *instname = NULL; ++ char s_ipath[MAGIC_LNK_FD_SIZE]; ++ char s_pptrdir[MAGIC_LNK_FD_SIZE]; struct stat statbuf; #ifdef WITH_SELINUX - security_context_t instcontext = NULL, origcontext = NULL; @@ -1004,9 +1623,11 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb #endif if (idata->flags & PAMNS_DEBUG) -@@ -1522,16 +1630,14 @@ static int ns_setup(struct polydir_s *po + pam_syslog(idata->pamh, LOG_DEBUG, + "Set namespace for directory %s", polyptr->dir); - retval = protect_dir(polyptr->dir, 0, 0, idata); +- retval = protect_dir(polyptr->dir, 0, 0, idata); ++ dfd_pptrdir = secure_opendir(polyptr->dir, SECURE_OPENDIR_PROTECT, 0, idata); - if (retval < 0 && errno != ENOENT) { - pam_syslog(idata->pamh, LOG_ERR, "Polydir %s access error: %m", @@ -1014,51 +1635,306 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb - return PAM_SESSION_ERR; - } - - if (retval < 0) { +- if (retval < 0) { - if ((polyptr->flags & POLYDIR_CREATE) && - create_polydir(polyptr, idata) != PAM_SUCCESS) - return PAM_SESSION_ERR; +- } else { +- close(retval); ++ if (dfd_pptrdir < 0) { + if (errno != ENOENT || !(polyptr->flags & POLYDIR_CREATE)) { + pam_syslog(idata->pamh, LOG_ERR, "Polydir %s access error: %m", + polyptr->dir); + return PAM_SESSION_ERR; + } -+ if (create_polydir(polyptr, idata) != PAM_SUCCESS) ++ dfd_pptrdir = create_polydir(polyptr, idata); ++ if (dfd_pptrdir < 0) + return PAM_SESSION_ERR; - } else { - close(retval); } -@@ -1580,7 +1686,7 @@ static int ns_setup(struct polydir_s *po + + if (polyptr->method == TMPFS) { +- if (mount("tmpfs", polyptr->dir, "tmpfs", polyptr->mount_flags, polyptr->mount_opts) < 0) { +- pam_syslog(idata->pamh, LOG_ERR, "Error mounting tmpfs on %s, %m", +- polyptr->dir); +- return PAM_SESSION_ERR; +- } ++ /* ++ * There is no function mount() that operate on a fd, so instead, we ++ * get the magic link corresponding to the fd and give it to mount(). ++ * This protects against potential races exploitable by an unpriv user. ++ */ ++ if (pam_sprintf(s_pptrdir, "/proc/self/fd/%d", dfd_pptrdir) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error pam_sprintf s_pptrdir"); ++ goto error_out; ++ } + +- if (polyptr->flags & POLYDIR_NOINIT) +- return PAM_SUCCESS; ++ if (mount("tmpfs", s_pptrdir, "tmpfs", polyptr->mount_flags, polyptr->mount_opts) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error mounting tmpfs on %s, %m", ++ polyptr->dir); ++ goto error_out; ++ } ++ ++ if (polyptr->flags & POLYDIR_NOINIT) { ++ retval = PAM_SUCCESS; ++ goto cleanup; ++ } + +- return inst_init(polyptr, "tmpfs", idata, 1); ++ retval = inst_init(polyptr, "tmpfs", idata, 1); ++ goto cleanup; + } + +- if (stat(polyptr->dir, &statbuf) < 0) { +- pam_syslog(idata->pamh, LOG_ERR, "Error stating %s: %m", +- polyptr->dir); +- return PAM_SESSION_ERR; ++ if (fstat(dfd_pptrdir, &statbuf) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error stating %s: %m", polyptr->dir); ++ goto error_out; + } + + /* +@@ -1561,15 +1920,16 @@ static int ns_setup(struct polydir_s *po + * security policy. + */ + #ifdef WITH_SELINUX +- retval = poly_name(polyptr, &instname, &instcontext, +- &origcontext, idata); ++ retval = poly_name(polyptr, &instcontext, &origcontext, idata); + #else +- retval = poly_name(polyptr, &instname, idata); ++ retval = poly_name(polyptr, idata); + #endif + + if (retval != PAM_SUCCESS) { +- if (retval != PAM_IGNORE) ++ if (retval != PAM_IGNORE) { + pam_syslog(idata->pamh, LOG_ERR, "Error getting instance name"); ++ goto error_out; ++ } + goto cleanup; + } else { + #ifdef WITH_SELINUX +@@ -1580,22 +1940,33 @@ static int ns_setup(struct polydir_s *po #endif } - if (asprintf(&inst_dir, "%s%s", polyptr->instance_prefix, instname) < 0) -+ if ((inst_dir = pam_asprintf("%s%s", polyptr->instance_prefix, instname)) == NULL) - goto error_out; +- goto error_out; +- +- if (idata->flags & PAMNS_DEBUG) +- pam_syslog(idata->pamh, LOG_DEBUG, "instance_dir %s", +- inst_dir); ++ /* ++ * Gets a fd in a secure manner (we may be operating on a path under ++ * user control), and check it's compliant. ++ * Then, we should *always* operate on *this* fd and a relative path ++ * to be protected against race conditions. ++ */ ++ dfd_iparent = secure_opendir(polyptr->instance_parent, ++ SECURE_OPENDIR_PROTECT | SECURE_OPENDIR_MKDIR, 0, idata); ++ if (dfd_iparent == -1) { ++ pam_syslog(idata->pamh, LOG_ERR, ++ "polyptr->instance_parent %s access error", ++ polyptr->instance_parent); ++ goto error_out; ++ } ++ if (check_inst_parent(dfd_iparent, idata)) { ++ goto error_out; ++ } - if (idata->flags & PAMNS_DEBUG) -@@ -1692,8 +1798,9 @@ static int cleanup_tmpdirs(struct instan + /* + * Create instance directory with appropriate security + * contexts, owner, group and mode bits. + */ + #ifdef WITH_SELINUX +- retval = create_instance(polyptr, inst_dir, &statbuf, instcontext, +- origcontext, idata); ++ retval = create_instance(polyptr, dfd_iparent, &statbuf, instcontext, ++ origcontext, idata); + #else +- retval = create_instance(polyptr, inst_dir, &statbuf, idata); ++ retval = create_instance(polyptr, dfd_iparent, &statbuf, idata); + #endif + + if (retval == PAM_IGNORE) { +@@ -1608,18 +1979,47 @@ static int ns_setup(struct polydir_s *po + } + + /* ++ * Instead of getting a new secure fd, we reuse the fd opened on directory ++ * polyptr->instance_parent to ensure we are working on the same dir as ++ * previously, and thus ensure that previous checks (e.g. check_inst_parent()) ++ * are still relevant. ++ */ ++ dfd_ipath = openat(dfd_iparent, polyptr->instname, ++ O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); ++ if (dfd_ipath == -1) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error openat on %s, %m", ++ polyptr->instname); ++ goto error_out; ++ } ++ ++ if (pam_sprintf(s_ipath, "/proc/self/fd/%d", dfd_ipath) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error pam_sprintf s_ipath"); ++ goto error_out; ++ } ++ ++ if (pam_sprintf(s_pptrdir, "/proc/self/fd/%d", dfd_pptrdir) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, "Error pam_sprintf s_pptrdir"); ++ goto error_out; ++ } ++ ++ /* + * Bind mount instance directory on top of the polyinstantiated + * directory to provide an instance of polyinstantiated directory + * based on polyinstantiated method. ++ * ++ * Operates on magic links created from two fd obtained securely ++ * to protect against race conditions and symlink attacks. Indeed, ++ * the source and destination can be in a user controled path. + */ +- if (mount(inst_dir, polyptr->dir, NULL, MS_BIND, NULL) < 0) { +- pam_syslog(idata->pamh, LOG_ERR, "Error mounting %s on %s, %m", +- inst_dir, polyptr->dir); ++ if(mount(s_ipath, s_pptrdir, NULL, MS_BIND, NULL) < 0) { ++ pam_syslog(idata->pamh, LOG_ERR, ++ "Error mounting %s on %s (%s on %s), %m", ++ s_ipath, s_pptrdir, polyptr->instance_absolute, polyptr->dir); + goto error_out; + } + + if (!(polyptr->flags & POLYDIR_NOINIT)) +- retval = inst_init(polyptr, inst_dir, idata, newdir); ++ retval = inst_init(polyptr, polyptr->instance_absolute, idata, newdir); + + goto cleanup; + +@@ -1631,8 +2031,12 @@ error_out: + retval = PAM_SESSION_ERR; + + cleanup: +- free(inst_dir); +- free(instname); ++ if (dfd_iparent != -1) ++ close(dfd_iparent); ++ if (dfd_ipath != -1) ++ close(dfd_ipath); ++ if (dfd_pptrdir != -1) ++ close(dfd_pptrdir); + #ifdef WITH_SELINUX + freecon(instcontext); + freecon(origcontext); +@@ -1671,6 +2075,7 @@ static int cleanup_tmpdirs(struct instan + { + struct polydir_s *pptr; + pid_t rc, pid; ++ int dfd = -1; + struct sigaction newsa, oldsa; + int status; + +@@ -1682,7 +2087,17 @@ static int cleanup_tmpdirs(struct instan + } + + for (pptr = idata->polydirs_ptr; pptr; pptr = pptr->next) { +- if (pptr->method == TMPDIR && access(pptr->instance_prefix, F_OK) == 0) { ++ if (pptr->method == TMPDIR) { ++ ++ dfd = secure_opendir_stateless(pptr->instance_parent); ++ if (dfd == -1) ++ continue; ++ ++ if (faccessat(dfd, pptr->instname, F_OK, AT_SYMLINK_NOFOLLOW) != 0) { ++ close(dfd); ++ continue; ++ } ++ + pid = fork(); + if (pid == 0) { + static char *envp[] = { NULL }; +@@ -1692,9 +2107,21 @@ static int cleanup_tmpdirs(struct instan _exit(1); } #endif - if (execle("/bin/rm", "/bin/rm", "-rf", pptr->instance_prefix, NULL, envp) < 0) - _exit(1); ++ if (fchdir(dfd) == -1) { ++ pam_syslog(idata->pamh, LOG_ERR, "Failed fchdir to %s: %m", ++ pptr->instance_absolute); ++ _exit(1); ++ } ++ + close_fds_pre_exec(idata); -+ execle("/bin/rm", "/bin/rm", "-rf", pptr->instance_prefix, NULL, envp); ++ ++ execle("/bin/rm", "/bin/rm", "-rf", pptr->instname, NULL, envp); + _exit(1); } else if (pid > 0) { ++ ++ if (dfd != -1) ++ close(dfd); ++ while (((rc = waitpid(pid, &status, 0)) == (pid_t)-1) && (errno == EINTR)); -@@ -1708,7 +1815,7 @@ static int cleanup_tmpdirs(struct instan + if (rc == (pid_t)-1) { +@@ -1707,8 +2134,12 @@ static int cleanup_tmpdirs(struct instan + "Error removing %s", pptr->instance_prefix); } } else if (pid < 0) { ++ ++ if (dfd != -1) ++ close(dfd); ++ pam_syslog(idata->pamh, LOG_ERR, - "Cannot fork to run namespace init script, %m"); + "Cannot fork to cleanup temporary directory, %m"); rc = PAM_SESSION_ERR; goto out; } -@@ -1948,7 +2055,7 @@ static int orig_namespace(struct instanc +@@ -1730,6 +2161,7 @@ out: + static int setup_namespace(struct instance_data *idata, enum unmnt_op unmnt) + { + int retval = 0, need_poly = 0, changing_dir = 0; ++ int dfd = -1; + char *cptr, *fptr, poly_parent[PATH_MAX]; + struct polydir_s *pptr; + +@@ -1845,13 +2277,21 @@ static int setup_namespace(struct instan + strcpy(poly_parent, "/"); + else if (cptr) + *cptr = '\0'; +- if (chdir(poly_parent) < 0) { ++ ++ dfd = secure_opendir_stateless(poly_parent); ++ if (dfd == -1) { ++ pam_syslog(idata->pamh, LOG_ERR, ++ "Failed opening %s to fchdir: %m", poly_parent); ++ } ++ else if (fchdir(dfd) == -1) { + pam_syslog(idata->pamh, LOG_ERR, +- "Can't chdir to %s, %m", poly_parent); ++ "Failed fchdir to %s: %m", poly_parent); + } ++ if (dfd != -1) ++ close(dfd); + } + +- if (umount(pptr->rdir) < 0) { ++ if (secure_umount(pptr->rdir) < 0) { + int saved_errno = errno; + pam_syslog(idata->pamh, LOG_ERR, "Unmount of %s failed, %m", + pptr->rdir); +@@ -1921,7 +2361,7 @@ static int orig_namespace(struct instanc + "Unmounting instance dir for user %d & dir %s", + idata->uid, pptr->dir); + +- if (umount(pptr->dir) < 0) { ++ if (secure_umount(pptr->dir) < 0) { + pam_syslog(idata->pamh, LOG_ERR, "Unmount of %s failed, %m", + pptr->dir); + return PAM_SESSION_ERR; +@@ -1948,7 +2388,7 @@ static int orig_namespace(struct instanc */ static int ctxt_based_inst_needed(void) { @@ -1067,7 +1943,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb int rc = 0; rc = getexeccon(&scon); -@@ -1994,7 +2101,7 @@ static int root_shared(void) +@@ -1994,7 +2434,7 @@ static int root_shared(void) break; if (i == 6) { @@ -1076,7 +1952,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb /* there might be more / mounts, the last one counts */ rv = 1; else -@@ -2162,7 +2269,7 @@ int pam_sm_close_session(pam_handle_t *p +@@ -2162,7 +2602,7 @@ int pam_sm_close_session(pam_handle_t *p { int i, retval; struct instance_data idata; @@ -1085,7 +1961,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb /* init instance data */ idata.flags = 0; -@@ -2202,7 +2309,7 @@ int pam_sm_close_session(pam_handle_t *p +@@ -2202,7 +2642,7 @@ int pam_sm_close_session(pam_handle_t *p pam_set_data(idata.pamh, NAMESPACE_PROTECT_DATA, NULL, NULL); if (idata.flags & PAMNS_DEBUG) @@ -1094,7 +1970,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb return PAM_SUCCESS; } -@@ -2210,12 +2317,14 @@ int pam_sm_close_session(pam_handle_t *p +@@ -2210,12 +2650,14 @@ int pam_sm_close_session(pam_handle_t *p if (retval != PAM_SUCCESS) return retval; @@ -1112,8 +1988,8 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.c.pam-namespace-reb if (idata.flags & PAMNS_DEBUG) pam_syslog(idata.pamh, LOG_DEBUG, "Resetting namespace for pid %d", diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-rebase Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h ---- Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-rebase 2025-06-17 12:50:04.921165460 +0200 -+++ Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h 2025-06-17 12:50:04.963237346 +0200 +--- Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-rebase 2025-08-06 10:54:56.542442240 +0200 ++++ Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h 2025-08-06 10:54:56.591448228 +0200 @@ -30,7 +30,7 @@ * DEALINGS IN THE SOFTWARE. */ @@ -1123,7 +1999,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-reb #error THIS CODE IS KNOWN TO WORK ONLY ON LINUX !!! #endif -@@ -44,21 +44,16 @@ +@@ -44,21 +44,17 @@ #include #include #include @@ -1137,7 +2013,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-reb -#include #include #include --#include + #include #include #include #include @@ -1145,7 +2021,7 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-reb #include "security/pam_modules.h" #include "security/pam_modutil.h" #include "security/pam_ext.h" -@@ -111,7 +106,7 @@ +@@ -111,7 +107,7 @@ #define PAMNS_MOUNT_PRIVATE 0x00080000 /* Make the polydir mounts private */ /* polydir flags */ @@ -1154,3 +2030,27 @@ diff -up Linux-PAM-1.3.1/modules/pam_namespace/pam_namespace.h.pam-namespace-reb #define POLYDIR_CREATE 0x00000002 /* create the polydir */ #define POLYDIR_NOINIT 0x00000004 /* no init script */ #define POLYDIR_SHARED 0x00000008 /* share context/level instances among users */ +@@ -124,6 +120,13 @@ + #define NAMESPACE_PROTECT_DATA "pam_namespace:protect_data" + + /* ++ * Operation mode for function secure_opendir() ++ */ ++#define SECURE_OPENDIR_PROTECT 0x00000001 ++#define SECURE_OPENDIR_MKDIR 0x00000002 ++#define SECURE_OPENDIR_FULL_FD 0x00000004 ++ ++/* + * Polyinstantiation method options, based on user, security context + * or both + */ +@@ -160,6 +163,9 @@ struct polydir_s { + char dir[PATH_MAX]; /* directory to polyinstantiate */ + char rdir[PATH_MAX]; /* directory to unmount (based on RUSER) */ + char instance_prefix[PATH_MAX]; /* prefix for instance dir path name */ ++ char instance_absolute[PATH_MAX]; /* absolute path to the instance dir (instance_parent + instname) */ ++ char instance_parent[PATH_MAX]; /* parent dir of the instance dir */ ++ char *instname; /* last segment of the path to the instance dir */ + enum polymethod method; /* method used to polyinstantiate */ + unsigned int num_uids; /* number of override uids */ + uid_t *uid; /* list of override uids */ diff --git a/pam.spec b/pam.spec index d38caa0..bec6c1c 100644 --- a/pam.spec +++ b/pam.spec @@ -3,7 +3,7 @@ Summary: An extensible library which provides authentication for applications Name: pam Version: 1.3.1 -Release: 37%{?dist} +Release: 38%{?dist} # The library is BSD licensed with option to relicense as GPLv2+ # - this option is redundant as the BSD license allows that anyway. # pam_timestamp, pam_loginuid, and pam_console modules are GPLv2+. @@ -125,7 +125,7 @@ Patch74: pam-1.3.1-libpam-support-long-lines.patch Patch75: pam-1.3.1-pam-access-resolve-ip.patch # https://github.com/linux-pam/linux-pam/commit/10b80543807e3fc5af5f8bcfd8bb6e219bb3cecc Patch76: pam-1.3.1-pam-inline-pam-asprintf.patch -# Available upstream +# https://github.com/linux-pam/linux-pam/commit/475bd60c552b98c7eddb3270b0b4196847c0072e Patch77: pam-1.3.1-pam-namespace-rebase.patch %define _pamlibdir %{_libdir} @@ -505,7 +505,7 @@ done %doc doc/specs/rfc86.0.txt %changelog -* Mon Jun 16 2025 Iker Pedrosa - 1.3.1-37 +* Wed Aug 6 2025 Iker Pedrosa - 1.3.1-38 - pam_namespace: fix potential privilege escalation. Resolves: CVE-2025-6020 and RHEL-96724