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