Home | History | Annotate | Line # | Download | only in libpam
      1 /*	$NetBSD: openpam_configure.c,v 1.8 2025/09/06 12:23:17 riastradh Exp $	*/
      2 
      3 /*-
      4  * Copyright (c) 2001-2003 Networks Associates Technology, Inc.
      5  * Copyright (c) 2004-2025 Dag-Erling Smrgrav
      6  * All rights reserved.
      7  *
      8  * This software was developed for the FreeBSD Project by ThinkSec AS and
      9  * Network Associates Laboratories, the Security Research Division of
     10  * Network Associates, Inc.  under DARPA/SPAWAR contract N66001-01-C-8035
     11  * ("CBOSS"), as part of the DARPA CHATS research program.
     12  *
     13  * Redistribution and use in source and binary forms, with or without
     14  * modification, are permitted provided that the following conditions
     15  * are met:
     16  * 1. Redistributions of source code must retain the above copyright
     17  *    notice, this list of conditions and the following disclaimer.
     18  * 2. Redistributions in binary form must reproduce the above copyright
     19  *    notice, this list of conditions and the following disclaimer in the
     20  *    documentation and/or other materials provided with the distribution.
     21  * 3. The name of the author may not be used to endorse or promote
     22  *    products derived from this software without specific prior written
     23  *    permission.
     24  *
     25  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
     26  * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     27  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
     28  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
     29  * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
     30  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
     31  * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
     32  * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
     33  * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
     34  * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
     35  * SUCH DAMAGE.
     36  */
     37 
     38 #ifdef HAVE_CONFIG_H
     39 # include "config.h"
     40 #endif
     41 
     42 #include <sys/cdefs.h>
     43 __RCSID("$NetBSD: openpam_configure.c,v 1.8 2025/09/06 12:23:17 riastradh Exp $");
     44 
     45 #include <sys/param.h>
     46 
     47 #include <errno.h>
     48 #include <stdio.h>
     49 #include <stdlib.h>
     50 #include <string.h>
     51 
     52 #include <security/pam_appl.h>
     53 
     54 #include "openpam_impl.h"
     55 #include "openpam_ctype.h"
     56 #include "openpam_strlcat.h"
     57 #include "openpam_strlcpy.h"
     58 
     59 static int openpam_load_chain(pam_handle_t *, const char *, pam_facility_t);
     60 
     61 /*
     62  * Validate a service name.
     63  *
     64  * Returns a non-zero value if the argument points to a NUL-terminated
     65  * string consisting entirely of characters in the POSIX portable filename
     66  * character set, excluding the path separator character.
     67  */
     68 static int
     69 valid_service_name(const char *name)
     70 {
     71 	const char *p;
     72 
     73 	if (OPENPAM_FEATURE(RESTRICT_SERVICE_NAME)) {
     74 		/* path separator not allowed */
     75 		for (p = name; *p != '\0'; ++p)
     76 			if (!is_pfcs(*p))
     77 				return (0);
     78 	} else {
     79 		/* path separator allowed */
     80 		for (p = name; *p != '\0'; ++p)
     81 			if (!is_pfcs(*p) && *p != '/')
     82 				return (0);
     83 	}
     84 	return (1);
     85 }
     86 
     87 /*
     88  * Parse the facility name.
     89  *
     90  * Returns the corresponding pam_facility_t value, or -1 if the argument
     91  * is not a valid facility name.
     92  */
     93 static pam_facility_t
     94 parse_facility_name(const char *name)
     95 {
     96 	int i;
     97 
     98 	for (i = 0; i < PAM_NUM_FACILITIES; ++i)
     99 		if (strcmp(pam_facility_name[i], name) == 0)
    100 			return (i);
    101 	return ((pam_facility_t)-1);
    102 }
    103 
    104 /*
    105  * Parse the control flag.
    106  *
    107  * Returns the corresponding pam_control_t value, or -1 if the argument is
    108  * not a valid control flag name.
    109  */
    110 static pam_control_t
    111 parse_control_flag(const char *name)
    112 {
    113 	pam_control_t i;
    114 
    115 	for (i = PAM_BINDING; i < PAM_NUM_CONTROL_FLAGS; ++i)
    116 		if (strcmp(pam_control_flag_name[i], name) == 0)
    117 			return (i);
    118 	return ((pam_control_t)-1);
    119 }
    120 
    121 /*
    122  * Validate a file name.
    123  *
    124  * Returns a non-zero value if the argument points to a NUL-terminated
    125  * string consisting entirely of characters in the POSIX portable filename
    126  * character set, including the path separator character.
    127  */
    128 static int
    129 valid_module_name(const char *name)
    130 {
    131 	const char *p;
    132 
    133 	if (OPENPAM_FEATURE(RESTRICT_MODULE_NAME)) {
    134 		/* path separator not allowed */
    135 		for (p = name; *p != '\0'; ++p)
    136 			if (!is_pfcs(*p))
    137 				return (0);
    138 	} else {
    139 		/* path separator allowed */
    140 		for (p = name; *p != '\0'; ++p)
    141 			if (!is_pfcs(*p) && *p != '/')
    142 				return (0);
    143 	}
    144 	return (1);
    145 }
    146 
    147 typedef enum { pam_conf_style, pam_d_style } openpam_style_t;
    148 
    149 /*
    150  * Extracts given chains from a policy file.
    151  *
    152  * Returns the number of policy entries which were found for the specified
    153  * service and facility, or -1 if a system error occurred or a syntax
    154  * error was encountered.
    155  */
    156 static int
    157 openpam_parse_chain(pam_handle_t *pamh,
    158 	const char *service,
    159 	pam_facility_t facility,
    160 	FILE *f,
    161 	const char *filename,
    162 	openpam_style_t style)
    163 {
    164 	pam_chain_t *this, **next;
    165 	pam_module_t *module;
    166 	pam_facility_t fclt;
    167 	pam_control_t ctlf;
    168 	char *name, *servicename, *modulename;
    169 	int count, lineno, nonfatal, ret, serrno;
    170 	char **wordv, *word;
    171 	int i, wordc;
    172 
    173 	count = 0;
    174 	this = NULL;
    175 	name = NULL;
    176 	lineno = 0;
    177 	wordc = 0;
    178 	wordv = NULL;
    179 	while ((wordv = openpam_readlinev(f, &lineno, &wordc)) != NULL) {
    180 		/* blank line? */
    181 		if (wordc == 0) {
    182 			FREEV(wordc, wordv);
    183 			continue;
    184 		}
    185 		i = 0;
    186 
    187 		/* check service name if necessary */
    188 		if (style == pam_conf_style &&
    189 		    strcmp(wordv[i++], service) != 0) {
    190 			FREEV(wordc, wordv);
    191 			continue;
    192 		}
    193 
    194 		/* check facility name */
    195 		if ((word = wordv[i++]) == NULL) {
    196 			openpam_log(PAM_LOG_ERROR,
    197 			    "%s(%d): missing facility",
    198 			    filename, lineno);
    199 			errno = EINVAL;
    200 			goto fail;
    201 		}
    202 		if (*word == '-') {
    203 			nonfatal = 1;
    204 			word++;
    205 		} else {
    206 			nonfatal = 0;
    207 		}
    208 		if ((fclt = parse_facility_name(word)) == (pam_facility_t)-1) {
    209 			openpam_log(PAM_LOG_ERROR,
    210 			    "%s(%d): invalid facility",
    211 			    filename, lineno);
    212 			errno = EINVAL;
    213 			goto fail;
    214 		}
    215 		if (facility != fclt && facility != PAM_FACILITY_ANY) {
    216 			FREEV(wordc, wordv);
    217 			continue;
    218 		}
    219 
    220 		/* control flag or "include" */
    221 		if ((word = wordv[i++]) == NULL) {
    222 			openpam_log(PAM_LOG_ERROR,
    223 			    "%s(%d): missing control flag",
    224 			    filename, lineno);
    225 			errno = EINVAL;
    226 			goto fail;
    227 		}
    228 		if (strcmp(word, "include") == 0) {
    229 			if ((servicename = wordv[i++]) == NULL) {
    230 				openpam_log(PAM_LOG_ERROR,
    231 				    "%s(%d): missing service name",
    232 				    filename, lineno);
    233 				errno = EINVAL;
    234 				goto fail;
    235 			}
    236 			if (!valid_service_name(servicename)) {
    237 				openpam_log(PAM_LOG_ERROR,
    238 				    "%s(%d): invalid service name",
    239 				    filename, lineno);
    240 				errno = EINVAL;
    241 				goto fail;
    242 			}
    243 			if (wordv[i] != NULL) {
    244 				openpam_log(PAM_LOG_ERROR,
    245 				    "%s(%d): garbage at end of line",
    246 				    filename, lineno);
    247 				errno = EINVAL;
    248 				goto fail;
    249 			}
    250 			ret = openpam_load_chain(pamh, servicename, fclt);
    251 			FREEV(wordc, wordv);
    252 			if (ret < 0) {
    253 				/*
    254 				 * Bogus errno, but this ensures that the
    255 				 * outer loop does not just ignore the
    256 				 * error and keep searching.
    257 				 */
    258 				if (errno == ENOENT) {
    259 					if (nonfatal)
    260 						continue;
    261 					errno = EINVAL;
    262 				}
    263 				goto fail;
    264 			}
    265 			continue;
    266 		}
    267 		if ((ctlf = parse_control_flag(word)) == (pam_control_t)-1) {
    268 			openpam_log(PAM_LOG_ERROR,
    269 			    "%s(%d): invalid control flag",
    270 			    filename, lineno);
    271 			errno = EINVAL;
    272 			goto fail;
    273 		}
    274 
    275 		/* get module name */
    276 		if ((modulename = wordv[i++]) == NULL) {
    277 			openpam_log(PAM_LOG_ERROR,
    278 			    "%s(%d): missing module name",
    279 			    filename, lineno);
    280 			errno = EINVAL;
    281 			goto fail;
    282 		}
    283 		if (!valid_module_name(modulename)) {
    284 			openpam_log(PAM_LOG_ERROR,
    285 			    "%s(%d): invalid module name",
    286 			    filename, lineno);
    287 			errno = EINVAL;
    288 			goto fail;
    289 		}
    290 
    291 		/* load module */
    292 		if ((module = openpam_load_module(modulename)) == NULL) {
    293 			if (errno == ENOENT) {
    294 				if (nonfatal) {
    295 					FREEV(wordc, wordv);
    296 					continue;
    297 				}
    298 				errno = ENOEXEC;
    299 			}
    300 			goto fail;
    301 		}
    302 
    303 		/* allocate new entry */
    304 		if ((this = calloc(1, sizeof *this)) == NULL)
    305 			goto syserr;
    306 		this->flag = (int)ctlf;
    307 		this->module = module;
    308 
    309 		/*
    310 		 * The remaining items in wordv are the module's
    311 		 * arguments.  We could set this->optv = wordv + i, but
    312 		 * then free(this->optv) wouldn't work.  Instead, we free
    313 		 * the words we've already consumed, shift the rest up,
    314 		 * and clear the tail end of the array.
    315 		 */
    316 		this->optc = wordc - i;
    317 		for (i = 0; i < wordc - this->optc; ++i) {
    318 			FREE(wordv[i]);
    319 		}
    320 		for (i = 0; i < this->optc; ++i) {
    321 			wordv[i] = wordv[wordc - this->optc + i];
    322 			wordv[wordc - this->optc + i] = NULL;
    323 		}
    324 		this->optv = wordv;
    325 		wordv = NULL;
    326 		wordc = 0;
    327 
    328 		/* hook it up */
    329 		for (next = &pamh->chains[fclt]; *next != NULL;
    330 		     next = &(*next)->next)
    331 			/* nothing */ ;
    332 		*next = this;
    333 		this = NULL;
    334 		++count;
    335 	}
    336 	/*
    337 	 * The loop ended because openpam_readword() returned NULL, which
    338 	 * can happen for four different reasons: an I/O error (ferror(f)
    339 	 * is true), a memory allocation failure (ferror(f) is false,
    340 	 * feof(f) is false, errno is non-zero), the file ended with an
    341 	 * unterminated quote or backslash escape (ferror(f) is false,
    342 	 * feof(f) is true, errno is non-zero), or the end of the file was
    343 	 * reached without error (ferror(f) is false, feof(f) is true,
    344 	 * errno is zero).
    345 	 */
    346 	if (ferror(f) || errno != 0)
    347 		goto syserr;
    348 	if (!feof(f))
    349 		goto fail;
    350 	fclose(f);
    351 	return (count);
    352 syserr:
    353 	serrno = errno;
    354 	openpam_log(PAM_LOG_ERROR, "%s: %m", filename);
    355 	errno = serrno;
    356 	/* fall through */
    357 fail:
    358 	serrno = errno;
    359 	if (this && this->optc && this->optv)
    360 		FREEV(this->optc, this->optv);
    361 	FREE(this);
    362 	FREEV(wordc, wordv);
    363 	FREE(wordv);
    364 	FREE(name);
    365 	fclose(f);
    366 	errno = serrno;
    367 	return (-1);
    368 }
    369 
    370 /*
    371  * Read the specified chains from the specified file.
    372  *
    373  * Returns 0 if the file exists but does not contain any matching lines.
    374  *
    375  * Returns -1 and sets errno to ENOENT if the file does not exist.
    376  *
    377  * Returns -1 and sets errno to some other non-zero value if the file
    378  * exists but is unsafe or unreadable, or an I/O error occurs.
    379  */
    380 static int
    381 openpam_load_file(pam_handle_t *pamh,
    382 	const char *service,
    383 	pam_facility_t facility,
    384 	const char *filename,
    385 	openpam_style_t style)
    386 {
    387 	FILE *f;
    388 	int ret, serrno;
    389 
    390 	/* attempt to open the file */
    391 	if ((f = fopen(filename, "r")) == NULL) {
    392 		serrno = errno;
    393 		openpam_log(errno == ENOENT ? PAM_LOG_DEBUG : PAM_LOG_ERROR,
    394 		    "%s: %m", filename);
    395 		errno = serrno;
    396 		RETURNN(-1);
    397 	} else {
    398 		openpam_log(PAM_LOG_DEBUG, "found %s", filename);
    399 	}
    400 
    401 	/* verify type, ownership and permissions */
    402 	if (OPENPAM_FEATURE(VERIFY_POLICY_FILE) &&
    403 	    openpam_check_desc_owner_perms(filename, fileno(f)) != 0) {
    404 		/* already logged the cause */
    405 		serrno = errno;
    406 		fclose(f);
    407 		errno = serrno;
    408 		RETURNN(-1);
    409 	}
    410 
    411 	/* parse the file */
    412 	ret = openpam_parse_chain(pamh, service, facility,
    413 	    f, filename, style);
    414 	RETURNN(ret);
    415 }
    416 
    417 /*
    418  * Locates the policy file for a given service and reads the given chains
    419  * from it.
    420  *
    421  * Returns the number of policy entries which were found for the specified
    422  * service and facility, or -1 if a system error occurred or a syntax
    423  * error was encountered.
    424  */
    425 static int
    426 openpam_load_chain(pam_handle_t *pamh,
    427 	const char *service,
    428 	pam_facility_t facility)
    429 {
    430 	const char *p, **path;
    431 	char filename[PATH_MAX];
    432 	size_t len;
    433 	openpam_style_t style;
    434 	int ret;
    435 
    436 	ENTERS(facility < 0 ? "any" : pam_facility_name[facility]);
    437 
    438 	/* either absolute or relative to cwd */
    439 	if (strchr(service, '/') != NULL) {
    440 		if ((p = strrchr(service, '.')) != NULL && strcmp(p, ".conf") == 0)
    441 			style = pam_conf_style;
    442 		else
    443 			style = pam_d_style;
    444 		ret = openpam_load_file(pamh, service, facility,
    445 		    service, style);
    446 		RETURNN(ret);
    447 	}
    448 
    449 	/* search standard locations */
    450 	for (path = openpam_policy_path; *path != NULL; ++path) {
    451 		/* construct filename */
    452 		len = strlcpy(filename, *path, sizeof filename);
    453 		if (len >= sizeof filename) {
    454 			errno = ENAMETOOLONG;
    455 			RETURNN(-1);
    456 		}
    457 		if (filename[len - 1] == '/') {
    458 			len = strlcat(filename, service, sizeof filename);
    459 			if (len >= sizeof filename) {
    460 				errno = ENAMETOOLONG;
    461 				RETURNN(-1);
    462 			}
    463 			style = pam_d_style;
    464 		} else {
    465 			style = pam_conf_style;
    466 		}
    467 		ret = openpam_load_file(pamh, service, facility,
    468 		    filename, style);
    469 		/* success */
    470 		if (ret > 0)
    471 			RETURNN(ret);
    472 		/* the file exists, but an error occurred */
    473 		if (ret == -1 && errno != ENOENT)
    474 			RETURNN(ret);
    475 		/* in pam.d style, an empty file counts as a hit */
    476 		if (ret == 0 && style == pam_d_style)
    477 			RETURNN(ret);
    478 	}
    479 
    480 	/* no hit */
    481 	errno = ENOENT;
    482 	RETURNN(-1);
    483 }
    484 
    485 /*
    486  * OpenPAM internal
    487  *
    488  * Configure a service
    489  */
    490 
    491 int
    492 openpam_configure(pam_handle_t *pamh,
    493 	const char *service)
    494 {
    495 	pam_facility_t fclt;
    496 	int serrno;
    497 
    498 	ENTERS(service);
    499 	if (!valid_service_name(service)) {
    500 		openpam_log(PAM_LOG_ERROR, "invalid service name");
    501 		RETURNC(PAM_SYSTEM_ERR);
    502 	}
    503 	if (openpam_load_chain(pamh, service, PAM_FACILITY_ANY) < 0) {
    504 		if (errno != ENOENT)
    505 			goto load_err;
    506 	}
    507 	for (fclt = 0; fclt < PAM_NUM_FACILITIES; ++fclt) {
    508 		if (pamh->chains[fclt] != NULL)
    509 			continue;
    510 		if (OPENPAM_FEATURE(FALLBACK_TO_OTHER)) {
    511 			if (openpam_load_chain(pamh, PAM_OTHER, fclt) < 0)
    512 				goto load_err;
    513 		}
    514 	}
    515 #ifdef __NetBSD__
    516 	/*
    517 	 * On NetBSD we require the AUTH chain to have a binding,
    518 	 * a required, or requisite module.
    519 	 */
    520 	{
    521 		pam_chain_t *this = pamh->chains[PAM_AUTH];
    522 		for (; this != NULL; this = this->next)
    523 			if (this->flag == PAM_BINDING ||
    524 			    this->flag == PAM_REQUIRED ||
    525 			    this->flag == PAM_REQUISITE)
    526 				break;
    527 		if (this == NULL) {
    528 			openpam_log(PAM_LOG_ERROR,
    529 			    "No required, requisite, or binding component "
    530 			    "in service %s, facility %s",
    531 			    service, pam_facility_name[PAM_AUTH]);
    532 			goto load_err;
    533 		}
    534 	}
    535 #endif
    536 	RETURNC(PAM_SUCCESS);
    537 load_err:
    538 	serrno = errno;
    539 	openpam_clear_chains(pamh->chains);
    540 	errno = serrno;
    541 	RETURNC(PAM_SYSTEM_ERR);
    542 }
    543 
    544 /*
    545  * NODOC
    546  *
    547  * Error codes:
    548  *	PAM_SYSTEM_ERR
    549  */
    550