| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 | |
| 11 | |
| 12 | |
| 13 | |
| 14 | |
| 15 | |
| 16 | |
| 17 | |
| 18 | |
| 19 | |
| 20 | |
| 21 | |
| 22 | |
| 23 | |
| 24 | |
| 25 | |
| 26 | |
| 27 | |
| 28 | |
| 29 | |
| 30 |
|
| 31 | |
| 32 | |
| 33 | |
| 34 | |
| 35 | |
| 36 | |
| 37 | |
| 38 |
|
| 39 | (define-module (gnu build activation)
|
| 40 | #:use-module (gnu system accounts)
|
| 41 | #:use-module (gnu system privilege)
|
| 42 | #:use-module (gnu build accounts)
|
| 43 | #:use-module (gnu build linux-boot)
|
| 44 | #:use-module (guix build utils)
|
| 45 | #:use-module ((guix build syscalls) #:select (with-file-lock))
|
| 46 | #:use-module (ice-9 ftw)
|
| 47 | #:use-module (ice-9 match)
|
| 48 | #:use-module (ice-9 vlist)
|
| 49 | #:use-module (srfi srfi-1)
|
| 50 | #:use-module (srfi srfi-11)
|
| 51 | #:use-module (srfi srfi-26)
|
| 52 | #:export (activate-users+groups
|
| 53 | activate-subuids+subgids
|
| 54 | activate-user-home
|
| 55 | activate-etc
|
| 56 | activate-privileged-programs
|
| 57 | activate-special-files
|
| 58 | activate-modprobe
|
| 59 | activate-firmware
|
| 60 | activate-ptrace-attach
|
| 61 | activate-current-system
|
| 62 | mkdir-p/perms))
|
| 63 |
|
| 64 | (define %skeleton-directory
|
| 65 | |
| 66 | |
| 67 | "/etc/skel/")
|
| 68 |
|
| 69 | (define (dot-or-dot-dot? file)
|
| 70 | (member file '("." "..")))
|
| 71 |
|
| 72 | (define (mkdir-p/perms directory owner bits)
|
| 73 | "Create directory DIRECTORY and all its ancestors.
|
| 74 |
|
| 75 | Additionally, verify no component of DIRECTORY is a symbolic link,
|
| 76 | without TOCTTOU races. However, if OWNER differs from the the current
|
| 77 | (process) uid/gid, there is a small window in which DIRECTORY is set to the
|
| 78 | current (process) uid/gid instead of OWNER. This is not expected to be
|
| 79 | a problem in practice.
|
| 80 |
|
| 81 | The permission bits and owner of DIRECTORY are set to BITS and OWNER.
|
| 82 | Anything above DIRECTORY that already exists keeps
|
| 83 | its old owner and bits. For components that do not exist yet, the owner
|
| 84 | and bits are set according to the default behaviour of 'mkdir'."
|
| 85 | (define absolute?
|
| 86 | (string-prefix? "/" directory))
|
| 87 |
|
| 88 | (define not-slash
|
| 89 | (char-set-complement (char-set #\/)))
|
| 90 |
|
| 91 | |
| 92 | |
| 93 | (define open-flags (logior O_CLOEXEC |
| 94 | O_RDONLY |
| 95 | O_NOFOLLOW |
| 96 | O_DIRECTORY)) |
| 97 |
|
| 98 | (let loop ((components (string-tokenize directory not-slash))
|
| 99 | (root (open (if absolute? "/" ".") open-flags)))
|
| 100 | (match components
|
| 101 | ((head tail ...)
|
| 102 | (let retry ()
|
| 103 | |
| 104 | (match (catch 'system-error
|
| 105 | (lambda ()
|
| 106 | (openat root head open-flags))
|
| 107 | (lambda args
|
| 108 | (if (= ENOENT (system-error-errno args))
|
| 109 | #false
|
| 110 | (begin
|
| 111 | (close-port root)
|
| 112 | (apply throw args)))))
|
| 113 | ((? port? new-root)
|
| 114 | (close root)
|
| 115 | (loop tail new-root))
|
| 116 | (#false
|
| 117 | |
| 118 | (catch 'system-error
|
| 119 | (lambda _
|
| 120 | (if (null? tail)
|
| 121 | (mkdirat root head bits)
|
| 122 | (mkdirat root head)))
|
| 123 | (lambda args
|
| 124 | |
| 125 | (unless (= EEXIST (system-error-errno args))
|
| 126 | (close-port root)
|
| 127 | (apply throw args))))
|
| 128 | (retry)))))
|
| 129 | (()
|
| 130 | (catch 'system-error
|
| 131 | (lambda ()
|
| 132 | (chown root (passwd:uid owner) (passwd:gid owner))
|
| 133 | (chmod root bits))
|
| 134 | (lambda args
|
| 135 | (close-port root)
|
| 136 | (apply throw args)))
|
| 137 | (close-port root)
|
| 138 | (values)))))
|
| 139 |
|
| 140 | (define* (copy-account-skeletons home
|
| 141 | #:key
|
| 142 | (directory %skeleton-directory)
|
| 143 | uid gid)
|
| 144 | "Copy the account skeletons from DIRECTORY to HOME and make them writable.
|
| 145 | When UID is an integer, make it the owner of all the files created except the
|
| 146 | home directory; likewise for GID."
|
| 147 | (define (set-permission file)
|
| 148 | (when (or uid gid)
|
| 149 | (chown file (or uid -1) (or gid -1)))
|
| 150 | (make-file-writable file))
|
| 151 |
|
| 152 | (let ((files (scandir directory (negate dot-or-dot-dot?)
|
| 153 | string<?)))
|
| 154 | (mkdir-p home)
|
| 155 | (for-each (lambda (file)
|
| 156 | (let ((target (string-append home "/" file)))
|
| 157 | (copy-recursively (string-append directory "/" file)
|
| 158 | target
|
| 159 | #:log (%make-void-port "w"))
|
| 160 | (for-each set-permission
|
| 161 | (find-files target (const #t)
|
| 162 | #:directories? #t))))
|
| 163 | files)))
|
| 164 |
|
| 165 | (define* (make-skeletons-writable home
|
| 166 | #:optional (directory %skeleton-directory))
|
| 167 | "Make sure that the files that have been copied from DIRECTORY to HOME are
|
| 168 | owner-writable in HOME."
|
| 169 | (let ((files (scandir directory (negate dot-or-dot-dot?)
|
| 170 | string<?)))
|
| 171 | (for-each (lambda (file)
|
| 172 | (let ((target (string-append home "/" file)))
|
| 173 | (when (file-exists? target)
|
| 174 | (make-file-writable target))))
|
| 175 | files)))
|
| 176 |
|
| 177 | (define (duplicates lst)
|
| 178 | "Return elements from LST present more than once in LST."
|
| 179 | (let loop ((lst lst)
|
| 180 | (seen vlist-null)
|
| 181 | (result '()))
|
| 182 | (match lst
|
| 183 | (()
|
| 184 | (reverse result))
|
| 185 | ((head . tail)
|
| 186 | (loop tail
|
| 187 | (vhash-cons head #t seen)
|
| 188 | (if (vhash-assoc head seen)
|
| 189 | (cons head result)
|
| 190 | result))))))
|
| 191 |
|
| 192 | (define (activate-users+groups users groups)
|
| 193 | "Make sure USERS (a list of user account records) and GROUPS (a list of user
|
| 194 | group records) are all available."
|
| 195 | (define (make-home-directory user)
|
| 196 | (let ((home (user-account-home-directory user))
|
| 197 | (pwd (getpwnam (user-account-name user))))
|
| 198 | (mkdir-p home)
|
| 199 |
|
| 200 | |
| 201 | |
| 202 | |
| 203 | (chown home (passwd:uid pwd) (passwd:gid pwd))
|
| 204 | (chmod home #o700)))
|
| 205 |
|
| 206 | (define system-accounts
|
| 207 | (filter (lambda (user)
|
| 208 | (and (user-account-system? user)
|
| 209 | (user-account-create-home-directory? user)))
|
| 210 | users))
|
| 211 |
|
| 212 | |
| 213 | (mkdir-p "/var/lib")
|
| 214 |
|
| 215 | |
| 216 | |
| 217 | |
| 218 | (with-file-lock %password-lock-file
|
| 219 | (let-values (((groups passwd shadow)
|
| 220 | (user+group-databases users groups)))
|
| 221 | (write-group groups)
|
| 222 | (write-passwd passwd)
|
| 223 | (write-shadow shadow)))
|
| 224 |
|
| 225 | |
| 226 | |
| 227 | (for-each make-home-directory system-accounts)
|
| 228 |
|
| 229 | |
| 230 | |
| 231 | (for-each (lambda (directory)
|
| 232 | (chown directory 0 0)
|
| 233 | (chmod directory #o555))
|
| 234 | (duplicates (map user-account-home-directory system-accounts))))
|
| 235 |
|
| 236 | (define (activate-subuids+subgids subuids subgids)
|
| 237 | "Make sure SUBUIDS (a list of subid range records) and SUBGIDS (a list of
|
| 238 | subid range records) are all available."
|
| 239 |
|
| 240 | |
| 241 | |
| 242 | |
| 243 | (with-file-lock "/etc/subgid.lock"
|
| 244 | (let-values (((subuid subgid)
|
| 245 | (subuid+subgid-databases subuids subgids)))
|
| 246 | (write-subgid subgid)))
|
| 247 |
|
| 248 | (with-file-lock "/etc/subuid.lock"
|
| 249 | (let-values (((subuid subgid)
|
| 250 | (subuid+subgid-databases subuids subgids)))
|
| 251 | (write-subuid subuid))))
|
| 252 |
|
| 253 | (define (activate-user-home users)
|
| 254 | "Create and populate the home directory of USERS, a list of tuples, unless
|
| 255 | they already exist."
|
| 256 | (define ensure-user-home
|
| 257 | (lambda (user)
|
| 258 | (let ((name (user-account-name user))
|
| 259 | (home (user-account-home-directory user))
|
| 260 | (create-home? (user-account-create-home-directory? user))
|
| 261 | (system? (user-account-system? user)))
|
| 262 | |
| 263 | |
| 264 | (unless (or (not home) (not create-home?) system?
|
| 265 | (directory-exists? home))
|
| 266 | (let* ((pw (getpwnam name))
|
| 267 | (uid (passwd:uid pw))
|
| 268 | (gid (passwd:gid pw)))
|
| 269 | (mkdir-p home)
|
| 270 | (chmod home #o700)
|
| 271 | (copy-account-skeletons home
|
| 272 | #:uid uid #:gid gid)
|
| 273 |
|
| 274 | |
| 275 | |
| 276 | |
| 277 | |
| 278 | (chown home uid gid))))))
|
| 279 |
|
| 280 | (for-each ensure-user-home users))
|
| 281 |
|
| 282 | (define* (canonicalize-path* file)
|
| 283 | "A safe version of `canonicalize-path' that warns rather than raises on errors.
|
| 284 | `canonicalize-path' uses `realpath(2)', which can return various errors like
|
| 285 | EINVAL, ELOOP, etc."
|
| 286 | (or (false-if-exception (canonicalize-path file))
|
| 287 | (begin
|
| 288 | (format (current-warning-port)
|
| 289 | "warning: could not canonicalize file `~a'; using as-is~%"
|
| 290 | file)
|
| 291 | file)))
|
| 292 |
|
| 293 | (define (activate-etc etc)
|
| 294 | "Install ETC, a directory in the store, as the source of static files for
|
| 295 | /etc."
|
| 296 |
|
| 297 | |
| 298 | |
| 299 |
|
| 300 | (define (rm-f file)
|
| 301 | (false-if-exception (delete-file file)))
|
| 302 |
|
| 303 | (format #t "populating /etc from ~a...~%" etc)
|
| 304 | (mkdir-p "/etc")
|
| 305 |
|
| 306 | |
| 307 | |
| 308 | |
| 309 | |
| 310 | |
| 311 | |
| 312 | (rm-f "/etc/ssl")
|
| 313 | (symlink "/run/current-system/profile/etc/ssl" "/etc/ssl")
|
| 314 |
|
| 315 | (for-each (lambda (file)
|
| 316 | (let ((target (string-append "/etc/" file))
|
| 317 | |
| 318 | |
| 319 | |
| 320 | |
| 321 | |
| 322 | (source (canonicalize-path* (string-append etc "/" file))))
|
| 323 | (rm-f target)
|
| 324 | (cond ((string=? (basename target) "sudoers")
|
| 325 | (begin
|
| 326 | |
| 327 | (copy-file source target)
|
| 328 | |
| 329 | (chmod target #o440)))
|
| 330 | ((string=? (basename target) "hosts")
|
| 331 | |
| 332 | |
| 333 | (copy-file source target))
|
| 334 | (else |
| 335 | (symlink source target)))))
|
| 336 | (scandir etc (negate dot-or-dot-dot?)
|
| 337 | |
| 338 | |
| 339 | |
| 340 | string<?)))
|
| 341 |
|
| 342 | (define %setuid-directory
|
| 343 | |
| 344 | |
| 345 | "/run/setuid-programs")
|
| 346 |
|
| 347 | (define %privileged-program-directory
|
| 348 | |
| 349 | "/run/privileged/bin")
|
| 350 |
|
| 351 | (define (activate-privileged-programs programs libcap)
|
| 352 | "Turn PROGRAMS, a list of file privileged-programs records, into privileged
|
| 353 | copies stored under %PRIVILEGED-PROGRAM-DIRECTORY, using LIBCAP's setcap(8)
|
| 354 | binary if needed."
|
| 355 | (define (ensure-empty-directory directory)
|
| 356 | (if (file-exists? directory)
|
| 357 | (for-each (compose delete-file
|
| 358 | (cut string-append directory "/" <>))
|
| 359 | (scandir directory
|
| 360 | (lambda (file)
|
| 361 | (not (member file '("." ".."))))
|
| 362 | string<?))
|
| 363 | (mkdir-p directory)) )
|
| 364 |
|
| 365 | (define (make-privileged-program program setuid? setgid? uid gid capabilities)
|
| 366 | (let ((target (string-append %privileged-program-directory
|
| 367 | "/" (basename program)))
|
| 368 | (mode (+ #o0555 |
| 369 | (if setuid? #o4000 0) |
| 370 | (if setgid? #o2000 0)))) |
| 371 | (copy-file program target)
|
| 372 | (chown target uid gid)
|
| 373 | (chmod target mode)
|
| 374 | (when (and capabilities libcap)
|
| 375 | (system* (string-append libcap "/sbin/setcap")
|
| 376 | "-q" capabilities target))))
|
| 377 |
|
| 378 | (define (make-deprecated-wrapper program)
|
| 379 | |
| 380 | (symlink (string-append %privileged-program-directory
|
| 381 | "/" (basename program))
|
| 382 | (string-append %setuid-directory
|
| 383 | "/" (basename program))))
|
| 384 |
|
| 385 | (format #t "setting up privileged programs in '~a'...~%"
|
| 386 | %privileged-program-directory)
|
| 387 | (ensure-empty-directory %privileged-program-directory)
|
| 388 | (ensure-empty-directory %setuid-directory)
|
| 389 |
|
| 390 | (for-each (lambda (program)
|
| 391 | (catch 'system-error
|
| 392 | (lambda ()
|
| 393 | (let* ((program-name (privileged-program-program program))
|
| 394 | (setuid? (privileged-program-setuid? program))
|
| 395 | (setgid? (privileged-program-setgid? program))
|
| 396 | (user (privileged-program-user program))
|
| 397 | (group (privileged-program-group program))
|
| 398 | (capabilities (privileged-program-capabilities program))
|
| 399 | (uid (match user
|
| 400 | ((? string?) (passwd:uid (getpwnam user)))
|
| 401 | ((? integer?) user)))
|
| 402 | (gid (match group
|
| 403 | ((? string?) (group:gid (getgrnam group)))
|
| 404 | ((? integer?) group))))
|
| 405 | (make-privileged-program program-name
|
| 406 | setuid? setgid? uid gid
|
| 407 | capabilities)
|
| 408 | (make-deprecated-wrapper program-name)))
|
| 409 | (lambda args
|
| 410 | |
| 411 | |
| 412 | |
| 413 | |
| 414 | (format (current-error-port)
|
| 415 | "warning: failed to privilege ~s: ~a~%"
|
| 416 | (privileged-program-program program)
|
| 417 | (strerror (system-error-errno args))))))
|
| 418 | programs))
|
| 419 |
|
| 420 | (define (activate-special-files special-files)
|
| 421 | "Install the files listed in SPECIAL-FILES. Each element of SPECIAL-FILES
|
| 422 | is a pair where the first element is the name of the special file and the
|
| 423 | second element is the name it should appear at, such as:
|
| 424 |
|
| 425 | ((\"/bin/sh\" \"/gnu/store/…-bash/bin/sh\")
|
| 426 | (\"/usr/bin/env\" \"/gnu/store/…-coreutils/bin/env\"))
|
| 427 | "
|
| 428 | (define install-special-file
|
| 429 | (match-lambda
|
| 430 | ((target file)
|
| 431 | (let ((pivot (string-append target ".new")))
|
| 432 | (mkdir-p (dirname target))
|
| 433 | (symlink file pivot)
|
| 434 | (rename-file pivot target)))))
|
| 435 |
|
| 436 | (for-each install-special-file special-files))
|
| 437 |
|
| 438 | (define (activate-modprobe modprobe)
|
| 439 | "Tell the kernel to use MODPROBE to load modules."
|
| 440 |
|
| 441 | |
| 442 | |
| 443 | (when (file-exists? "/proc/sys/kernel/modprobe")
|
| 444 | (call-with-output-file "/proc/sys/kernel/modprobe"
|
| 445 | (lambda (port)
|
| 446 | (display modprobe port)))))
|
| 447 |
|
| 448 | (define (activate-firmware directory)
|
| 449 | "Tell the kernel to look for device firmware under DIRECTORY. This
|
| 450 | mechanism bypasses udev: it allows Linux to handle firmware loading directly
|
| 451 | by itself, without having to resort to a \"user helper\"."
|
| 452 |
|
| 453 | |
| 454 | |
| 455 | (let ((firmware-path "/sys/module/firmware_class/parameters/path"))
|
| 456 | (when (file-exists? firmware-path)
|
| 457 | (call-with-output-file firmware-path
|
| 458 | (lambda (port)
|
| 459 | (display directory port))))))
|
| 460 |
|
| 461 | (define (activate-ptrace-attach)
|
| 462 | "Allow users to PTRACE_ATTACH their own processes.
|
| 463 |
|
| 464 | This works around a regression introduced in the default \"security\" policy
|
| 465 | found in Linux 3.4 onward that prevents users from attaching to their own
|
| 466 | processes--see Yama.txt in the Linux source tree for the rationale. This
|
| 467 | sounds like an unacceptable restriction for little or no security
|
| 468 | improvement."
|
| 469 | (let ((file "/proc/sys/kernel/yama/ptrace_scope"))
|
| 470 | (when (file-exists? file)
|
| 471 | (call-with-output-file file
|
| 472 | (lambda (port)
|
| 473 | (display 0 port))))))
|
| 474 |
|
| 475 |
|
| 476 | (define %current-system
|
| 477 | |
| 478 | |
| 479 | |
| 480 | "/run/current-system")
|
| 481 |
|
| 482 | (define (boot-time-system)
|
| 483 | "Return the 'gnu.system' argument passed on the kernel command line."
|
| 484 | (find-long-option "gnu.system" (if (string-contains %host-type "linux-gnu")
|
| 485 | (linux-command-line)
|
| 486 | (command-line))))
|
| 487 |
|
| 488 | (define* (activate-current-system
|
| 489 | #:optional (system (or (getenv "GUIX_NEW_SYSTEM")
|
| 490 | (boot-time-system))))
|
| 491 | "Atomically make SYSTEM the current system."
|
| 492 | |
| 493 | |
| 494 |
|
| 495 | (format #t "making '~a' the current system...~%" system)
|
| 496 |
|
| 497 | (mkdir-p "/run")
|
| 498 | |
| 499 | (let ((new (string-append %current-system ".new")))
|
| 500 | (when (file-exists? new)
|
| 501 | (delete-file new))
|
| 502 | (symlink system new)
|
| 503 | (rename-file new %current-system)))
|
| 504 |
|
| 505 | |