1 /* 2 * Copyright (c) 2015-2026 Devin Teske <dteske@FreeBSD.org> 3 * Copyright (c) 2021-2026 Faraz Vahedi <kfv@FreeBSD.org> 4 * 5 * SPDX-License-Identifier: BSD-2-Clause 6 */ 7 8 #include <sys/stat.h> 9 10 #include <ctype.h> 11 #include <errno.h> 12 #include <fcntl.h> 13 #include <limits.h> 14 #include <stdint.h> 15 #include <stdio.h> 16 #include <stdlib.h> 17 #include <string.h> 18 #include <unistd.h> 19 20 #include "bsdconf.h" 21 #include "bsdconf_internal.h" 22 23 /* 24 * Rewrite the configuration file at `path', applying the per-directive 25 * actions described by the array of config options (first argument). Each 26 * option's action is one of: 27 * 28 * BSDCONF_ACTION_SET_VALUE set the directive to option->value.str, 29 * editing it in place if present or appending 30 * it to the file if absent; 31 * BSDCONF_ACTION_REMOVE delete the directive from the file; 32 * BSDCONF_ACTION_CHECK report (via option->result) whether the 33 * current value differs from option->value, 34 * without modifying the file. 35 * 36 * Comments, blank lines, statement ordering, and the formatting of untouched 37 * statements are preserved. With BSDCONF_OPERATOR_EQUALS, a non-default 38 * option->op both matches and rewrites the statement's assignment operator 39 * (make.conf(5) `+=' et al.). For each processed option, option->result is 40 * set to a bitmask of BSDCONF_DIRECTIVE_FOUND, BSDCONF_VALUE_CHANGED, 41 * BSDCONF_DIRECTIVE_ADDED, and BSDCONF_DIRECTIVE_REMOVED, and option->line 42 * is set to the line of the first match (if found). 43 * 44 * When every option is BSDCONF_ACTION_CHECK, or every SET_VALUE/REMOVE would 45 * leave the file unchanged, the original is left untouched: no temporary is 46 * created, mtime is not bumped, and hard links are not severed. Otherwise 47 * the file is replaced atomically: output is written to a temporary file in 48 * the same directory, flushed to disk with fsync(2), given the original's 49 * mode masked to 0666 (and, if permitted, its ownership), and then renamed 50 * over it. A crash mid-transaction leaves the original untouched. 51 * 52 * Returns zero on success; otherwise returns -1 and errno should be 53 * consulted. 54 */ 55 int 56 bsdconf_put(struct bsdconf_option options[], const char *path, 57 uint16_t processing_options, uint16_t put_options) 58 { 59 bool backup; 60 bool bequals; 61 bool bsemicolon; 62 bool case_sensitive; 63 bool emptyok; 64 bool nodup; 65 bool operator_equals; 66 bool quote_always; 67 bool require_equals; 68 bool strict_equals; 69 bool unquoted; 70 int dirfd = -1; 71 int fd = -1; 72 int last_ch = -1; /* last byte emitted, or -1 if none */ 73 int opchange; 74 int rv = -1; 75 int saved_errno; 76 int tmpfd = -1; 77 char *buf = NULL; 78 char *slash; 79 char *val = NULL; 80 const char *sep; 81 size_t buflen = 0; 82 size_t i; 83 size_t n; 84 size_t vlen; 85 size_t wc; /* write cursor: next unemitted byte of buf */ 86 uint32_t line = 1; 87 struct bsdconf_option *option; 88 struct bsdconf_stmt st; 89 struct stat sb; 90 char rpath[PATH_MAX]; 91 char tpath[PATH_MAX]; 92 93 /* Sanity check the arguments */ 94 if (options == NULL || path == NULL) { 95 errno = EINVAL; 96 return (-1); 97 } 98 99 /* Nothing to unlink until mkstemp(3) succeeds (see cleanup) */ 100 tpath[0] = '\0'; 101 102 /* Decode processing options */ 103 bequals = processing_options & BSDCONF_BREAK_ON_EQUALS; 104 bsemicolon = processing_options & BSDCONF_BREAK_ON_SEMICOLON; 105 case_sensitive = processing_options & BSDCONF_CASE_SENSITIVE; 106 operator_equals = processing_options & BSDCONF_OPERATOR_EQUALS; 107 require_equals = processing_options & BSDCONF_REQUIRE_EQUALS; 108 strict_equals = processing_options & BSDCONF_STRICT_EQUALS; 109 110 /* Decode put options */ 111 backup = put_options & BSDCONF_PUT_BACKUP; 112 emptyok = put_options & BSDCONF_PUT_ALLOW_EMPTY; 113 nodup = put_options & BSDCONF_PUT_NO_DUPLICATES; 114 quote_always = put_options & BSDCONF_PUT_QUOTE_ALWAYS; 115 unquoted = put_options & BSDCONF_PUT_UNQUOTED; 116 117 /* Quoting directives are mutually exclusive */ 118 if (unquoted && quote_always) { 119 errno = EINVAL; 120 return (-1); 121 } 122 123 /* Reset per-option results and reject empty values up front */ 124 for (n = 0; options[n].directive != NULL; n++) { 125 options[n].result = 0; 126 options[n].line = 0; 127 if (!emptyok && options[n].action == BSDCONF_ACTION_SET_VALUE && 128 bsdconf_value_empty(&options[n])) { 129 errno = EINVAL; 130 return (-1); 131 } 132 } 133 134 /* Resolve the file path */ 135 if (realpath(path, rpath) == NULL) 136 return (-1); 137 138 /* 139 * Open the original for reading. Only a regular file can be 140 * atomically replaced, so anything else is rejected below; 141 * O_NONBLOCK makes that check reachable (it is a no-op for regular 142 * files, but without it opening a fifo blocks awaiting a writer). 143 */ 144 if ((fd = open(rpath, O_RDONLY | O_NONBLOCK)) < 0) 145 return (-1); 146 if (fstat(fd, &sb) != 0) 147 goto cleanup; 148 if (!S_ISREG(sb.st_mode)) { 149 errno = EINVAL; 150 goto cleanup; 151 } 152 153 /* Slurp the original into memory */ 154 if ((buf = bsdconf_slurp(fd, &buflen)) == NULL) 155 goto cleanup; 156 157 /* 158 * Walk the original statement by statement. The replacement temporary 159 * is created lazily on the first real edit (see bsdconf_ensure_tmp()); 160 * CHECK and equal-value paths never open it. Bytes are emitted in 161 * order via the write cursor `wc'; a matched statement diverts 162 * around the region it edits or removes. 163 */ 164 wc = 0; 165 i = 0; 166 while (bsdconf_scan(buf, buflen, &i, &line, bequals, bsemicolon, 167 strict_equals, operator_equals, &st)) { 168 /* Statements without an `=' are unwritable targets */ 169 if (require_equals && !st.have_equals) 170 continue; 171 172 /* Locate the option (if any) matching this directive */ 173 option = NULL; 174 for (n = 0; options[n].directive != NULL; n++) { 175 if (!bsdconf_dir_matches(buf, st.dir_start, st.dir_end, 176 options[n].directive, case_sensitive)) 177 continue; 178 /* 179 * A non-zero match_line selects one physical 180 * statement (e.g. make(1) `+=' strike/edit of the 181 * last assignment containing a word). 182 */ 183 if (options[n].match_line != 0 && 184 options[n].match_line != st.line) 185 continue; 186 option = &options[n]; 187 break; 188 } 189 if (option == NULL) 190 continue; /* untouched; bytes flushed later */ 191 192 /* 193 * make(1)-style `+=': when match_line is unset, never rewrite 194 * an existing statement and do not mark the directive FOUND, 195 * so a fresh `name+=value' line is appended below. Existing 196 * assignments for the same name are left intact so make's 197 * cumulative semantics are preserved. With match_line set, 198 * rewrite (or remove) that specific statement instead. The 199 * caller (sysconf(8)) skips the put entirely when an 200 * identical `+=value' line is already present. 201 */ 202 if (operator_equals && 203 option->action == BSDCONF_ACTION_SET_VALUE && 204 option->op == BSDCONF_OP_APPEND && 205 option->match_line == 0) 206 continue; 207 208 /* Enforce the no-duplicates policy */ 209 if (nodup && (option->result & BSDCONF_DIRECTIVE_FOUND) != 0) { 210 errno = EEXIST; 211 goto cleanup; 212 } 213 if ((option->result & BSDCONF_DIRECTIVE_FOUND) == 0) 214 option->line = st.line; 215 option->result |= BSDCONF_DIRECTIVE_FOUND; 216 217 if (option->action == BSDCONF_ACTION_REMOVE) { 218 size_t rm_start; 219 size_t rm_end; 220 size_t k; 221 int done = 0; 222 int first_on_line = 1; 223 224 /* 225 * Is this the first statement on its physical line? 226 * With BSDCONF_BREAK_ON_SEMICOLON an earlier 227 * statement may precede it on the same line. 228 */ 229 for (k = st.line_start; k < st.dir_start; k++) 230 if (!isspace((unsigned char)buf[k])) { 231 first_on_line = 0; 232 break; 233 } 234 235 /* 236 * If a further statement follows on the same line 237 * (terminator is a semicolon with a directive after 238 * it), drop this statement and the trailing 239 * separator, leaving the neighbour intact. 240 */ 241 if (bsemicolon && st.term < buflen && 242 buf[st.term] == ';') { 243 size_t f = st.term + 1; 244 245 while (f < buflen && 246 isspace((unsigned char)buf[f]) && 247 buf[f] != '\n') 248 f++; 249 if (f < buflen && buf[f] != '\n' && 250 buf[f] != '#') { 251 rm_start = st.dir_start; 252 rm_end = f; 253 done = 1; 254 } 255 } 256 257 if (!done && first_on_line) { 258 /* Alone on the line: drop the whole line */ 259 rm_start = st.line_start; 260 rm_end = st.line_end; 261 } else if (!done) { 262 /* 263 * Last on a shared line: drop the preceding 264 * separator (whitespace, `;', whitespace) 265 * along with this statement, keeping the 266 * terminator. 267 */ 268 size_t s = st.dir_start; 269 270 while (s > st.line_start && 271 isspace((unsigned char)buf[s - 1])) 272 s--; 273 if (s > st.line_start && buf[s - 1] == ';') 274 s--; 275 while (s > st.line_start && 276 isspace((unsigned char)buf[s - 1])) 277 s--; 278 rm_start = s; 279 rm_end = st.term; 280 } 281 282 /* Never rewind before already-emitted bytes */ 283 if (rm_start < wc) 284 rm_start = wc; 285 if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath), 286 rpath, &sb) != 0) 287 goto cleanup; 288 if (bsdconf_emit(tmpfd, buf + wc, rm_start - wc, 289 &last_ch) != 0) 290 goto cleanup; 291 if (rm_end > wc) 292 wc = rm_end; 293 option->result |= BSDCONF_DIRECTIVE_REMOVED; 294 continue; 295 } 296 297 /* SET_VALUE and CHECK both need the formatted value */ 298 free(val); 299 if ((val = bsdconf_format_value(option, unquoted, 300 quote_always, bsemicolon)) == NULL) 301 goto cleanup; 302 vlen = strlen(val); 303 304 /* Does the assignment operator need to be rewritten? */ 305 opchange = operator_equals && 306 option->op != BSDCONF_OP_DEFAULT && st.have_equals && 307 st.op != option->op; 308 309 /* Compare against the value currently in the file */ 310 n = st.val_end - st.val_start; 311 if (!opchange && vlen == n && (n == 0 || 312 memcmp(val, buf + st.val_start, n) == 0)) { 313 /* Unchanged; nothing to do for either action */ 314 continue; 315 } 316 317 if (option->action == BSDCONF_ACTION_CHECK) { 318 option->result |= BSDCONF_VALUE_CHANGED; 319 continue; 320 } 321 322 /* BSDCONF_ACTION_SET_VALUE and an edit is required */ 323 if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath), 324 rpath, &sb) != 0) 325 goto cleanup; 326 if (opchange) { 327 /* Rewrite the operator token in place */ 328 if (bsdconf_emit(tmpfd, buf + wc, st.op_start - wc, 329 &last_ch) != 0) 330 goto cleanup; 331 if (bsdconf_emit(tmpfd, bsdconf_op_token(option->op), 332 strlen(bsdconf_op_token(option->op)), 333 &last_ch) != 0) 334 goto cleanup; 335 wc = st.eq + 1; 336 } 337 338 if (st.have_value) { 339 /* Replace the existing value in place */ 340 if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc, 341 &last_ch) != 0) 342 goto cleanup; 343 if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0) 344 goto cleanup; 345 wc = st.val_end; 346 } else if (option->type != BSDCONF_TYPE_NONE) { 347 /* Insert a value onto a value-less directive */ 348 if (bsdconf_emit(tmpfd, buf + wc, st.val_start - wc, 349 &last_ch) != 0) 350 goto cleanup; 351 /* Supply a separator unless one already precedes */ 352 if (st.val_start == wc || 353 (buf[st.val_start - 1] != '=' && 354 !isspace((unsigned char)buf[st.val_start - 1]))) { 355 sep = st.have_equals || require_equals || 356 bequals ? "=" : " "; 357 if (bsdconf_emit(tmpfd, sep, 1, 358 &last_ch) != 0) 359 goto cleanup; 360 } 361 if (bsdconf_emit(tmpfd, val, vlen, &last_ch) != 0) 362 goto cleanup; 363 wc = st.val_start; 364 } 365 option->result |= BSDCONF_VALUE_CHANGED; 366 } 367 368 /* 369 * Append any SET_VALUE directives that were not found, and finalize 370 * the result flags for CHECK directives that were absent. The tail of 371 * the original is flushed only when a temporary already exists or an 372 * append forces one into being. 373 */ 374 for (n = 0; options[n].directive != NULL; n++) { 375 option = &options[n]; 376 if ((option->result & BSDCONF_DIRECTIVE_FOUND) != 0) 377 continue; 378 if (option->action == BSDCONF_ACTION_CHECK) { 379 /* Absent means it differs from the desired value */ 380 option->result |= BSDCONF_VALUE_CHANGED; 381 continue; 382 } 383 if (option->action != BSDCONF_ACTION_SET_VALUE) 384 continue; /* REMOVE of an absent directive: no-op */ 385 386 if (bsdconf_ensure_tmp(&tmpfd, tpath, sizeof(tpath), 387 rpath, &sb) != 0) 388 goto cleanup; 389 /* Flush any unread prefix once before the first append */ 390 if (wc < buflen) { 391 if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, 392 &last_ch) != 0) 393 goto cleanup; 394 wc = buflen; 395 } 396 397 /* Ensure the emitted output ends with a newline first */ 398 if (last_ch != -1 && last_ch != '\n') { 399 if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0) 400 goto cleanup; 401 } 402 403 if (bsdconf_emit(tmpfd, option->directive, 404 strlen(option->directive), &last_ch) != 0) 405 goto cleanup; 406 if (option->type != BSDCONF_TYPE_NONE) { 407 free(val); 408 if ((val = bsdconf_format_value(option, unquoted, 409 quote_always, bsemicolon)) == NULL) 410 goto cleanup; 411 if (operator_equals && 412 option->op != BSDCONF_OP_DEFAULT) 413 sep = bsdconf_op_token(option->op); 414 else 415 sep = (bequals || require_equals) ? "=" : " "; 416 if (bsdconf_emit(tmpfd, sep, strlen(sep), 417 &last_ch) != 0) 418 goto cleanup; 419 if (bsdconf_emit(tmpfd, val, strlen(val), 420 &last_ch) != 0) 421 goto cleanup; 422 } 423 if (bsdconf_emit(tmpfd, "\n", 1, &last_ch) != 0) 424 goto cleanup; 425 /* 426 * Line number of the statement appended at EOF: one past 427 * the number of newlines in the original, or the next line 428 * if we have to emit a separating newline first. 429 */ 430 { 431 uint32_t al = 1; 432 size_t k; 433 434 for (k = 0; k < buflen; k++) 435 if (buf[k] == '\n') 436 al++; 437 if (buflen > 0 && buf[buflen - 1] != '\n') 438 al++; 439 option->line = al; 440 } 441 option->result |= 442 BSDCONF_DIRECTIVE_ADDED | BSDCONF_VALUE_CHANGED; 443 } 444 445 /* 446 * Nothing changed: every option was CHECK or an equal-value 447 * SET_VALUE/REMOVE-miss. Leave the original file alone. 448 */ 449 if (tmpfd < 0) { 450 rv = 0; 451 goto cleanup; 452 } 453 454 /* Flush any remaining unread tail of the original */ 455 if (bsdconf_emit(tmpfd, buf + wc, buflen - wc, &last_ch) != 0) 456 goto cleanup; 457 458 /* Optionally back up the original before replacing it */ 459 if (backup) { 460 char bpath[PATH_MAX]; 461 int bfd; 462 463 if (snprintf(bpath, sizeof(bpath), "%s.bak", rpath) >= 464 (int)sizeof(bpath)) { 465 errno = ENAMETOOLONG; 466 goto cleanup; 467 } 468 /* 469 * O_NOFOLLOW: refuse to follow a planted symbolic link 470 * lest the backup clobber whatever it points at. 471 */ 472 if ((bfd = open(bpath, O_WRONLY | O_CREAT | O_TRUNC | 473 O_NOFOLLOW, 0600)) < 0) 474 goto cleanup; 475 if (fchmod(bfd, sb.st_mode & 0777) != 0) { 476 saved_errno = errno; 477 close(bfd); 478 errno = saved_errno; 479 goto cleanup; 480 } 481 if (bsdconf_writeall(bfd, buf, buflen) != 0) { 482 saved_errno = errno; 483 close(bfd); 484 errno = saved_errno; 485 goto cleanup; 486 } 487 if (close(bfd) != 0) 488 goto cleanup; 489 } 490 491 /* Commit: flush to disk, then atomically replace the original */ 492 if (fsync(tmpfd) != 0) 493 goto cleanup; 494 if (close(tmpfd) != 0) { 495 tmpfd = -1; 496 goto cleanup; 497 } 498 tmpfd = -1; 499 if (rename(tpath, rpath) != 0) 500 goto cleanup; 501 tpath[0] = '\0'; /* renamed away; nothing to unlink */ 502 503 /* Best-effort: persist the directory entry change */ 504 if ((slash = strrchr(rpath, '/')) != NULL) { 505 char dpath[PATH_MAX]; 506 size_t dlen = (size_t)(slash - rpath); 507 508 if (dlen == 0) 509 dlen = 1; /* the root directory */ 510 if (dlen < sizeof(dpath)) { 511 memcpy(dpath, rpath, dlen); 512 dpath[dlen] = '\0'; 513 if ((dirfd = open(dpath, O_RDONLY)) >= 0) { 514 (void)fsync(dirfd); 515 close(dirfd); 516 dirfd = -1; 517 } 518 } 519 } 520 521 rv = 0; 522 523 cleanup: 524 saved_errno = errno; 525 if (fd >= 0) 526 close(fd); 527 if (tmpfd >= 0) 528 close(tmpfd); 529 if (dirfd >= 0) 530 close(dirfd); 531 if (rv != 0 && tpath[0] != '\0') 532 (void)unlink(tpath); /* discard the incomplete temporary */ 533 free(buf); 534 free(val); 535 errno = saved_errno; 536 return (rv); 537 } 538