diff --git a/pam.changes b/pam.changes index 210835b..8a618ee 100644 --- a/pam.changes +++ b/pam.changes @@ -1,3 +1,9 @@ +------------------------------------------------------------------- +Tue Oct 11 14:44:56 UTC 2022 - Stefan Schubert + +- pam_env: Using libeconf for reading configuration and environment + files. + ------------------------------------------------------------------- Fri Jun 17 15:26:20 UTC 2022 - Thorsten Kukuk diff --git a/pam.spec b/pam.spec index fc8bf01..070c148 100644 --- a/pam.spec +++ b/pam.spec @@ -64,12 +64,14 @@ Source12: pam-login_defs-check.sh Source13: pam.tmpfiles Source14: Linux-PAM-%{version}-docs.tar.xz.asc Source15: Linux-PAM-%{version}.tar.xz.asc +Source16: tst-pam_env-retval.c Patch1: pam-limit-nproc.patch Patch2: pam-hostnames-in-access_conf.patch Patch3: pam-xauth_ownership.patch Patch4: pam-bsc1177858-dont-free-environment-string.patch Patch10: pam_xauth_data.3.xml.patch Patch11: pam-git.diff +Patch12: pam_env_econf.patch BuildRequires: audit-devel BuildRequires: bison BuildRequires: flex @@ -175,12 +177,14 @@ building both PAM-aware applications and modules for use with PAM. %prep %setup -q -n Linux-PAM-%{version} -b 1 cp -a %{SOURCE12} . +cp %{SOURCE16} ./modules/pam_env %patch1 -p1 %patch2 -p1 %patch3 -p1 %patch4 -p1 %patch10 -p1 %patch11 -p1 +%patch12 -p1 %build bash ./pam-login_defs-check.sh diff --git a/pam_env_econf.patch b/pam_env_econf.patch new file mode 100644 index 0000000..e2db6d9 --- /dev/null +++ b/pam_env_econf.patch @@ -0,0 +1,1019 @@ +diff -Naur org/configure.ac patch/configure.ac +--- org/configure.ac 2022-10-11 12:35:53.558193223 +0200 ++++ patch/configure.ac 2022-10-11 12:36:32.502192985 +0200 +@@ -511,7 +511,11 @@ + [Directory for distribution provided configuration files]) + AC_DEFINE_UNQUOTED([VENDOR_SCONFIGDIR], ["$enable_vendordir/security"], + [Directory for PAM modules distribution provided configuration files]) +- STRINGPARAM_VENDORDIR="--stringparam vendordir '$enable_vendordir' --stringparam profile.condition 'with_vendordir'" ++ if test "$WITH_ECONF" = "yes" ; then ++ STRINGPARAM_VENDORDIR="--stringparam vendordir '$enable_vendordir' --stringparam profile.condition 'with_vendordir;with_vendordir_and_with_econf'" ++ else ++ STRINGPARAM_VENDORDIR="--stringparam vendordir '$enable_vendordir' --stringparam profile.condition 'with_vendordir;with_vendordir_and_without_econf" ++ fi + else + STRINGPARAM_VENDORDIR="--stringparam profile.condition 'without_vendordir'" + fi +diff -Naur org/modules/pam_env/Makefile.am patch/modules/pam_env/Makefile.am +--- org/modules/pam_env/Makefile.am 2022-10-11 12:35:53.574193223 +0200 ++++ patch/modules/pam_env/Makefile.am 2022-10-11 12:36:32.518192985 +0200 +@@ -12,20 +12,23 @@ + endif + XMLS = README.xml pam_env.conf.5.xml pam_env.8.xml + dist_check_SCRIPTS = tst-pam_env +-TESTS = $(dist_check_SCRIPTS) ++TESTS = $(dist_check_SCRIPTS) $(check_PROGRAMS) + + securelibdir = $(SECUREDIR) + secureconfdir = $(SCONFIGDIR) + + AM_CFLAGS = -I$(top_srcdir)/libpam/include -I$(top_srcdir)/libpamc/include \ +- $(WARN_CFLAGS) ++ $(WARN_CFLAGS) -DSYSCONFDIR=\"$(sysconfdir)\" $(ECONF_CFLAGS) + AM_LDFLAGS = -no-undefined -avoid-version -module + if HAVE_VERSIONING + AM_LDFLAGS += -Wl,--version-script=$(srcdir)/../modules.map + endif + + securelib_LTLIBRARIES = pam_env.la +-pam_env_la_LIBADD = $(top_builddir)/libpam/libpam.la ++pam_env_la_LIBADD = $(top_builddir)/libpam/libpam.la $(ECONF_LIBS) ++ ++check_PROGRAMS = tst-pam_env-retval ++tst_pam_env_retval_LDADD = $(top_builddir)/libpam/libpam.la + + dist_secureconf_DATA = pam_env.conf + dist_sysconf_DATA = environment +diff -Naur org/modules/pam_env/pam_env.8.xml patch/modules/pam_env/pam_env.8.xml +--- org/modules/pam_env/pam_env.8.xml 2022-10-11 12:35:53.574193223 +0200 ++++ patch/modules/pam_env/pam_env.8.xml 2022-10-11 12:36:32.518192985 +0200 +@@ -52,13 +52,55 @@ + variables as well as PAM_ITEMs such as + PAM_RHOST. + +- ++ ++ Rules for (un)setting of variables can be defined in an own config ++ file. The path to this file can be specified with the ++ conffile option. ++ If this file does not exist, the default rules are taken from the ++ config files /etc/security/pam_env.conf and ++ /etc/security/pam_env.conf.d/*.conf. ++ If the file /etc/security/pam_env.conf does not ++ exist, the rules are taken from the files ++ %vendordir%/security/pam_env.conf, ++ %vendordir%/security/pam_env.conf.d/*.conf and ++ /etc/security/pam_env.conf.d/*.conf in that order. ++ ++ ++ By default rules for (un)setting of variables are taken from the ++ config file /etc/security/pam_env.conf. ++ If this file does not exist %vendordir%/security/pam_env.conf is used. ++ An alternate file can be specified with the conffile ++ option, which overrules all other files. ++ ++ + By default rules for (un)setting of variables are taken from the + config file /etc/security/pam_env.conf. An + alternate file can be specified with the conffile + option. + +- ++ ++ Environment variables can be defined in a file with simple KEY=VAL ++ pairs on separate lines. The path to this file can be specified with the ++ envfile option. ++ If this file has not been defined, the settings are read from the ++ files /etc/security/environment and ++ /etc/security/environment.d/*. ++ If the file /etc/environment does not exist, the ++ settings are read from the files %vendordir%/environment, ++ %vendordir%/environment.d/* and ++ /etc/environment.d/* in that order. ++ And last but not least, with the readenv option this mechanism can ++ be completely disabled. ++ ++ ++ Second a file (/etc/environment by default) with simple ++ KEY=VAL pairs on separate lines will be read. ++ If this file does not exist, %vendordir%/etc/environment is used. ++ With the envfile option an alternate file can be specified, ++ which overrules all other files. ++ And with the readenv option this can be completely disabled. ++ ++ + Second a file (/etc/environment by default) with simple + KEY=VAL pairs on separate lines will be read. + With the envfile option an alternate file can be specified. +@@ -224,12 +266,14 @@ + FILES + + ++ /usr/etc/security/pam_env.conf + /etc/security/pam_env.conf + + Default configuration file + + + ++ /usr/etc/environment + /etc/environment + + Default environment file +diff -Naur org/modules/pam_env/pam_env.c patch/modules/pam_env/pam_env.c +--- org/modules/pam_env/pam_env.c 2022-10-11 12:35:53.574193223 +0200 ++++ patch/modules/pam_env/pam_env.c 2022-10-11 12:36:32.518192985 +0200 +@@ -7,6 +7,9 @@ + */ + + #define DEFAULT_ETC_ENVFILE "/etc/environment" ++#ifdef VENDORDIR ++#define VENDOR_DEFAULT_ETC_ENVFILE (VENDORDIR "/etc/environment") ++#endif + #define DEFAULT_READ_ENVFILE 1 + + #define DEFAULT_USER_ENVFILE ".pam_environment" +@@ -25,6 +28,9 @@ + #include + #include + #include ++#ifdef USE_ECONF ++#include ++#endif + + #include + #include +@@ -42,6 +48,9 @@ + } VAR; + + #define DEFAULT_CONF_FILE (SCONFIGDIR "/pam_env.conf") ++#ifdef VENDOR_SCONFIGDIR ++#define VENDOR_DEFAULT_CONF_FILE (VENDOR_SCONFIGDIR "/pam_env.conf") ++#endif + + #define BUF_SIZE 8192 + #define MAX_ENV 8192 +@@ -53,18 +62,19 @@ + #define UNDEFINE_VAR 102 + #define ILLEGAL_VAR 103 + +-static int _assemble_line(FILE *, char *, int); +-static int _parse_line(const pam_handle_t *, const char *, VAR *); +-static int _check_var(pam_handle_t *, VAR *); /* This is the real meat */ +-static void _clean_var(VAR *); +-static int _expand_arg(pam_handle_t *, char **); +-static const char * _pam_get_item_byname(pam_handle_t *, const char *); +-static int _define_var(pam_handle_t *, int, VAR *); +-static int _undefine_var(pam_handle_t *, int, VAR *); +- + /* This is a special value used to designate an empty string */ + static char quote='\0'; + ++static void free_string_array(char **array) ++{ ++ if (array == NULL) ++ return; ++ for (char **entry = array; *entry != NULL; ++entry) { ++ free(*entry); ++ } ++ free(array); ++} ++ + /* argument parsing */ + + #define PAM_DEBUG_ARG 0x01 +@@ -77,10 +87,10 @@ + int ctrl=0; + + *user_envfile = DEFAULT_USER_ENVFILE; +- *envfile = DEFAULT_ETC_ENVFILE; ++ *envfile = NULL; + *readenv = DEFAULT_READ_ENVFILE; + *user_readenv = DEFAULT_USER_READ_ENVFILE; +- *conffile = DEFAULT_CONF_FILE; ++ *conffile = NULL; + + /* step through arguments */ + for (; argc-- > 0; ++argv) { +@@ -128,166 +138,145 @@ + return ctrl; + } + +-static int +-_parse_config_file(pam_handle_t *pamh, int ctrl, const char *file) +-{ +- int retval; +- char buffer[BUF_SIZE]; +- FILE *conf; +- VAR Var, *var=&Var; +- +- D(("Called.")); +- +- var->name=NULL; var->defval=NULL; var->override=NULL; ++#ifdef USE_ECONF + +- D(("Config file name is: %s", file)); ++#define ENVIRONMENT "environment" ++#define PAM_ENV "pam_env" + +- /* +- * Lets try to open the config file, parse it and process +- * any variables found. +- */ ++static int ++isDirectory(const char *path) { ++ struct stat statbuf; ++ if (stat(path, &statbuf) != 0) ++ return 0; ++ return S_ISDIR(statbuf.st_mode); ++} + +- if ((conf = fopen(file,"r")) == NULL) { +- pam_syslog(pamh, LOG_ERR, "Unable to open config file: %s: %m", file); +- return PAM_IGNORE; ++static int ++econf_read_file(const pam_handle_t *pamh, const char *filename, const char *delim, ++ const char *name, const char *suffix, const char *subpath, ++ char ***lines) ++{ ++ econf_file *key_file = NULL; ++ econf_err error; ++ size_t key_number = 0; ++ char **keys = NULL; ++ const char *base_dir = ""; ++ ++ if (filename != NULL) { ++ if (isDirectory(filename)) { ++ /* Set base directory which can be different from root */ ++ D(("filename argument is a directory: %s", filename)); ++ base_dir = filename; ++ } else { ++ /* Read only one file */ ++ error = econf_readFile (&key_file, filename, delim, "#"); ++ D(("File name is: %s", filename)); ++ if (error != ECONF_SUCCESS) { ++ pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s: %s", filename, ++ econf_errString(error)); ++ if (error == ECONF_NOFILE) ++ return PAM_IGNORE; ++ else ++ return PAM_ABORT; ++ } ++ } + } +- +- /* _pam_assemble_line will provide a complete line from the config file, +- * with all comments removed and any escaped newlines fixed up +- */ +- +- while (( retval = _assemble_line(conf, buffer, BUF_SIZE)) > 0) { +- D(("Read line: %s", buffer)); +- +- if ((retval = _parse_line(pamh, buffer, var)) == GOOD_LINE) { +- retval = _check_var(pamh, var); +- +- if (DEFINE_VAR == retval) { +- retval = _define_var(pamh, ctrl, var); +- +- } else if (UNDEFINE_VAR == retval) { +- retval = _undefine_var(pamh, ctrl, var); ++ if (filename == NULL || base_dir[0] != '\0') { ++ /* Read and merge all setting in e.g. /usr/etc and /etc */ ++ char *vendor_dir = NULL, *sysconf_dir; ++ if (subpath != NULL && subpath[0] != '\0') { ++#ifdef VENDORDIR ++ if (asprintf(&vendor_dir, "%s%s/%s/", base_dir, VENDORDIR, subpath) < 0) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ return PAM_BUF_ERR; ++ } ++#endif ++ if (asprintf(&sysconf_dir, "%s%s/%s/", base_dir, SYSCONFDIR, subpath) < 0) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ free(vendor_dir); ++ return PAM_BUF_ERR; ++ } ++ } else { ++#ifdef VENDORDIR ++ if (asprintf(&vendor_dir, "%s%s/", base_dir, VENDORDIR) < 0) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ return PAM_BUF_ERR; ++ } ++#endif ++ if (asprintf(&sysconf_dir, "%s%s/", base_dir, SYSCONFDIR) < 0) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ free(vendor_dir); ++ return PAM_BUF_ERR; + } + } +- if (PAM_SUCCESS != retval && ILLEGAL_VAR != retval +- && BAD_LINE != retval && PAM_BAD_ITEM != retval) break; +- +- _clean_var(var); +- +- } /* while */ +- +- (void) fclose(conf); +- +- /* tidy up */ +- _clean_var(var); /* We could have got here prematurely, +- * this is safe though */ +- D(("Exit.")); +- return (retval != 0 ? PAM_ABORT : PAM_SUCCESS); +-} + +-static int +-_parse_env_file(pam_handle_t *pamh, int ctrl, const char *file) +-{ +- int retval=PAM_SUCCESS, i, t; +- char buffer[BUF_SIZE], *key, *mark; +- FILE *conf; +- +- D(("Env file name is: %s", file)); +- +- if ((conf = fopen(file,"r")) == NULL) { +- pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s: %m", file); +- return PAM_IGNORE; ++ D(("Read configuration from directory %s and %s", vendor_dir, sysconf_dir)); ++ error = econf_readDirs (&key_file, vendor_dir, sysconf_dir, name, suffix, ++ delim, "#"); ++ free(vendor_dir); ++ free(sysconf_dir); ++ if (error != ECONF_SUCCESS) { ++ pam_syslog(pamh, LOG_ERR, "Unable to read configuration in different directories: %s", ++ econf_errString(error)); ++ if (error == ECONF_NOFILE) ++ return PAM_IGNORE; ++ else ++ return PAM_ABORT; ++ } + } + +- while (_assemble_line(conf, buffer, BUF_SIZE) > 0) { +- D(("Read line: %s", buffer)); +- key = buffer; +- +- /* skip leading white space */ +- key += strspn(key, " \n\t"); +- +- /* skip blanks lines and comments */ +- if (key[0] == '#') +- continue; +- +- /* skip over "export " if present so we can be compat with +- bash type declarations */ +- if (strncmp(key, "export ", (size_t) 7) == 0) +- key += 7; +- +- /* now find the end of value */ +- mark = key; +- while(mark[0] != '\n' && mark[0] != '#' && mark[0] != '\0') +- mark++; +- if (mark[0] != '\0') +- mark[0] = '\0'; +- +- /* +- * sanity check, the key must be alphanumeric +- */ +- +- if (key[0] == '=') { +- pam_syslog(pamh, LOG_ERR, +- "missing key name '%s' in %s', ignoring", +- key, file); +- continue; +- } +- +- for ( i = 0 ; key[i] != '=' && key[i] != '\0' ; i++ ) +- if (!isalnum(key[i]) && key[i] != '_') { +- pam_syslog(pamh, LOG_ERR, +- "non-alphanumeric key '%s' in %s', ignoring", +- key, file); +- break; +- } +- /* non-alphanumeric key, ignore this line */ +- if (key[i] != '=' && key[i] != '\0') +- continue; ++ error = econf_getKeys(key_file, NULL, &key_number, &keys); ++ if (error != ECONF_SUCCESS && error != ECONF_NOKEY) { ++ pam_syslog(pamh, LOG_ERR, "Unable to read keys: %s", ++ econf_errString(error)); ++ econf_freeFile(key_file); ++ return PAM_ABORT; ++ } + +- /* now we try to be smart about quotes around the value, +- but not too smart, we can't get all fancy with escaped +- values like bash */ +- if (key[i] == '=' && (key[++i] == '\"' || key[i] == '\'')) { +- for ( t = i+1 ; key[t] != '\0' ; t++) +- if (key[t] != '\"' && key[t] != '\'') +- key[i++] = key[t]; +- else if (key[t+1] != '\0') +- key[i++] = key[t]; +- key[i] = '\0'; +- } ++ *lines = malloc((key_number +1)* sizeof(char**)); ++ if (*lines == NULL) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ econf_free(keys); ++ econf_freeFile(key_file); ++ return PAM_BUF_ERR; ++ } + +- /* if this is a request to delete a variable, check that it's +- actually set first, so we don't get a vague error back from +- pam_putenv() */ +- for (i = 0; key[i] != '=' && key[i] != '\0'; i++); ++ (*lines)[key_number] = 0; + +- if (key[i] == '\0' && !pam_getenv(pamh,key)) +- continue; ++ for (size_t i = 0; i < key_number; i++) { ++ char *val; + +- /* set the env var, if it fails, we break out of the loop */ +- retval = pam_putenv(pamh, key); +- if (retval != PAM_SUCCESS) { +- D(("error setting env \"%s\"", key)); +- break; +- } else if (ctrl & PAM_DEBUG_ARG) { +- pam_syslog(pamh, LOG_DEBUG, +- "pam_putenv(\"%s\")", key); ++ error = econf_getStringValue (key_file, NULL, keys[i], &val); ++ if (error != ECONF_SUCCESS) { ++ pam_syslog(pamh, LOG_ERR, "Unable to get string from key %s: %s", ++ keys[i], ++ econf_errString(error)); ++ } else { ++ if (asprintf(&(*lines)[i],"%s%c%s", keys[i], delim[0], val) < 0) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ econf_free(keys); ++ econf_freeFile(key_file); ++ free_string_array(*lines); ++ free (val); ++ return PAM_BUF_ERR; + } ++ free (val); ++ } + } + +- (void) fclose(conf); +- +- /* tidy up */ +- D(("Exit.")); +- return retval; ++ econf_free(keys); ++ econf_free(key_file); ++ return PAM_SUCCESS; + } + ++#else ++ + /* + * This is where we read a line of the PAM config file. The line may be + * preceded by lines of comments and also extended with "\\\n" + */ +- +-static int _assemble_line(FILE *f, char *buffer, int buf_len) ++static int ++_assemble_line(FILE *f, char *buffer, int buf_len) + { + char *p = buffer; + char *s, *os; +@@ -375,8 +364,54 @@ + return used; + } + ++static int read_file(const pam_handle_t *pamh, const char*filename, char ***lines) ++{ ++ FILE *conf; ++ char buffer[BUF_SIZE]; ++ ++ D(("Parsed file name is: %s", filename)); ++ ++ if ((conf = fopen(filename,"r")) == NULL) { ++ pam_syslog(pamh, LOG_ERR, "Unable to open env file: %s", filename); ++ return PAM_IGNORE; ++ } ++ ++ size_t i = 0; ++ *lines = malloc((i + 1)* sizeof(char**)); ++ if (*lines == NULL) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ (void) fclose(conf); ++ return PAM_BUF_ERR; ++ } ++ (*lines)[i] = 0; ++ while (_assemble_line(conf, buffer, BUF_SIZE) > 0) { ++ char **tmp = NULL; ++ D(("Read line: %s", buffer)); ++ tmp = realloc(*lines, (++i + 1) * sizeof(char**)); ++ if (tmp == NULL) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ (void) fclose(conf); ++ free_string_array(*lines); ++ return PAM_BUF_ERR; ++ } ++ *lines = tmp; ++ (*lines)[i-1] = strdup(buffer); ++ if ((*lines)[i-1] == NULL) { ++ pam_syslog(pamh, LOG_ERR, "Cannot allocate memory."); ++ (void) fclose(conf); ++ free_string_array(*lines); ++ return PAM_BUF_ERR; ++ } ++ (*lines)[i] = 0; ++ } ++ ++ (void) fclose(conf); ++ return PAM_SUCCESS; ++} ++#endif ++ + static int +-_parse_line (const pam_handle_t *pamh, const char *buffer, VAR *var) ++_parse_line(const pam_handle_t *pamh, const char *buffer, VAR *var) + { + /* + * parse buffer into var, legal syntax is +@@ -471,75 +506,57 @@ + return GOOD_LINE; + } + +-static int _check_var(pam_handle_t *pamh, VAR *var) ++static const char * ++_pam_get_item_byname(pam_handle_t *pamh, const char *name) + { + /* +- * Examine the variable and determine what action to take. +- * Returns DEFINE_VAR, UNDEFINE_VAR depending on action to take +- * or a PAM_* error code if passed back from other routines +- * +- * if no DEFAULT provided, the empty string is assumed +- * if no OVERRIDE provided, the empty string is assumed +- * if DEFAULT= and OVERRIDE evaluates to the empty string, +- * this variable should be undefined +- * if DEFAULT="" and OVERRIDE evaluates to the empty string, +- * this variable should be defined with no value +- * if OVERRIDE=value and value turns into the empty string, DEFAULT is used +- * +- * If DEFINE_VAR is to be returned, the correct value to define will +- * be pointed to by var->value ++ * This function just allows me to use names as given in the config ++ * file and translate them into the appropriate PAM_ITEM macro + */ + +- int retval; ++ int item; ++ const void *itemval; + + D(("Called.")); +- +- /* +- * First thing to do is to expand any arguments, but only +- * if they are not the special quote values (cause expand_arg +- * changes memory). +- */ +- +- if (var->defval && ("e != var->defval) && +- ((retval = _expand_arg(pamh, &(var->defval))) != PAM_SUCCESS)) { +- return retval; +- } +- if (var->override && ("e != var->override) && +- ((retval = _expand_arg(pamh, &(var->override))) != PAM_SUCCESS)) { +- return retval; ++ if (strcmp(name, "PAM_USER") == 0 || strcmp(name, "HOME") == 0 || strcmp(name, "SHELL") == 0) { ++ item = PAM_USER; ++ } else if (strcmp(name, "PAM_USER_PROMPT") == 0) { ++ item = PAM_USER_PROMPT; ++ } else if (strcmp(name, "PAM_TTY") == 0) { ++ item = PAM_TTY; ++ } else if (strcmp(name, "PAM_RUSER") == 0) { ++ item = PAM_RUSER; ++ } else if (strcmp(name, "PAM_RHOST") == 0) { ++ item = PAM_RHOST; ++ } else { ++ D(("Unknown PAM_ITEM: <%s>", name)); ++ pam_syslog (pamh, LOG_ERR, "Unknown PAM_ITEM: <%s>", name); ++ return NULL; + } + +- /* Now its easy */ +- +- if (var->override && *(var->override)) { +- /* if there is a non-empty string in var->override, we use it */ +- D(("OVERRIDE variable <%s> being used: <%s>", var->name, var->override)); +- var->value = var->override; +- retval = DEFINE_VAR; +- } else { ++ if (pam_get_item(pamh, item, &itemval) != PAM_SUCCESS) { ++ D(("pam_get_item failed")); ++ return NULL; /* let pam_get_item() log the error */ ++ } + +- var->value = var->defval; +- if ("e == var->defval) { +- /* +- * This means that the empty string was given for defval value +- * which indicates that a variable should be defined with no value +- */ +- D(("An empty variable: <%s>", var->name)); +- retval = DEFINE_VAR; +- } else if (var->defval) { +- D(("DEFAULT variable <%s> being used: <%s>", var->name, var->defval)); +- retval = DEFINE_VAR; +- } else { +- D(("UNDEFINE variable <%s>", var->name)); +- retval = UNDEFINE_VAR; ++ if (itemval && (strcmp(name, "HOME") == 0 || strcmp(name, "SHELL") == 0)) { ++ struct passwd *user_entry; ++ user_entry = pam_modutil_getpwnam (pamh, itemval); ++ if (!user_entry) { ++ pam_syslog(pamh, LOG_ERR, "No such user!?"); ++ return NULL; + } ++ return (strcmp(name, "SHELL") == 0) ? ++ user_entry->pw_shell : ++ user_entry->pw_dir; + } + + D(("Exit.")); +- return retval; ++ return itemval; + } + +-static int _expand_arg(pam_handle_t *pamh, char **value) ++static int ++_expand_arg(pam_handle_t *pamh, char **value) + { + const char *orig=*value, *tmpptr=NULL; + char *ptr; /* +@@ -679,55 +696,96 @@ + return PAM_SUCCESS; + } + +-static const char * _pam_get_item_byname(pam_handle_t *pamh, const char *name) ++static int ++_check_var(pam_handle_t *pamh, VAR *var) + { + /* +- * This function just allows me to use names as given in the config +- * file and translate them into the appropriate PAM_ITEM macro ++ * Examine the variable and determine what action to take. ++ * Returns DEFINE_VAR, UNDEFINE_VAR depending on action to take ++ * or a PAM_* error code if passed back from other routines ++ * ++ * if no DEFAULT provided, the empty string is assumed ++ * if no OVERRIDE provided, the empty string is assumed ++ * if DEFAULT= and OVERRIDE evaluates to the empty string, ++ * this variable should be undefined ++ * if DEFAULT="" and OVERRIDE evaluates to the empty string, ++ * this variable should be defined with no value ++ * if OVERRIDE=value and value turns into the empty string, DEFAULT is used ++ * ++ * If DEFINE_VAR is to be returned, the correct value to define will ++ * be pointed to by var->value + */ + +- int item; +- const void *itemval; ++ int retval; + + D(("Called.")); +- if (strcmp(name, "PAM_USER") == 0 || strcmp(name, "HOME") == 0 || strcmp(name, "SHELL") == 0) { +- item = PAM_USER; +- } else if (strcmp(name, "PAM_USER_PROMPT") == 0) { +- item = PAM_USER_PROMPT; +- } else if (strcmp(name, "PAM_TTY") == 0) { +- item = PAM_TTY; +- } else if (strcmp(name, "PAM_RUSER") == 0) { +- item = PAM_RUSER; +- } else if (strcmp(name, "PAM_RHOST") == 0) { +- item = PAM_RHOST; +- } else { +- D(("Unknown PAM_ITEM: <%s>", name)); +- pam_syslog (pamh, LOG_ERR, "Unknown PAM_ITEM: <%s>", name); +- return NULL; +- } + +- if (pam_get_item(pamh, item, &itemval) != PAM_SUCCESS) { +- D(("pam_get_item failed")); +- return NULL; /* let pam_get_item() log the error */ ++ /* ++ * First thing to do is to expand any arguments, but only ++ * if they are not the special quote values (cause expand_arg ++ * changes memory). ++ */ ++ ++ if (var->defval && ("e != var->defval) && ++ ((retval = _expand_arg(pamh, &(var->defval))) != PAM_SUCCESS)) { ++ return retval; ++ } ++ if (var->override && ("e != var->override) && ++ ((retval = _expand_arg(pamh, &(var->override))) != PAM_SUCCESS)) { ++ return retval; + } + +- if (itemval && (strcmp(name, "HOME") == 0 || strcmp(name, "SHELL") == 0)) { +- struct passwd *user_entry; +- user_entry = pam_modutil_getpwnam (pamh, itemval); +- if (!user_entry) { +- pam_syslog(pamh, LOG_ERR, "No such user!?"); +- return NULL; ++ /* Now its easy */ ++ ++ if (var->override && *(var->override)) { ++ /* if there is a non-empty string in var->override, we use it */ ++ D(("OVERRIDE variable <%s> being used: <%s>", var->name, var->override)); ++ var->value = var->override; ++ retval = DEFINE_VAR; ++ } else { ++ ++ var->value = var->defval; ++ if ("e == var->defval) { ++ /* ++ * This means that the empty string was given for defval value ++ * which indicates that a variable should be defined with no value ++ */ ++ D(("An empty variable: <%s>", var->name)); ++ retval = DEFINE_VAR; ++ } else if (var->defval) { ++ D(("DEFAULT variable <%s> being used: <%s>", var->name, var->defval)); ++ retval = DEFINE_VAR; ++ } else { ++ D(("UNDEFINE variable <%s>", var->name)); ++ retval = UNDEFINE_VAR; + } +- return (strcmp(name, "SHELL") == 0) ? +- user_entry->pw_shell : +- user_entry->pw_dir; + } + + D(("Exit.")); +- return itemval; ++ return retval; + } + +-static int _define_var(pam_handle_t *pamh, int ctrl, VAR *var) ++static void ++_clean_var(VAR *var) ++{ ++ if (var->name) { ++ free(var->name); ++ } ++ if (var->defval && ("e != var->defval)) { ++ free(var->defval); ++ } ++ if (var->override && ("e != var->override)) { ++ free(var->override); ++ } ++ var->name = NULL; ++ var->value = NULL; /* never has memory specific to it */ ++ var->defval = NULL; ++ var->override = NULL; ++ return; ++} ++ ++static int ++_define_var(pam_handle_t *pamh, int ctrl, VAR *var) + { + /* We have a variable to define, this is a simple function */ + +@@ -749,7 +807,8 @@ + return retval; + } + +-static int _undefine_var(pam_handle_t *pamh, int ctrl, VAR *var) ++static int ++_undefine_var(pam_handle_t *pamh, int ctrl, VAR *var) + { + /* We have a variable to undefine, this is a simple function */ + +@@ -760,25 +819,176 @@ + return pam_putenv(pamh, var->name); + } + +-static void _clean_var(VAR *var) ++static int ++_parse_config_file(pam_handle_t *pamh, int ctrl, const char *file) + { +- if (var->name) { +- free(var->name); +- } +- if (var->defval && ("e != var->defval)) { +- free(var->defval); +- } +- if (var->override && ("e != var->override)) { +- free(var->override); +- } +- var->name = NULL; +- var->value = NULL; /* never has memory specific to it */ +- var->defval = NULL; +- var->override = NULL; +- return; ++ int retval; ++ VAR Var, *var=&Var; ++ char **conf_list = NULL; ++ ++ var->name=NULL; var->defval=NULL; var->override=NULL; ++ ++ D(("Called.")); ++ ++#ifdef USE_ECONF ++ /* If "file" is not NULL, only this file will be parsed. */ ++ retval = econf_read_file(pamh, file, " \t", PAM_ENV, ".conf", "security", &conf_list); ++#else ++ /* Only one file will be parsed. So, file has to be set. */ ++ if (file == NULL) /* No filename has been set via argv. */ ++ file = DEFAULT_CONF_FILE; ++#ifdef VENDOR_DEFAULT_CONF_FILE ++ /* ++ * Check whether file is available. ++ * If it does not exist, fall back to VENDOR_DEFAULT_CONF_FILE file. ++ */ ++ struct stat stat_buffer; ++ if (stat(file, &stat_buffer) != 0 && errno == ENOENT) { ++ file = VENDOR_DEFAULT_CONF_FILE; ++ } ++#endif ++ retval = read_file(pamh, file, &conf_list); ++#endif ++ ++ if (retval != PAM_SUCCESS) ++ return retval; ++ ++ for (char **conf = conf_list; *conf != NULL; ++conf) { ++ if ((retval = _parse_line(pamh, *conf, var)) == GOOD_LINE) { ++ retval = _check_var(pamh, var); ++ ++ if (DEFINE_VAR == retval) { ++ retval = _define_var(pamh, ctrl, var); ++ ++ } else if (UNDEFINE_VAR == retval) { ++ retval = _undefine_var(pamh, ctrl, var); ++ } ++ } ++ if (PAM_SUCCESS != retval && ILLEGAL_VAR != retval ++ && BAD_LINE != retval && PAM_BAD_ITEM != retval) break; ++ ++ _clean_var(var); ++ ++ } /* for */ ++ ++ /* tidy up */ ++ free_string_array(conf_list); ++ _clean_var(var); /* We could have got here prematurely, ++ * this is safe though */ ++ D(("Exit.")); ++ return (retval != 0 ? PAM_ABORT : PAM_SUCCESS); + } + ++static int ++_parse_env_file(pam_handle_t *pamh, int ctrl, const char *file) ++{ ++ int retval=PAM_SUCCESS, i, t; ++ char *key, *mark; ++ char **env_list = NULL; ++ ++#ifdef USE_ECONF ++ retval = econf_read_file(pamh, file, "=", ENVIRONMENT, "", "", &env_list); ++#else ++ /* Only one file will be parsed. So, file has to be set. */ ++ if (file == NULL) /* No filename has been set via argv. */ ++ file = DEFAULT_ETC_ENVFILE; ++#ifdef VENDOR_DEFAULT_ETC_ENVFILE ++ /* ++ * Check whether file is available. ++ * If it does not exist, fall back to VENDOR_DEFAULT_ETC_ENVFILE; file. ++ */ ++ struct stat stat_buffer; ++ if (stat(file, &stat_buffer) != 0 && errno == ENOENT) { ++ file = VENDOR_DEFAULT_ETC_ENVFILE; ++ } ++#endif ++ retval = read_file(pamh, file, &env_list); ++#endif ++ ++ if (retval != PAM_SUCCESS) ++ return retval == PAM_IGNORE ? PAM_SUCCESS : retval; ++ ++ for (char **env = env_list; *env != NULL; ++env) { ++ key = *env; ++ ++ /* skip leading white space */ ++ key += strspn(key, " \n\t"); ++ ++ /* skip blanks lines and comments */ ++ if (key[0] == '#') ++ continue; ++ ++ /* skip over "export " if present so we can be compat with ++ bash type declarations */ ++ if (strncmp(key, "export ", (size_t) 7) == 0) ++ key += 7; ++ ++ /* now find the end of value */ ++ mark = key; ++ while(mark[0] != '\n' && mark[0] != '#' && mark[0] != '\0') ++ mark++; ++ if (mark[0] != '\0') ++ mark[0] = '\0'; ++ ++ /* ++ * sanity check, the key must be alphanumeric ++ */ ++ ++ if (key[0] == '=') { ++ pam_syslog(pamh, LOG_ERR, ++ "missing key name '%s' in %s', ignoring", ++ key, file); ++ continue; ++ } ++ ++ for ( i = 0 ; key[i] != '=' && key[i] != '\0' ; i++ ) ++ if (!isalnum(key[i]) && key[i] != '_') { ++ pam_syslog(pamh, LOG_ERR, ++ "non-alphanumeric key '%s' in %s', ignoring", ++ key, file); ++ break; ++ } ++ /* non-alphanumeric key, ignore this line */ ++ if (key[i] != '=' && key[i] != '\0') ++ continue; ++ ++ /* now we try to be smart about quotes around the value, ++ but not too smart, we can't get all fancy with escaped ++ values like bash */ ++ if (key[i] == '=' && (key[++i] == '\"' || key[i] == '\'')) { ++ for ( t = i+1 ; key[t] != '\0' ; t++) ++ if (key[t] != '\"' && key[t] != '\'') ++ key[i++] = key[t]; ++ else if (key[t+1] != '\0') ++ key[i++] = key[t]; ++ key[i] = '\0'; ++ } ++ ++ /* if this is a request to delete a variable, check that it's ++ actually set first, so we don't get a vague error back from ++ pam_putenv() */ ++ for (i = 0; key[i] != '=' && key[i] != '\0'; i++); ++ ++ if (key[i] == '\0' && !pam_getenv(pamh,key)) ++ continue; ++ ++ /* set the env var, if it fails, we break out of the loop */ ++ retval = pam_putenv(pamh, key); ++ if (retval != PAM_SUCCESS) { ++ D(("error setting env \"%s\"", key)); ++ break; ++ } else if (ctrl & PAM_DEBUG_ARG) { ++ pam_syslog(pamh, LOG_DEBUG, ++ "pam_putenv(\"%s\")", key); ++ } ++ free(*env); ++ } + ++ /* tidy up */ ++ free(env_list); ++ D(("Exit.")); ++ return retval; ++} + + /* --- authentication management functions (only) --- */ + +diff -Naur org/modules/pam_env/pam_env.conf.5.xml patch/modules/pam_env/pam_env.conf.5.xml +--- org/modules/pam_env/pam_env.conf.5.xml 2022-10-11 12:35:53.574193223 +0200 ++++ patch/modules/pam_env/pam_env.conf.5.xml 2022-10-11 12:36:32.518192985 +0200 +@@ -20,7 +20,15 @@ + + DESCRIPTION + +- ++ ++ The /usr/etc/security/pam_env.conf and ++ /etc/security/pam_env.conf files specify ++ the environment variables to be set, unset or modified by ++ pam_env8. ++ When someone logs in, these files are read and the environment ++ variables are set according. ++ ++ + The /etc/security/pam_env.conf file specifies + the environment variables to be set, unset or modified by + pam_env8. +@@ -61,7 +69,15 @@ + at front) can be used to mark this line as a comment line. + + +- ++ ++ The /usr/etc/environment and /etc/environment files specify ++ the environment variables to be set. These files must consist of simple ++ NAME=VALUE pairs on separate lines. ++ The pam_env8 ++ module will read these files after the pam_env.conf ++ file. ++ ++ + The /etc/environment file specifies + the environment variables to be set. The file must consist of simple + NAME=VALUE pairs on separate lines. diff --git a/tst-pam_env-retval.c b/tst-pam_env-retval.c new file mode 100644 index 0000000..524d183 --- /dev/null +++ b/tst-pam_env-retval.c @@ -0,0 +1,259 @@ +/* + * Check pam_env return values. + * + * Copyright (c) 2020-2022 Dmitry V. Levin + * Copyright (c) 2022 Stefan Schubert + */ + +#include "test_assert.h" + +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_NAME "pam_env" +#define TEST_NAME "tst-" MODULE_NAME "-retval" +#define TEST_NAME_DIR TEST_NAME ".dir" + +static const char service_file[] = TEST_NAME ".service"; +static const char missing_file[] = TEST_NAME ".missing"; +static const char dir[] = TEST_NAME_DIR; +static const char dir_usr[] = TEST_NAME_DIR "/usr"; +static const char dir_usr_etc[] = TEST_NAME_DIR "/usr/etc"; +static const char dir_usr_etc_security[] = TEST_NAME_DIR "/usr/etc/security"; +static const char my_conf[] = TEST_NAME ".conf"; +static const char my_env[] = TEST_NAME ".env"; +static const char usr_env[] = TEST_NAME_DIR "/usr/etc/environment"; +static const char usr_conf[] = TEST_NAME_DIR "/usr/etc/security/pam_env.conf"; + +static struct pam_conv conv; + +static void +setup(void) +{ + FILE *fp; + + ASSERT_EQ(0, mkdir(dir, 0755)); + ASSERT_EQ(0, mkdir(dir_usr, 0755)); + ASSERT_EQ(0, mkdir(dir_usr_etc, 0755)); + ASSERT_EQ(0, mkdir(dir_usr_etc_security, 0755)); + + ASSERT_NE(NULL, fp = fopen(my_conf, "w")); + ASSERT_LT(0, fprintf(fp, + "EDITOR\tDEFAULT=vim\n" + "PAGER\tDEFAULT=more\n")); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_NE(NULL, fp = fopen(my_env, "w")); + ASSERT_LT(0, fprintf(fp, + "test_value=foo\n" + "test2_value=bar\n")); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_NE(NULL, fp = fopen(usr_env, "w")); + ASSERT_LT(0, fprintf(fp, + "usr_etc_test=foo\n" + "usr_etc_test2=bar\n")); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_NE(NULL, fp = fopen(usr_conf, "w")); + ASSERT_LT(0, fprintf(fp, + "PAGER DEFAULT=emacs\n" + "MANPAGER DEFAULT=less\n")); + ASSERT_EQ(0, fclose(fp)); +} + +static void +cleanup(void) +{ + ASSERT_EQ(0, unlink(my_conf)); + ASSERT_EQ(0, unlink(my_env)); + ASSERT_EQ(0, unlink(usr_env)); + ASSERT_EQ(0, unlink(usr_conf)); + ASSERT_EQ(0, rmdir(dir_usr_etc_security)); + ASSERT_EQ(0, rmdir(dir_usr_etc)); + ASSERT_EQ(0, rmdir(dir_usr)); + ASSERT_EQ(0, rmdir(dir)); +} + +static void +check_array(const char **array1, char **array2) +{ + for (const char **a1 = array1; *a1 != NULL; ++a1) { + char **a2; + for (a2 = array2; *a2 != NULL; ++a2) { + if (strcmp(*a1, *a2) == 0) + break; + } + ASSERT_NE(NULL, *a2); + } +} + +static void +check_env(const char **list) +{ + pam_handle_t *pamh = NULL; + + ASSERT_EQ(PAM_SUCCESS, + pam_start_confdir(service_file, "", &conv, ".", &pamh)); + ASSERT_NE(NULL, pamh); + + ASSERT_EQ(PAM_SUCCESS, pam_open_session(pamh, 0)); + + char **env_list = pam_getenvlist(pamh); + ASSERT_NE(NULL, env_list); + + check_array(list, env_list); + + for (char **e = env_list; *e != NULL; ++e) + free(*e); + free(env_list); + + ASSERT_EQ(PAM_SUCCESS, pam_close_session(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_end(pamh, 0)); +} + +int +main(void) +{ + pam_handle_t *pamh = NULL; + FILE *fp; + char cwd[PATH_MAX]; + + ASSERT_NE(NULL, getcwd(cwd, sizeof(cwd))); + + setup(); + + /* + * When conffile= specifies a missing file, all methods except + * pam_sm_acct_mgmt and pam_sm_chauthtok return PAM_IGNORE. + * The return code of the stack where every module returns PAM_IGNORE + * is PAM_PERM_DENIED. + */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "auth required %s/.libs/%s.so conffile=%s/%s\n" + "account required %s/.libs/%s.so conffile=%s/%s\n" + "password required %s/.libs/%s.so conffile=%s/%s\n" + "session required %s/.libs/%s.so conffile=%s/%s\n", + cwd, MODULE_NAME, cwd, missing_file, + cwd, MODULE_NAME, cwd, missing_file, + cwd, MODULE_NAME, cwd, missing_file, + cwd, MODULE_NAME, cwd, missing_file)); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_EQ(PAM_SUCCESS, + pam_start_confdir(service_file, "", &conv, ".", &pamh)); + ASSERT_NE(NULL, pamh); + ASSERT_EQ(PAM_PERM_DENIED, pam_authenticate(pamh, 0)); + ASSERT_EQ(PAM_PERM_DENIED, pam_setcred(pamh, 0)); + ASSERT_EQ(PAM_SERVICE_ERR, pam_acct_mgmt(pamh, 0)); + ASSERT_EQ(PAM_SERVICE_ERR, pam_chauthtok(pamh, 0)); + ASSERT_EQ(PAM_PERM_DENIED, pam_open_session(pamh, 0)); + ASSERT_EQ(PAM_PERM_DENIED, pam_close_session(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_end(pamh, 0)); + pamh = NULL; + + /* + * When conffile= specifies a missing file, all methods except + * pam_sm_acct_mgmt and pam_sm_chauthtok return PAM_IGNORE. + * pam_permit is added after pam_env to convert PAM_IGNORE to PAM_SUCCESS. + */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "auth required %s/.libs/%s.so conffile=%s/%s\n" + "auth required %s/../pam_permit/.libs/pam_permit.so\n" + "account required %s/.libs/%s.so conffile=%s/%s\n" + "account required %s/../pam_permit/.libs/pam_permit.so\n" + "password required %s/.libs/%s.so conffile=%s/%s\n" + "password required %s/../pam_permit/.libs/pam_permit.so\n" + "session required %s/.libs/%s.so conffile=%s/%s\n" + "session required %s/../pam_permit/.libs/pam_permit.so\n", + cwd, MODULE_NAME, cwd, missing_file, cwd, + cwd, MODULE_NAME, cwd, missing_file, cwd, + cwd, MODULE_NAME, cwd, missing_file, cwd, + cwd, MODULE_NAME, cwd, missing_file, cwd)); + ASSERT_EQ(0, fclose(fp)); + + ASSERT_EQ(PAM_SUCCESS, + pam_start_confdir(service_file, "", &conv, ".", &pamh)); + ASSERT_NE(NULL, pamh); + ASSERT_EQ(PAM_SUCCESS, pam_authenticate(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_setcred(pamh, 0)); + ASSERT_EQ(PAM_SERVICE_ERR, pam_acct_mgmt(pamh, 0)); + ASSERT_EQ(PAM_SERVICE_ERR, pam_chauthtok(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_open_session(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_close_session(pamh, 0)); + ASSERT_EQ(PAM_SUCCESS, pam_end(pamh, 0)); + pamh = NULL; + + /* + * conffile= specifies an existing file, + * envfile= specifies an empty file. + */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s/%s envfile=%s\n", + cwd, MODULE_NAME, + cwd, my_conf, "/dev/null")); + ASSERT_EQ(0, fclose(fp)); + + const char *env1[] = { "EDITOR=vim", "PAGER=more", NULL }; + check_env(env1); + + /* + * conffile= specifies an empty file, + * envfile= specifies an existing file. + */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s envfile=%s/%s\n", + cwd, MODULE_NAME, + "/dev/null", cwd, my_env)); + ASSERT_EQ(0, fclose(fp)); + + const char *env2[] = { "test_value=foo", "test2_value=bar", NULL }; + check_env(env2); + +#if defined (USE_ECONF) && defined (VENDORDIR) + + /* envfile is a directory. So values will be read from {TEST_NAME_DIR}/usr/etc and {TEST_NAME_DIR}/etc */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s envfile=%s/%s/\n", + cwd, MODULE_NAME, + "/dev/null", + cwd, dir)); + ASSERT_EQ(0, fclose(fp)); + + const char *env3[] = {"usr_etc_test=foo", "usr_etc_test2=bar", NULL}; + check_env(env3); + + /* conffile is a directory. So values will be read from {TEST_NAME_DIR}/usr/etc and {TEST_NAME_DIR}/etc */ + ASSERT_NE(NULL, fp = fopen(service_file, "w")); + ASSERT_LT(0, fprintf(fp, "#%%PAM-1.0\n" + "session required %s/.libs/%s.so" + " conffile=%s/%s/ envfile=%s\n", + cwd, MODULE_NAME, + cwd, dir, + "/dev/null")); + ASSERT_EQ(0, fclose(fp)); + + const char *env4[] = {"PAGER=emacs", "MANPAGER=less", NULL}; + check_env(env4); + +#endif + + /* cleanup */ + cleanup(); + ASSERT_EQ(0, unlink(service_file)); + + return 0; +}