1-- 2-- SPDX-License-Identifier: BSD-2-Clause-FreeBSD 3-- 4-- Copyright (c) 2015 Pedro Souza <pedrosouza@freebsd.org> 5-- Copyright (C) 2018 Kyle Evans <kevans@FreeBSD.org> 6-- All rights reserved. 7-- 8-- Redistribution and use in source and binary forms, with or without 9-- modification, are permitted provided that the following conditions 10-- are met: 11-- 1. Redistributions of source code must retain the above copyright 12-- notice, this list of conditions and the following disclaimer. 13-- 2. Redistributions in binary form must reproduce the above copyright 14-- notice, this list of conditions and the following disclaimer in the 15-- documentation and/or other materials provided with the distribution. 16-- 17-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 18-- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20-- ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 21-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 23-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 24-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 25-- LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 26-- OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 27-- SUCH DAMAGE. 28-- 29-- $FreeBSD$ 30-- 31 32local hook = require("hook") 33 34local config = {} 35local modules = {} 36local carousel_choices = {} 37-- Which variables we changed 38local env_changed = {} 39-- Values to restore env to (nil to unset) 40local env_restore = {} 41 42local MSG_FAILEXEC = "Failed to exec '%s'" 43local MSG_FAILSETENV = "Failed to '%s' with value: %s" 44local MSG_FAILOPENCFG = "Failed to open config: '%s'" 45local MSG_FAILREADCFG = "Failed to read config: '%s'" 46local MSG_FAILPARSECFG = "Failed to parse config: '%s'" 47local MSG_FAILEXBEF = "Failed to execute '%s' before loading '%s'" 48local MSG_FAILEXMOD = "Failed to execute '%s'" 49local MSG_FAILEXAF = "Failed to execute '%s' after loading '%s'" 50local MSG_MALFORMED = "Malformed line (%d):\n\t'%s'" 51local MSG_DEFAULTKERNFAIL = "No kernel set, failed to load from module_path" 52local MSG_KERNFAIL = "Failed to load kernel '%s'" 53local MSG_XENKERNFAIL = "Failed to load Xen kernel '%s'" 54local MSG_XENKERNLOADING = "Loading Xen kernel..." 55local MSG_KERNLOADING = "Loading kernel..." 56local MSG_MODLOADING = "Loading configured modules..." 57local MSG_MODBLACKLIST = "Not loading blacklisted module '%s'" 58 59local MODULEEXPR = '([%w-_]+)' 60local QVALEXPR = "\"([%w%s%p]-)\"" 61local QVALREPL = QVALEXPR:gsub('%%', '%%%%') 62local WORDEXPR = "([%w]+)" 63local WORDREPL = WORDEXPR:gsub('%%', '%%%%') 64 65local function restoreEnv() 66 -- Examine changed environment variables 67 for k, v in pairs(env_changed) do 68 local restore_value = env_restore[k] 69 if restore_value == nil then 70 -- This one doesn't need restored for some reason 71 goto continue 72 end 73 local current_value = loader.getenv(k) 74 if current_value ~= v then 75 -- This was overwritten by some action taken on the menu 76 -- most likely; we'll leave it be. 77 goto continue 78 end 79 restore_value = restore_value.value 80 if restore_value ~= nil then 81 loader.setenv(k, restore_value) 82 else 83 loader.unsetenv(k) 84 end 85 ::continue:: 86 end 87 88 env_changed = {} 89 env_restore = {} 90end 91 92local function setEnv(key, value) 93 -- Track the original value for this if we haven't already 94 if env_restore[key] == nil then 95 env_restore[key] = {value = loader.getenv(key)} 96 end 97 98 env_changed[key] = value 99 100 return loader.setenv(key, value) 101end 102 103-- name here is one of 'name', 'type', flags', 'before', 'after', or 'error.' 104-- These are set from lines in loader.conf(5): ${key}_${name}="${value}" where 105-- ${key} is a module name. 106local function setKey(key, name, value) 107 if modules[key] == nil then 108 modules[key] = {} 109 end 110 modules[key][name] = value 111end 112 113-- Escapes the named value for use as a literal in a replacement pattern. 114-- e.g. dhcp.host-name gets turned into dhcp%.host%-name to remove the special 115-- meaning. 116local function escapeName(name) 117 return name:gsub("([%p])", "%%%1") 118end 119 120local function processEnvVar(value) 121 for name in value:gmatch("${([^}]+)}") do 122 local replacement = loader.getenv(name) or "" 123 value = value:gsub("${" .. escapeName(name) .. "}", replacement) 124 end 125 for name in value:gmatch("$([%w%p]+)%s*") do 126 local replacement = loader.getenv(name) or "" 127 value = value:gsub("$" .. escapeName(name), replacement) 128 end 129 return value 130end 131 132local function checkPattern(line, pattern) 133 local function _realCheck(_line, _pattern) 134 return _line:match(_pattern) 135 end 136 137 if pattern:find('$VALUE') then 138 local k, v, c 139 k, v, c = _realCheck(line, pattern:gsub('$VALUE', QVALREPL)) 140 if k ~= nil then 141 return k,v, c 142 end 143 return _realCheck(line, pattern:gsub('$VALUE', WORDREPL)) 144 else 145 return _realCheck(line, pattern) 146 end 147end 148 149-- str in this table is a regex pattern. It will automatically be anchored to 150-- the beginning of a line and any preceding whitespace will be skipped. The 151-- pattern should have no more than two captures patterns, which correspond to 152-- the two parameters (usually 'key' and 'value') that are passed to the 153-- process function. All trailing characters will be validated. Any $VALUE 154-- token included in a pattern will be tried first with a quoted value capture 155-- group, then a single-word value capture group. This is our kludge for Lua 156-- regex not supporting branching. 157-- 158-- We have two special entries in this table: the first is the first entry, 159-- a full-line comment. The second is for 'exec' handling. Both have a single 160-- capture group, but the difference is that the full-line comment pattern will 161-- match the entire line. This does not run afoul of the later end of line 162-- validation that we'll do after a match. However, the 'exec' pattern will. 163-- We document the exceptions with a special 'groups' index that indicates 164-- the number of capture groups, if not two. We'll use this later to do 165-- validation on the proper entry. 166-- 167local pattern_table = { 168 { 169 str = "(#.*)", 170 process = function(_, _) end, 171 groups = 1, 172 }, 173 -- module_load="value" 174 { 175 str = MODULEEXPR .. "_load%s*=%s*$VALUE", 176 process = function(k, v) 177 if modules[k] == nil then 178 modules[k] = {} 179 end 180 modules[k].load = v:upper() 181 end, 182 }, 183 -- module_name="value" 184 { 185 str = MODULEEXPR .. "_name%s*=%s*$VALUE", 186 process = function(k, v) 187 setKey(k, "name", v) 188 end, 189 }, 190 -- module_type="value" 191 { 192 str = MODULEEXPR .. "_type%s*=%s*$VALUE", 193 process = function(k, v) 194 setKey(k, "type", v) 195 end, 196 }, 197 -- module_flags="value" 198 { 199 str = MODULEEXPR .. "_flags%s*=%s*$VALUE", 200 process = function(k, v) 201 setKey(k, "flags", v) 202 end, 203 }, 204 -- module_before="value" 205 { 206 str = MODULEEXPR .. "_before%s*=%s*$VALUE", 207 process = function(k, v) 208 setKey(k, "before", v) 209 end, 210 }, 211 -- module_after="value" 212 { 213 str = MODULEEXPR .. "_after%s*=%s*$VALUE", 214 process = function(k, v) 215 setKey(k, "after", v) 216 end, 217 }, 218 -- module_error="value" 219 { 220 str = MODULEEXPR .. "_error%s*=%s*$VALUE", 221 process = function(k, v) 222 setKey(k, "error", v) 223 end, 224 }, 225 -- exec="command" 226 { 227 str = "exec%s*=%s*" .. QVALEXPR, 228 process = function(k, _) 229 if cli_execute_unparsed(k) ~= 0 then 230 print(MSG_FAILEXEC:format(k)) 231 end 232 end, 233 groups = 1, 234 }, 235 -- env_var="value" 236 { 237 str = "([%w%p]+)%s*=%s*$VALUE", 238 process = function(k, v) 239 if setEnv(k, processEnvVar(v)) ~= 0 then 240 print(MSG_FAILSETENV:format(k, v)) 241 end 242 end, 243 }, 244 -- env_var=num 245 { 246 str = "([%w%p]+)%s*=%s*(-?%d+)", 247 process = function(k, v) 248 if setEnv(k, processEnvVar(v)) ~= 0 then 249 print(MSG_FAILSETENV:format(k, tostring(v))) 250 end 251 end, 252 }, 253} 254 255local function isValidComment(line) 256 if line ~= nil then 257 local s = line:match("^%s*#.*") 258 if s == nil then 259 s = line:match("^%s*$") 260 end 261 if s == nil then 262 return false 263 end 264 end 265 return true 266end 267 268local function getBlacklist() 269 local blacklist = {} 270 local blacklist_str = loader.getenv('module_blacklist') 271 if blacklist_str == nil then 272 return blacklist 273 end 274 275 for mod in blacklist_str:gmatch("[;, ]?([%w-_]+)[;, ]?") do 276 blacklist[mod] = true 277 end 278 return blacklist 279end 280 281local function loadModule(mod, silent) 282 local status = true 283 local blacklist = getBlacklist() 284 local pstatus 285 for k, v in pairs(mod) do 286 if v.load ~= nil and v.load:lower() == "yes" then 287 local module_name = v.name or k 288 if blacklist[module_name] ~= nil then 289 if not silent then 290 print(MSG_MODBLACKLIST:format(module_name)) 291 end 292 goto continue 293 end 294 if not silent then 295 loader.printc(module_name .. "...") 296 end 297 local str = "load " 298 if v.type ~= nil then 299 str = str .. "-t " .. v.type .. " " 300 end 301 str = str .. module_name 302 if v.flags ~= nil then 303 str = str .. " " .. v.flags 304 end 305 if v.before ~= nil then 306 pstatus = cli_execute_unparsed(v.before) == 0 307 if not pstatus and not silent then 308 print(MSG_FAILEXBEF:format(v.before, k)) 309 end 310 status = status and pstatus 311 end 312 313 if cli_execute_unparsed(str) ~= 0 then 314 -- XXX Temporary shim: don't break the boot if 315 -- loader hadn't been recompiled with this 316 -- function exposed. 317 if loader.command_error then 318 print(loader.command_error()) 319 end 320 if not silent then 321 print("failed!") 322 end 323 if v.error ~= nil then 324 cli_execute_unparsed(v.error) 325 end 326 status = false 327 elseif v.after ~= nil then 328 pstatus = cli_execute_unparsed(v.after) == 0 329 if not pstatus and not silent then 330 print(MSG_FAILEXAF:format(v.after, k)) 331 end 332 if not silent then 333 print("ok") 334 end 335 status = status and pstatus 336 end 337 end 338 ::continue:: 339 end 340 341 return status 342end 343 344local function readConfFiles(loaded_files) 345 local f = loader.getenv("loader_conf_files") 346 if f ~= nil then 347 for name in f:gmatch("([%w%p]+)%s*") do 348 if loaded_files[name] ~= nil then 349 goto continue 350 end 351 352 local prefiles = loader.getenv("loader_conf_files") 353 354 print("Loading " .. name) 355 -- These may or may not exist, and that's ok. Do a 356 -- silent parse so that we complain on parse errors but 357 -- not for them simply not existing. 358 if not config.processFile(name, true) then 359 print(MSG_FAILPARSECFG:format(name)) 360 end 361 362 loaded_files[name] = true 363 local newfiles = loader.getenv("loader_conf_files") 364 if prefiles ~= newfiles then 365 readConfFiles(loaded_files) 366 end 367 ::continue:: 368 end 369 end 370end 371 372local function readFile(name, silent) 373 local f = io.open(name) 374 if f == nil then 375 if not silent then 376 print(MSG_FAILOPENCFG:format(name)) 377 end 378 return nil 379 end 380 381 local text, _ = io.read(f) 382 -- We might have read in the whole file, this won't be needed any more. 383 io.close(f) 384 385 if text == nil and not silent then 386 print(MSG_FAILREADCFG:format(name)) 387 end 388 return text 389end 390 391local function checkNextboot() 392 local nextboot_file = loader.getenv("nextboot_conf") 393 if nextboot_file == nil then 394 return 395 end 396 397 local text = readFile(nextboot_file, true) 398 if text == nil then 399 return 400 end 401 402 if text:match("^nextboot_enable=\"NO\"") ~= nil then 403 -- We're done; nextboot is not enabled 404 return 405 end 406 407 if not config.parse(text) then 408 print(MSG_FAILPARSECFG:format(nextboot_file)) 409 end 410 411 -- Attempt to rewrite the first line and only the first line of the 412 -- nextboot_file. We overwrite it with nextboot_enable="NO", then 413 -- check for that on load. 414 -- It's worth noting that this won't work on every filesystem, so we 415 -- won't do anything notable if we have any errors in this process. 416 local nfile = io.open(nextboot_file, 'w') 417 if nfile ~= nil then 418 -- We need the trailing space here to account for the extra 419 -- character taken up by the string nextboot_enable="YES" 420 -- Or new end quotation mark lands on the S, and we want to 421 -- rewrite the entirety of the first line. 422 io.write(nfile, "nextboot_enable=\"NO\" ") 423 io.close(nfile) 424 end 425end 426 427-- Module exports 428config.verbose = false 429 430-- The first item in every carousel is always the default item. 431function config.getCarouselIndex(id) 432 return carousel_choices[id] or 1 433end 434 435function config.setCarouselIndex(id, idx) 436 carousel_choices[id] = idx 437end 438 439-- Returns true if we processed the file successfully, false if we did not. 440-- If 'silent' is true, being unable to read the file is not considered a 441-- failure. 442function config.processFile(name, silent) 443 if silent == nil then 444 silent = false 445 end 446 447 local text = readFile(name, silent) 448 if text == nil then 449 return silent 450 end 451 452 return config.parse(text) 453end 454 455-- silent runs will not return false if we fail to open the file 456function config.parse(text) 457 local n = 1 458 local status = true 459 460 for line in text:gmatch("([^\n]+)") do 461 if line:match("^%s*$") == nil then 462 for _, val in ipairs(pattern_table) do 463 local pattern = '^%s*' .. val.str .. '%s*(.*)'; 464 local cgroups = val.groups or 2 465 local k, v, c = checkPattern(line, pattern) 466 if k ~= nil then 467 -- Offset by one, drats 468 if cgroups == 1 then 469 c = v 470 v = nil 471 end 472 473 if isValidComment(c) then 474 val.process(k, v) 475 goto nextline 476 end 477 478 break 479 end 480 end 481 482 print(MSG_MALFORMED:format(n, line)) 483 status = false 484 end 485 ::nextline:: 486 n = n + 1 487 end 488 489 return status 490end 491 492-- other_kernel is optionally the name of a kernel to load, if not the default 493-- or autoloaded default from the module_path 494function config.loadKernel(other_kernel) 495 local flags = loader.getenv("kernel_options") or "" 496 local kernel = other_kernel or loader.getenv("kernel") 497 498 local function tryLoad(names) 499 for name in names:gmatch("([^;]+)%s*;?") do 500 local r = loader.perform("load " .. name .. 501 " " .. flags) 502 if r == 0 then 503 return name 504 end 505 end 506 return nil 507 end 508 509 local function getModulePath() 510 local module_path = loader.getenv("module_path") 511 local kernel_path = loader.getenv("kernel_path") 512 513 if kernel_path == nil then 514 return module_path 515 end 516 517 -- Strip the loaded kernel path from module_path. This currently assumes 518 -- that the kernel path will be prepended to the module_path when it's 519 -- found. 520 kernel_path = escapeName(kernel_path .. ';') 521 return module_path:gsub(kernel_path, '') 522 end 523 524 local function loadBootfile() 525 local bootfile = loader.getenv("bootfile") 526 527 -- append default kernel name 528 if bootfile == nil then 529 bootfile = "kernel" 530 else 531 bootfile = bootfile .. ";kernel" 532 end 533 534 return tryLoad(bootfile) 535 end 536 537 -- kernel not set, try load from default module_path 538 if kernel == nil then 539 local res = loadBootfile() 540 541 if res ~= nil then 542 -- Default kernel is loaded 543 config.kernel_loaded = nil 544 return true 545 else 546 print(MSG_DEFAULTKERNFAIL) 547 return false 548 end 549 else 550 -- Use our cached module_path, so we don't end up with multiple 551 -- automatically added kernel paths to our final module_path 552 local module_path = getModulePath() 553 local res 554 555 if other_kernel ~= nil then 556 kernel = other_kernel 557 end 558 -- first try load kernel with module_path = /boot/${kernel} 559 -- then try load with module_path=${kernel} 560 local paths = {"/boot/" .. kernel, kernel} 561 562 for _, v in pairs(paths) do 563 loader.setenv("module_path", v) 564 res = loadBootfile() 565 566 -- succeeded, add path to module_path 567 if res ~= nil then 568 config.kernel_loaded = kernel 569 if module_path ~= nil then 570 loader.setenv("module_path", v .. ";" .. 571 module_path) 572 loader.setenv("kernel_path", v) 573 end 574 return true 575 end 576 end 577 578 -- failed to load with ${kernel} as a directory 579 -- try as a file 580 res = tryLoad(kernel) 581 if res ~= nil then 582 config.kernel_loaded = kernel 583 return true 584 else 585 print(MSG_KERNFAIL:format(kernel)) 586 return false 587 end 588 end 589end 590 591function config.selectKernel(kernel) 592 config.kernel_selected = kernel 593end 594 595function config.load(file, reloading) 596 if not file then 597 file = "/boot/defaults/loader.conf" 598 end 599 600 if not config.processFile(file) then 601 print(MSG_FAILPARSECFG:format(file)) 602 end 603 604 local loaded_files = {file = true} 605 readConfFiles(loaded_files) 606 607 checkNextboot() 608 609 local verbose = loader.getenv("verbose_loading") or "no" 610 config.verbose = verbose:lower() == "yes" 611 if not reloading then 612 hook.runAll("config.loaded") 613 end 614end 615 616-- Reload configuration 617function config.reload(file) 618 modules = {} 619 restoreEnv() 620 config.load(file, true) 621 hook.runAll("config.reloaded") 622end 623 624function config.loadelf() 625 local xen_kernel = loader.getenv('xen_kernel') 626 local kernel = config.kernel_selected or config.kernel_loaded 627 local loaded 628 629 if xen_kernel ~= nil then 630 print(MSG_XENKERNLOADING) 631 if cli_execute_unparsed('load ' .. xen_kernel) ~= 0 then 632 print(MSG_XENKERNFAIL:format(xen_kernel)) 633 return false 634 end 635 end 636 print(MSG_KERNLOADING) 637 loaded = config.loadKernel(kernel) 638 639 if not loaded then 640 return false 641 end 642 643 print(MSG_MODLOADING) 644 return loadModule(modules, not config.verbose) 645end 646 647hook.registerType("config.loaded") 648hook.registerType("config.reloaded") 649return config 650