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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540 1541 1542 1543 1544 1545 1546 1547 1548 1549 1550 1551 1552 1553 1554 1555 1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566 1567 1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600 1601 1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615 1616 1617 1618 1619 1620 1621 1622 1623 1624 1625 1626 1627 1628 1629 1630 1631 1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653 1654 1655 1656 1657 1658 1659 1660 1661 1662 1663 1664 1665 1666 1667 1668 1669 1670 1671 1672 1673 1674 1675 1676 1677 1678 1679 1680 1681 1682 1683 1684 1685 1686 1687 1688 1689 1690 1691 1692 1693 1694 1695 1696 1697 1698 1699 1700 1701 1702 1703 1704 1705 1706 1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742 1743 1744 1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764 1765 1766 1767 1768 1769 1770 1771 1772 1773 1774 1775 1776 1777 1778 1779 1780 1781 1782 1783 1784 1785 1786 1787 1788 1789 1790 1791 1792 1793 1794 1795 1796 1797 1798 1799 1800 1801 1802 1803 1804 1805 1806 1807 1808 1809 1810 1811 1812 1813 1814 1815 1816 1817 1818 1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832 1833 1834 1835 1836 1837 1838 1839 1840 1841 1842 1843 1844 1845 1846 1847 1848 1849 1850 1851 1852 1853 1854 1855 1856 1857 1858 1859 1860 1861 1862 1863 1864 1865 1866 1867 1868 1869 1870 1871 1872 1873 1874 1875 1876 1877 1878 1879 1880 1881 1882 1883 1884 1885 1886 1887 1888 1889 1890 1891 1892 1893 1894 1895 1896 1897 1898 1899 1900 1901 1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916 1917 1918 1919 1920 1921 1922 1923 1924 1925 1926 1927 1928 1929 1930 1931 1932 1933 1934 1935 1936 1937 1938 1939 1940 1941 1942 1943 1944 1945 1946 1947 1948 1949 1950 1951 1952 1953 1954 1955 1956 1957 1958 1959 1960 1961 1962 1963 1964 1965 1966 1967 1968 1969 1970 1971 1972 1973 1974 1975 1976 1977 1978 1979 1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072 2073 2074 2075 2076 2077 2078 2079 2080 2081 2082 2083 2084 2085 2086 2087 2088 2089 2090 2091 2092 2093 2094 2095 2096 2097 2098 2099 2100 2101 2102 2103 2104 2105 2106 2107 2108 2109 2110 2111 2112 2113 2114 2115 2116 2117 2118 2119 2120 2121 2122 2123 2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134 2135 2136 2137 2138 2139 2140 2141 2142 2143 2144 2145 2146 2147 2148 2149 2150 2151 2152 2153 2154 2155 2156 2157 2158 2159 2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177 2178 2179 2180 2181 2182 2183 2184 2185 2186 2187 2188 2189 2190 2191 2192 2193 2194 2195 2196 2197 2198 2199 2200 2201 2202 2203 2204 2205 2206 2207 2208 2209 2210 2211 2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224 2225 2226 2227 2228 2229 2230 2231 2232 2233 2234 2235 2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283 2284 2285 2286 2287 2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307 2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380 2381 2382 2383 2384 2385 2386 2387 2388 2389 2390 2391 2392 2393 2394 2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431 2432 2433 2434 2435 2436 2437 2438 2439 2440 2441 2442 2443 2444 2445 2446 2447 2448 2449 2450 2451 2452 2453 2454 2455 2456 2457 2458 2459 2460 2461 2462 2463 2464 2465 2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477 2478 2479 2480 2481 2482 2483 2484 2485 2486 2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502 2503 2504 2505 2506 2507 2508 2509 2510 2511 2512 2513 2514 2515 2516 2517 2518 2519 2520 2521 2522 2523 2524 2525 2526 2527 2528 2529 2530 2531 2532 2533 2534 2535 2536 2537 2538 2539 2540 2541 2542 2543 2544 2545 2546 2547 2548 2549 2550 2551 2552 2553 2554 2555 2556 2557 2558 2559 2560 2561 2562 2563 2564 2565 2566 2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578 2579 2580 2581 2582 2583 2584 2585 2586 2587 2588 2589 2590 2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601 2602 2603 2604 2605 2606 2607 2608 2609 2610 2611 2612 2613 2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625 2626 2627 2628 2629 2630 2631 2632 2633 2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650 2651 2652 2653 2654 2655 2656 2657 2658 2659 2660 2661 2662 2663 2664 2665 2666 2667 2668 2669 2670 2671 2672 2673 2674 2675 2676 2677 2678 2679 2680 2681 2682 2683 2684 2685 2686 2687 2688 2689 2690 2691 2692 2693 2694 2695 2696 2697 2698 2699 2700 2701 2702 2703 2704 2705 2706 2707 2708 2709 2710 2711 2712 2713 2714 2715 2716 2717 2718 2719
|
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
* Copyright (C) 2019-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package main
import (
"bytes"
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
gadgetInstall "github.com/snapcore/snapd/gadget/install"
"github.com/snapcore/snapd/kernel"
"github.com/snapcore/snapd/kernel/fde"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/snapdtool"
"github.com/snapcore/snapd/systemd"
// to set sysconfig.ApplyFilesystemOnlyDefaultsImpl
_ "github.com/snapcore/snapd/overlord/configstate/configcore"
// to set [boot.SealKeyForBootChains]
_ "github.com/snapcore/snapd/overlord/fdestate/backend"
"github.com/snapcore/snapd/overlord/install"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/snapfile"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/sysconfig"
"github.com/snapcore/snapd/timings"
)
func init() {
const (
short = "Generate mounts for the initramfs"
long = "Generate and perform all mounts for the initramfs before transitioning to userspace"
)
addCommandBuilder(func(parser *flags.Parser) {
if _, err := parser.AddCommand("initramfs-mounts", short, long, &cmdInitramfsMounts{}); err != nil {
panic(err)
}
})
snap.SanitizePlugsSlots = func(*snap.Info) {}
}
type cmdInitramfsMounts struct{}
func (c *cmdInitramfsMounts) Execute([]string) error {
boot.HookKeyProtectorFactory = hookKeyProtectorFactory
logger.Noticef("snap-bootstrap version %v starting", snapdtool.Version)
return generateInitramfsMounts()
}
var (
osutilIsMounted = osutil.IsMounted
osGetenv = os.Getenv
snapTypeToMountDir = map[snap.Type]string{
snap.TypeBase: "base",
snap.TypeGadget: "gadget",
snap.TypeKernel: "kernel",
snap.TypeSnapd: "snapd",
}
secbootMeasureSnapSystemEpochWhenPossible func() error
secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error
secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)
secbootUnlockEncryptedVolumeUsingProtectorKey func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error)
secbootLockSealedKeys func() error
bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk
mountReadOnlyOptions = &systemdMountOptions{
ReadOnly: true,
Private: true,
}
gadgetInstallRun = gadgetInstall.Run
bootMakeRunnableStandaloneSystem = boot.MakeRunnableStandaloneSystemFromInitrd
installApplyPreseededData = install.ApplyPreseededData
bootEnsureNextBootToRunMode = boot.EnsureNextBootToRunMode
installBuildInstallObserver = install.BuildInstallObserver
)
func stampedAction(stamp string, action func() error) error {
stampFile := filepath.Join(dirs.SnapBootstrapRunDir, stamp)
if osutil.FileExists(stampFile) {
return nil
}
if err := os.MkdirAll(filepath.Dir(stampFile), 0755); err != nil {
return err
}
if err := action(); err != nil {
return err
}
return os.WriteFile(stampFile, nil, 0644)
}
func generateInitramfsMounts() (err error) {
// ensure that the last thing we do is to lock access to sealed keys,
// regardless of mode or early failures.
defer func() {
if e := secbootLockSealedKeys(); e != nil {
e = fmt.Errorf("error locking access to sealed keys: %v", e)
if err == nil {
err = e
} else {
// preserve err but log
logger.Noticef("%v", e)
}
}
}()
// Ensure there is a very early initial measurement
err = stampedAction("secboot-epoch-measured", func() error {
return secbootMeasureSnapSystemEpochWhenPossible()
})
if err != nil {
return err
}
mode, recoverySystem, err := boot.ModeAndRecoverySystemFromKernelCommandLine()
if err != nil {
return err
}
mst := &initramfsMountsState{
mode: mode,
recoverySystem: recoverySystem,
}
// generate mounts and set mst.validatedModel
switch mode {
case "recover":
err = generateMountsModeRecover(mst)
case "install":
err = generateMountsModeInstall(mst)
case "factory-reset":
err = generateMountsModeFactoryReset(mst)
case "run":
err = generateMountsModeRun(mst)
case "cloudimg-rootfs":
err = generateMountsModeRunCVM(mst)
default:
// this should never be reached, ModeAndRecoverySystemFromKernelCommandLine
// will have returned a non-nill error above if there was another mode
// specified on the kernel command line for some reason
return fmt.Errorf("internal error: mode in generateInitramfsMounts not handled")
}
if err != nil {
return err
}
model := mst.verifiedModel
if model == nil {
return fmt.Errorf("internal error: no verified model set")
}
isRunMode := (mode == "run")
rootfsDir := boot.InitramfsWritableDir(model, isRunMode)
// finally, the initramfs is responsible for reading the boot flags and
// copying them to /run, so that userspace has an unambiguous place to read
// the boot flags for the current boot from
flags, err := boot.InitramfsActiveBootFlags(mode, rootfsDir)
if err != nil {
// We don't die on failing to read boot flags, we just log the error and
// don't set any flags, this is because the boot flags in the case of
// install comes from untrusted input, the bootenv. In the case of run
// mode, boot flags are read from the modeenv, which should be valid and
// trusted, but if the modeenv becomes corrupted, we would block
// accessing the system (except through an initramfs shell), to recover
// the modeenv (though maybe we could enable some sort of fixing from
// recover mode instead?)
logger.Noticef("error accessing boot flags: %v", err)
} else {
// write the boot flags
if err := boot.InitramfsExposeBootFlagsForSystem(flags); err != nil {
// cannot write to /run, error here since arguably we have major
// problems if we can't write to /run
return err
}
}
return nil
}
func canInstallAndRunAtOnce(mst *initramfsMountsState) (bool, error) {
currentSeed, err := mst.LoadSeed(mst.recoverySystem)
if err != nil {
return false, err
}
preseedSeed, ok := currentSeed.(seed.PreseedCapable)
if !ok {
return false, nil
}
// TODO: relax this condition when "install and run" well tested
if !preseedSeed.HasArtifact("preseed.tgz") {
return false, nil
}
// If kernel has fde-setup hook, then we should also have fde-setup in initramfs
kernelPath := filepath.Join(boot.InitramfsRunMntDir, "kernel")
kernelHasFdeSetup := osutil.FileExists(filepath.Join(kernelPath, "meta", "hooks", "fde-setup"))
_, fdeSetupErr := exec.LookPath("fde-setup")
if kernelHasFdeSetup && fdeSetupErr != nil {
return false, nil
}
gadgetPath := filepath.Join(boot.InitramfsRunMntDir, "gadget")
if osutil.FileExists(filepath.Join(gadgetPath, "meta", "hooks", "install-device")) {
return false, nil
}
return true, nil
}
func readSnapInfo(sysSnaps map[snap.Type]*seed.Snap, snapType snap.Type) (*snap.Info, error) {
seedSnap := sysSnaps[snapType]
mountPoint := filepath.Join(boot.InitramfsRunMntDir, snapTypeToMountDir[snapType])
info, err := snap.ReadInfoFromMountPoint(seedSnap.SnapName(), mountPoint, seedSnap.Path, seedSnap.SideInfo)
if err != nil {
return nil, err
}
// Comes from the seed and it might be unasserted, set revision in that case
if info.Revision.Unset() {
info.Revision = snap.R(-1)
}
return info, nil
}
func readComponentInfo(seedComp *seed.Component, mntPt string, snapInfo *snap.Info, csi *snap.ComponentSideInfo) (*snap.ComponentInfo, error) {
container := snapdir.New(mntPt)
ci, err := snap.ReadComponentInfoFromContainer(container, snapInfo, csi)
if err != nil {
return nil, err
}
// Comes from the seed and it might be unasserted, set revision in that case
if ci.Revision.Unset() {
ci.Revision = snap.R(-1)
}
return ci, nil
}
func runFDESetupHook(req *fde.SetupRequest) ([]byte, error) {
// TODO: use systemd-run
encoded, err := json.Marshal(req)
if err != nil {
return nil, err
}
cmd := exec.Command("fde-setup")
cmd.Stdin = bytes.NewBuffer(encoded)
output, err := cmd.Output()
if err != nil {
return nil, err
}
return output, nil
}
func hookKeyProtectorFactory(kernelInfo *snap.Info) (secboot.KeyProtectorFactory, error) {
if _, ok := kernelInfo.Hooks["fde-setup"]; ok {
return secboot.FDESetupHookKeyProtectorFactory(runFDESetupHook), nil
}
if secboot.FDEOpteeTAPresent() {
return secboot.OPTEEKeyProtectorFactory(), nil
}
return nil, secboot.ErrNoKeyProtector
}
func readSnapInfoFromSeed(seedSnap *seed.Snap) (*snap.Info, error) {
snapf, err := snapfile.Open(seedSnap.Path)
if err != nil {
return nil, err
}
info, err := snap.ReadInfoFromSnapFile(snapf, seedSnap.SideInfo)
if err != nil {
return nil, err
}
// Comes from the seed and it might be unasserted, set revision in that case
if info.Revision.Unset() {
info.Revision = snap.R(-1)
}
return info, nil
}
func setUbuntuCoreDataMountOptions(mountOpts systemdMountOptions) systemdMountOptions {
// fsck and mount with nosuid to prevent snaps from being able to bypass
// the sandbox by creating suid root files there and trying to escape the
// sandbox
mountOpts.NoSuid = true
// Note that on classic the default is to allow mount propagation
mountOpts.Private = true
return mountOpts
}
func doInstall(mst *initramfsMountsState, model *asserts.Model, sysSnaps map[snap.Type]*seed.Snap) error {
kernelSnap, err := readSnapInfo(sysSnaps, snap.TypeKernel)
if err != nil {
return err
}
var baseSnap *snap.Info
if createSysrootMount() {
// On UC24+ the base is not mounted yet, peek into the file
baseSnap, err = readSnapInfoFromSeed(sysSnaps[snap.TypeBase])
} else {
baseSnap, err = readSnapInfo(sysSnaps, snap.TypeBase)
}
if err != nil {
return err
}
gadgetSnap, err := readSnapInfo(sysSnaps, snap.TypeGadget)
if err != nil {
return err
}
kernelMountDir := filepath.Join(boot.InitramfsRunMntDir, snapTypeToMountDir[snap.TypeKernel])
gadgetMountDir := filepath.Join(boot.InitramfsRunMntDir, snapTypeToMountDir[snap.TypeGadget])
gadgetInfo, err := gadget.ReadInfo(gadgetMountDir, model)
if err != nil {
return err
}
encryptionSupport, err := install.CheckEncryptionSupport(install.EncryptionConstraints{
Model: model,
Kernel: kernelSnap,
Gadget: gadgetInfo,
TPMMode: secboot.TPMProvisionFull,
}, runFDESetupHook)
if err != nil {
return err
}
useEncryption := (encryptionSupport != device.EncryptionTypeNone)
installObserver, trustedInstallObserver, err := installBuildInstallObserver(model, gadgetMountDir, useEncryption)
if err != nil {
return err
}
options := gadgetInstall.Options{
Mount: true,
EncryptionType: encryptionSupport,
}
validationConstraints := gadget.ValidationConstraints{
EncryptedData: useEncryption,
}
gadgetInfo, err = gadget.ReadInfoAndValidate(gadgetMountDir, model, &validationConstraints)
if err != nil {
return fmt.Errorf("cannot use gadget: %v", err)
}
if err := gadget.ValidateContent(gadgetInfo, gadgetMountDir, kernelMountDir); err != nil {
return fmt.Errorf("cannot use gadget: %v", err)
}
// Get kernel-modules information to have them ready early on first boot
kernCompsByName := make(map[string]*snap.Component)
for _, c := range kernelSnap.Components {
kernCompsByName[c.Name] = c
}
kernelSeed := sysSnaps[snap.TypeKernel]
kernCompsMntPts := make(map[string]string)
compSeedInfos := []install.ComponentSeedInfo{}
compsDir := filepath.Join(boot.InitramfsRunMntDir, "snap-content")
defer func() {
// Remove dirs used by ancillary mounts
if err := os.RemoveAll(compsDir); err != nil {
logger.Noticef("warning: cannot remove %s: %v", compsDir, err)
}
}()
for _, sc := range kernelSeed.Components {
seedComp := sc
comp, ok := kernCompsByName[seedComp.CompSideInfo.Component.ComponentName]
if !ok {
return fmt.Errorf("component %s in seed but not defined by snap!",
seedComp.CompSideInfo.Component.ComponentName)
}
if comp.Type != snap.KernelModulesComponent {
continue
}
// Mount ephemerally the kernel-modules components to read
// their metadata and also to make them accessible if building
// the drivers tree.
mntPt := filepath.Join(filepath.Join(compsDir, seedComp.CompSideInfo.Component.String()))
if err := doSystemdMount(seedComp.Path, mntPt, &systemdMountOptions{
ReadOnly: true,
Private: true,
Ephemeral: true}); err != nil {
return err
}
kernCompsMntPts[seedComp.CompSideInfo.Component.String()] = mntPt
defer func() {
stdout, stderr, err := osutil.RunSplitOutput("systemd-mount", "--umount", mntPt)
if err != nil {
logger.Noticef("cannot unmount component in %s: %v",
mntPt, osutil.OutputErrCombine(stdout, stderr, err))
}
}()
compInfo, err := readComponentInfo(&seedComp, mntPt, kernelSnap, &seedComp.CompSideInfo)
if err != nil {
return err
}
compSeedInfos = append(compSeedInfos, install.ComponentSeedInfo{
Info: compInfo,
Seed: &seedComp,
})
}
currentSeed, err := mst.LoadSeed(mst.recoverySystem)
if err != nil {
return err
}
preseedSeed, ok := currentSeed.(seed.PreseedCapable)
preseed := false
if ok && preseedSeed.HasArtifact("preseed.tgz") {
preseed = true
}
// Drivers tree will already be built if using the preseed tarball
needsKernelSetup := kernel.NeedsKernelDriversTree(model) && !preseed
isCore := !model.Classic()
kernelBootInfo := install.BuildKernelBootInfo(
kernelSnap, compSeedInfos, kernelMountDir, kernCompsMntPts,
install.BuildKernelBootInfoOpts{IsCore: isCore, NeedsDriversTree: needsKernelSetup})
bootDevice := ""
installedSystem, err := gadgetInstallRun(model, gadgetMountDir, kernelBootInfo.KSnapInfo, bootDevice, options, installObserver, timings.New(nil))
if err != nil {
return err
}
if trustedInstallObserver != nil {
// We are required to call ObserveExistingTrustedRecoveryAssets on trusted observers
if err := trustedInstallObserver.ObserveExistingTrustedRecoveryAssets(boot.InitramfsUbuntuSeedDir); err != nil {
return fmt.Errorf("cannot observe existing trusted recovery assets: %v", err)
}
}
if useEncryption {
if err := install.PrepareEncryptedSystemData(model, installedSystem.BootstrappedContainerForRole, nil, nil, trustedInstallObserver); err != nil {
return err
}
}
err = install.PrepareRunSystemData(model, gadgetMountDir, timings.New(nil))
if err != nil {
return err
}
bootWith := &boot.BootableSet{
Base: baseSnap,
BasePath: sysSnaps[snap.TypeBase].Path,
Gadget: gadgetSnap,
GadgetPath: sysSnaps[snap.TypeGadget].Path,
Kernel: kernelSnap,
KernelPath: sysSnaps[snap.TypeKernel].Path,
UnpackedGadgetDir: gadgetMountDir,
RecoverySystemLabel: mst.recoverySystem,
KernelMods: kernelBootInfo.BootableKMods,
}
if err := bootMakeRunnableStandaloneSystem(model, bootWith, trustedInstallObserver); err != nil {
return err
}
dataMountOpts := setUbuntuCoreDataMountOptions(systemdMountOptions{
Bind: true,
})
if err := doSystemdMount(boot.InstallUbuntuDataDir, boot.InitramfsDataDir, &dataMountOpts); err != nil {
return err
}
// We do not need anymore the extra data partition mount created on installation
if output, err := exec.Command("umount", boot.InstallUbuntuDataDir).CombinedOutput(); err != nil {
logger.Noticef("cannot unmount install data mount %s: %v",
boot.InstallUbuntuDataDir, osutil.OutputErr(output, err))
return osutil.OutputErr(output, err)
}
// We do not need the directory either
if err := os.Remove(boot.InstallUbuntuDataDir); err != nil {
logger.Noticef("warning: cannot remove %s: %v", boot.InstallUbuntuDataDir, err)
}
// Now we can write the snapd mount unit (needed as this is the first boot)
// It is debatable if we are in run mode or not as after installation
// from initramfs we run as normal, but anyway this does not change
// anything as this code is run only by UC.
isRunMode := false
rootfsDir := boot.InitramfsWritableDir(model, isRunMode)
snapdSeed := sysSnaps[snap.TypeSnapd]
if err := setupSeedSnapdSnap(rootfsDir, snapdSeed); err != nil {
return err
}
if preseed {
// Extract pre-seed tarball
runMode := false
if err := installApplyPreseededData(preseedSeed,
boot.InitramfsWritableDir(model, runMode)); err != nil {
return err
}
}
// Create drivers tree mount units to make it available before switch root.
// daemon-reload is not needed because it is done from initramfs later, this
// happens because on UC /etc/fstab is changed and systemd's
// initrd-parse-etc.service does the reload, as it detects entries with the
// x-initrd.mount option.
hasDriversTree, err := createKernelMounts(
rootfsDir, kernelSnap.SnapName(), kernelSnap.Revision, !isCore)
if err != nil {
return err
}
if hasDriversTree {
// FIXME: we should not remove or stop units while
// booting. That causes inconsistent jobs when
// something is depending on the removed unit,
// like: "[unit] has 'start' job queued, but 'stop' is
// included in transaction"
// Unmount the kernel snap mount, we keep it only for UC20/22
stdout, stderr, err := osutil.RunSplitOutput("systemd-mount", "--umount", kernelMountDir)
if err != nil {
return osutil.OutputErrCombine(stdout, stderr, err)
}
// Remove the unit file so it is not re-mounted after switch root
kernMntUnit := filepath.Join(dirs.SnapSystemdRunDir, "transient", "run-mnt-kernel.mount")
logger.Debugf("removing transient unit file %s", kernMntUnit)
if err := os.Remove(kernMntUnit); err != nil {
logger.Noticef("warning: cannot delete %s: %v", kernMntUnit, err)
}
// We do not need the directory either
if err := os.Remove(kernelMountDir); err != nil {
logger.Noticef("warning: cannot remove %s: %v", kernelMountDir, err)
}
}
if err := bootEnsureNextBootToRunMode(mst.recoverySystem); err != nil {
return fmt.Errorf("failed to set system to run mode: %v\n", err)
}
mst.mode = "run"
mst.recoverySystem = ""
return nil
}
// generateMountsMode* is called multiple times from initramfs until it
// no longer generates more mount points and just returns an empty output.
func generateMountsModeInstall(mst *initramfsMountsState) error {
// steps 1 and 2 are shared with recover mode
model, snaps, err := generateMountsCommonInstallRecoverStart(mst)
if err != nil {
return err
}
installAndRun, err := canInstallAndRunAtOnce(mst)
if err != nil {
return err
}
if installAndRun {
kernSnap := snaps[snap.TypeKernel]
// seed is cached at this point
theSeed, err := mst.LoadSeed("")
if err != nil {
return fmt.Errorf("internal error: cannot load seed: %v", err)
}
// Filter by mode, this is relevant only to get the
// kernel-modules components that are used in run mode and
// therefore need to be considered when installing from the
// initramfs to have the modules available early on first boot.
// TODO when running normal install or recover/factory-reset,
// we would need also this if we want the modules to be
// available early.
kernSnap, err = theSeed.ModeSnap(kernSnap.SnapName(), "run")
if err != nil {
return err
}
snaps[snap.TypeKernel] = kernSnap
if err := doInstall(mst, model, snaps); err != nil {
return err
}
return nil
} else {
if err := generateMountsCommonInstallRecoverContinue(model, snaps); err != nil {
return err
}
// 3. final step: write modeenv to tmpfs data dir and disable cloud-init in
// install mode
modeEnv, err := mst.EphemeralModeenvForModel(model, snaps)
if err != nil {
return err
}
isRunMode := false
if err := modeEnv.WriteTo(boot.InitramfsWritableDir(model, isRunMode)); err != nil {
return err
}
// done, no output, no error indicates to initramfs we are done with
// mounting stuff
return nil
}
}
// copyNetworkConfig copies the network configuration to the target
// directory. This is used to copy the network configuration
// data from a real uc20 ubuntu-data partition into a ephemeral one.
//
// The given srcRoot should point to the directory that contains the writable
// host system data. The given dstRoot should point to the directory that
// contains the writable system data for the ephemeral recovery system.
func copyNetworkConfig(srcRoot, dstRoot string) error {
for _, globEx := range []string{
// for network configuration setup by console-conf, etc.
// TODO:UC20: we want some way to "try" or "verify" the network
// configuration or to only use known-to-be-good network
// configuration i.e. from ubuntu-save before installing it
// onto recover mode, because the network configuration could
// have been what was broken so we don't want to break
// network configuration for recover mode as well, but for
// now this is fine
"etc/netplan/*",
// etc/machine-id is part of what systemd-networkd uses to generate a
// DHCP clientid (the other part being the interface name), so to have
// the same IP addresses across run mode and recover mode, we need to
// also copy the machine-id across
"etc/machine-id",
} {
if err := copyFromGlobHelper(srcRoot, dstRoot, globEx); err != nil {
return err
}
}
return nil
}
// copyUbuntuDataMisc copies miscellaneous other files from the run mode system
// to the recover system such as:
// - timesync clock to keep the same time setting in recover as in run mode
//
// The given srcRoot should point to the directory that contains the writable
// host system data. The given dstRoot should point to the directory that
// contains the writable system data for the ephemeral recovery system.
func copyUbuntuDataMisc(srcRoot, dstRoot string) error {
for _, globEx := range []string{
// systemd's timesync clock file so that the time in recover mode moves
// forward to what it was in run mode
// NOTE: we don't sync back the time movement from recover mode to run
// mode currently, unclear how/when we could do this, but recover mode
// isn't meant to be long lasting and as such it's probably not a big
// problem to "lose" the time spent in recover mode
"var/lib/systemd/timesync/clock",
} {
if err := copyFromGlobHelper(srcRoot, dstRoot, globEx); err != nil {
return err
}
}
return nil
}
// copyCoreUbuntuAuthData copies the authentication files like
// - extrausers passwd,shadow etc
// - sshd host configuration
// - user .ssh dir
//
// to the target directory. This is used to copy the authentication
// data from a real uc20 ubuntu-data partition into a ephemeral one.
func copyCoreUbuntuAuthData(srcUbuntuData, destUbuntuData string) error {
for _, globEx := range []string{
"system-data/var/lib/extrausers/*",
"system-data/etc/ssh/*",
// so that users have proper perms, i.e. console-conf added users are
// sudoers
"system-data/etc/sudoers.d/*",
"user-data/*/.ssh/*",
// this ensures we get proper authentication to snapd from "snap"
// commands in recover mode
"user-data/*/.snap/auth.json",
// this ensures we also get non-ssh enabled accounts copied
"user-data/*/.profile",
} {
if err := copyFromGlobHelper(srcUbuntuData, destUbuntuData, globEx); err != nil {
return err
}
}
// ensure the user state is transferred as well
srcState := filepath.Join(srcUbuntuData, "system-data/var/lib/snapd/state.json")
dstState := filepath.Join(destUbuntuData, "system-data/var/lib/snapd/state.json")
err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"})
if err != nil && !errors.Is(err, state.ErrNoState) {
return fmt.Errorf("cannot copy user state: %v", err)
}
return nil
}
// copyHybridUbuntuDataAuth copies the authentication files that are relevant on
// a hybrid system to the ubuntu data directory. Non-user specific files are
// copied to <destUbuntuData>/system-data. User specific files are copied to
// <destUbuntuData>/user-data.
func copyHybridUbuntuDataAuth(srcUbuntuData, destUbuntuData string) error {
destSystemData := filepath.Join(destUbuntuData, "system-data")
for _, globEx := range []string{
"etc/ssh/*",
"etc/sudoers.d/*",
"root/.ssh/*",
} {
if err := copyFromGlobHelper(
srcUbuntuData,
destSystemData,
globEx,
); err != nil {
return err
}
}
destHomeData := filepath.Join(srcUbuntuData, "home")
destUserData := filepath.Join(destUbuntuData, "user-data")
for _, globEx := range []string{
"*/.ssh/*",
"*/.snap/auth.json",
} {
if err := copyFromGlobHelper(
destHomeData,
destUserData,
globEx,
); err != nil {
return err
}
}
// ensure the user state is transferred as well
srcState := filepath.Join(srcUbuntuData, "var/lib/snapd/state.json")
dstState := filepath.Join(destUbuntuData, "system-data/var/lib/snapd/state.json")
err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"})
if err != nil && !errors.Is(err, state.ErrNoState) {
return fmt.Errorf("cannot copy user state: %v", err)
}
return nil
}
// drop a marker file that disables console-conf
func disableConsoleConf(dst string) error {
consoleConfCompleteFile := filepath.Join(dst, "system-data/var/lib/console-conf/complete")
if err := os.MkdirAll(filepath.Dir(consoleConfCompleteFile), 0755); err != nil {
return err
}
return os.WriteFile(consoleConfCompleteFile, nil, 0644)
}
// copySafeDefaultData will copy to the destination a "safe" set of data for
// a blank recover mode, i.e. one where we cannot copy authentication, etc. from
// the actual host ubuntu-data. Currently this is just a file to disable
// console-conf from running.
func copySafeDefaultData(dst string) error {
return disableConsoleConf(dst)
}
func copyFromGlobHelper(src, dst, globEx string) error {
matches, err := filepath.Glob(filepath.Join(src, globEx))
if err != nil {
return err
}
for _, p := range matches {
comps := strings.Split(strings.TrimPrefix(p, src), "/")
for i := range comps {
part := filepath.Join(comps[0 : i+1]...)
fi, err := os.Stat(filepath.Join(src, part))
if err != nil {
return err
}
if fi.IsDir() {
if err := os.Mkdir(filepath.Join(dst, part), fi.Mode()); err != nil && !os.IsExist(err) {
return err
}
st, ok := fi.Sys().(*syscall.Stat_t)
if !ok {
return fmt.Errorf("cannot get stat data: %v", err)
}
if err := os.Chown(filepath.Join(dst, part), int(st.Uid), int(st.Gid)); err != nil {
return err
}
} else {
if err := osutil.CopyFile(p, filepath.Join(dst, part), osutil.CopyFlagPreserveAll); err != nil {
return err
}
}
}
}
return nil
}
const (
// states for findState
partitionFound = "found"
partitionNotFound = "not-found"
partitionErrFinding = "error-finding"
)
// partitionState is the state of a partition after recover mode has completed
// for degraded mode.
type partitionState struct {
boot.PartitionState
// fsDevice is what decrypted mapper device corresponds to the
// partition, it can have the following states
// - successfully decrypted => the decrypted mapper device
// - unencrypted => the block device of the partition
// - identified as decrypted, but failed to decrypt => empty string
fsDevice string
// partDevice is always the physical block device of the partition, in the
// encrypted case this is the physical encrypted partition.
partDevice string
// findState indicates whether the partition was found on the disk or not.
findState string
}
type diskUnlockState struct {
// UbuntuData is the state of the ubuntu-data (or ubuntu-data-enc)
// partition.
UbuntuData partitionState
// UbuntuBoot is the state of the ubuntu-boot partition.
UbuntuBoot partitionState
// UbuntuSave is the state of the ubuntu-save (or ubuntu-save-enc)
// partition.
UbuntuSave partitionState
isDegraded bool
}
func (r *diskUnlockState) serializeTo(name string) error {
exportState := &boot.DiskUnlockState{
UbuntuData: r.UbuntuData.PartitionState,
UbuntuBoot: r.UbuntuBoot.PartitionState,
UbuntuSave: r.UbuntuSave.PartitionState,
}
return exportState.WriteTo(name)
}
func (r *diskUnlockState) LogDegraded(format string, v ...any) {
msg := fmt.Sprintf(format, v...)
r.isDegraded = true
logger.Notice(msg)
}
func (r *diskUnlockState) partition(part string) *partitionState {
switch part {
case "ubuntu-data":
return &r.UbuntuData
case "ubuntu-boot":
return &r.UbuntuBoot
case "ubuntu-save":
return &r.UbuntuSave
}
panic(fmt.Sprintf("unknown partition %s", part))
}
// stateFunc is a function which executes a state action, returns the next
// function (for the next) state or nil if it is the final state.
type stateFunc func() (stateFunc, error)
// recoverModeStateMachine is a state machine implementing the logic for
// degraded recover mode.
// A full state diagram for the state machine can be found in
// /cmd/snap-bootstrap/degraded-recover-mode.svg in this repo.
type recoverModeStateMachine struct {
// the current state is the one that is about to be executed
current stateFunc
// device model
model *asserts.Model
// boot mode (factory-reset or recover)
mode string
// the disk we have all our partitions on
disk disks.Disk
// when true, the fallback unlock paths will not be tried
noFallback bool
// TODO:UC20: for clarity turn this into into tristate:
// unknown|encrypted|unencrypted
isEncryptedDev bool
// state for tracking what happens as we progress through degraded mode of
// recovery
degradedState *diskUnlockState
}
func (m *recoverModeStateMachine) whichModel() (*asserts.Model, error) {
return m.model, nil
}
// degraded returns whether a degraded recover mode state has fallen back from
// the typical operation to some sort of degraded mode.
func (m *recoverModeStateMachine) degraded() bool {
r := m.degradedState
if m.isEncryptedDev {
// for encrypted devices, we need to have ubuntu-save mounted
if r.UbuntuSave.MountState != boot.PartitionMounted {
return true
}
// we also should have all the unlock keys as run keys
if r.UbuntuData.UnlockKey != boot.KeyRun {
return true
}
if r.UbuntuSave.UnlockKey != boot.KeyRun {
return true
}
} else {
// for unencrypted devices, ubuntu-save must either be mounted or
// absent-but-optional
if r.UbuntuSave.MountState != boot.PartitionMounted {
if r.UbuntuSave.MountState != boot.PartitionAbsentOptional {
return true
}
}
}
// ubuntu-boot and ubuntu-data should both be mounted
if r.UbuntuBoot.MountState != boot.PartitionMounted {
return true
}
if r.UbuntuData.MountState != boot.PartitionMounted {
return true
}
// TODO: should we also check MountLocation too?
// we should have nothing in the error log
return r.isDegraded
}
func (m *recoverModeStateMachine) diskOpts() *disks.Options {
if m.isEncryptedDev {
return &disks.Options{
IsDecryptedDevice: true,
}
}
return nil
}
func (m *recoverModeStateMachine) verifyMountPoint(dir, name string) error {
matches, err := m.disk.MountPointIsFromDisk(dir, m.diskOpts())
if err != nil {
return err
}
if !matches {
return fmt.Errorf("cannot validate mount: %s mountpoint target %s is expected to be from disk %s but is not", name, dir, m.disk.Dev())
}
return nil
}
func (m *recoverModeStateMachine) setFindState(partName, partUUID string, err error, optionalPartition bool) error {
part := m.degradedState.partition(partName)
if err != nil {
if _, ok := err.(disks.PartitionNotFoundError); ok {
// explicit error that the device was not found
part.findState = partitionNotFound
if !optionalPartition {
// partition is not optional, thus the error is relevant
m.degradedState.LogDegraded("cannot find %v partition on disk %s", partName, m.disk.Dev())
}
return nil
}
// the error is not "not-found", so we have a real error
part.findState = partitionErrFinding
m.degradedState.LogDegraded("error finding %v partition on disk %s: %v", partName, m.disk.Dev(), err)
return nil
}
// device was found
part.findState = partitionFound
dev := fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID)
part.partDevice = dev
part.fsDevice = dev
return nil
}
func (m *recoverModeStateMachine) setMountState(part, where string, err error) error {
if err != nil {
m.degradedState.LogDegraded("cannot mount %v: %v", part, err)
m.degradedState.partition(part).MountState = boot.PartitionErrMounting
return nil
}
m.degradedState.partition(part).MountState = boot.PartitionMounted
m.degradedState.partition(part).MountLocation = where
if err := m.verifyMountPoint(where, part); err != nil {
m.degradedState.LogDegraded("cannot verify %s mount point at %v: %v", part, where, err)
return err
}
return nil
}
func (m *recoverModeStateMachine) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) {
if unlockRes.IsEncrypted {
m.isEncryptedDev = true
}
m.degradedState.setUnlockStateWithRunKey(partName, unlockRes, err)
}
func (d *diskUnlockState) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) {
part := d.partition(partName)
// save the device if we found it from secboot
if unlockRes.PartDevice != "" {
part.findState = partitionFound
part.partDevice = unlockRes.PartDevice
part.fsDevice = unlockRes.FsDevice
} else {
part.findState = partitionNotFound
}
if err != nil {
// create different error message for encrypted vs unencrypted
if unlockRes.IsEncrypted {
// if we know the device is decrypted we must also always know at
// least the partDevice (which is the encrypted block device)
d.LogDegraded("cannot unlock encrypted %s (device %s) with sealed run key: %v", partName, part.partDevice, err)
part.UnlockState = boot.PartitionErrUnlocking
} else {
// TODO: we don't know if this is a plain not found or a different error
d.LogDegraded("cannot locate %s partition for mounting host data: %v", partName, err)
}
return
}
if unlockRes.IsEncrypted {
// unlocked successfully
part.UnlockState = boot.PartitionUnlocked
switch unlockRes.UnlockMethod {
case secboot.UnlockedWithSealedKey:
part.UnlockKey = boot.KeyRun
case secboot.UnlockedWithRecoveryKey:
part.UnlockKey = boot.KeyRecovery
case secboot.UnlockedWithKey:
// This is the case when opening the save with the key file
part.UnlockKey = boot.KeyRun
default:
panic(fmt.Errorf("Unexpected unlock method: %v", unlockRes.UnlockMethod))
}
}
}
func (m *recoverModeStateMachine) setUnlockStateWithFallbackKey(partName string, unlockRes secboot.UnlockResult, err error) error {
// first check the result and error for consistency; since we are using udev
// there could be inconsistent results at different points in time
// TODO: consider refactoring UnlockVolumeUsingSealedKeyIfEncrypted to not
// also find the partition on the disk, that should eliminate this
// consistency checking as we can code it such that we don't get these
// possible inconsistencies
// do basic consistency checking on unlockRes to make sure the
// result makes sense.
if unlockRes.FsDevice != "" && err != nil {
// This case should be impossible to enter, we can't
// have a filesystem device but an error set
return fmt.Errorf("internal error: inconsistent return values from UnlockVolumeUsingSealedKeyIfEncrypted for partition %s: %v", partName, err)
}
part := m.degradedState.partition(partName)
// Also make sure that if we previously saw a partition device that we see
// the same device again.
if unlockRes.PartDevice != "" && part.partDevice != "" && unlockRes.PartDevice != part.partDevice {
return fmt.Errorf("inconsistent partitions found for %s: previously found %s but now found %s", partName, part.partDevice, unlockRes.PartDevice)
}
// ensure consistency between encrypted state of the device/disk and what we
// may have seen previously
if m.isEncryptedDev && !unlockRes.IsEncrypted {
// then we previously were able to positively identify an
// ubuntu-data-enc but can't anymore, so we have inconsistent results
// from inspecting the disk which is suspicious and we should fail
return fmt.Errorf("inconsistent disk encryption status: previous access resulted in encrypted, but now is unencrypted from partition %s", partName)
}
// now actually process the result into the state
if unlockRes.PartDevice != "" {
part.findState = partitionFound
// Note that in some case this may be redundantly assigning the same
// value to partDevice again.
part.partDevice = unlockRes.PartDevice
part.fsDevice = unlockRes.FsDevice
}
// There are a few cases where this could be the first time that we found a
// decrypted device in the UnlockResult, but m.isEncryptedDev is still
// false.
// - The first case is if we couldn't find ubuntu-boot at all, in which case
// we can't use the run object keys from there and instead need to directly
// fallback to trying the fallback object keys from ubuntu-seed
// - The second case is if we couldn't identify an ubuntu-data-enc or an
// ubuntu-data partition at all, we still could have an ubuntu-save-enc
// partition in which case we maybe could still have an encrypted disk that
// needs unlocking with the fallback object keys from ubuntu-seed
//
// As such, if m.isEncryptedDev is false, but unlockRes.IsEncrypted is
// true, then it is safe to assign m.isEncryptedDev to true.
if !m.isEncryptedDev && unlockRes.IsEncrypted {
m.isEncryptedDev = true
}
if err != nil {
// create different error message for encrypted vs unencrypted
if m.isEncryptedDev {
m.degradedState.LogDegraded("cannot unlock encrypted %s partition with sealed fallback key: %v", partName, err)
part.UnlockState = boot.PartitionErrUnlocking
} else {
// if we don't have an encrypted device and err != nil, then the
// device must be not-found, see above checks
// log an error the partition is mandatory
m.degradedState.LogDegraded("cannot locate %s partition: %v", partName, err)
}
return nil
}
if m.isEncryptedDev {
// unlocked successfully
part.UnlockState = boot.PartitionUnlocked
// figure out which key/method we used to unlock the partition
switch unlockRes.UnlockMethod {
case secboot.UnlockedWithSealedKey:
part.UnlockKey = boot.KeyFallback
case secboot.UnlockedWithRecoveryKey:
part.UnlockKey = boot.KeyRecovery
// TODO: should we fail with internal error for default case here?
}
}
return nil
}
func newRecoverModeStateMachine(model *asserts.Model, bootMode string, disk disks.Disk, allowFallback bool) *recoverModeStateMachine {
m := &recoverModeStateMachine{
model: model,
mode: bootMode,
disk: disk,
degradedState: &diskUnlockState{},
noFallback: !allowFallback,
}
// first step is to mount ubuntu-boot to check for run mode keys to unlock
// ubuntu-data
m.current = m.mountBoot
return m
}
func (m *recoverModeStateMachine) execute() (finished bool, err error) {
next, err := m.current()
m.current = next
finished = next == nil
if finished && err == nil {
if err := m.finalize(); err != nil {
return true, err
}
}
return finished, err
}
func (m *recoverModeStateMachine) finalize() error {
// check soundness
// the grade check makes sure that if data was mounted unencrypted
// but the model is secured it will end up marked as untrusted
isEncrypted := m.isEncryptedDev || m.model.StorageSafety() == asserts.StorageSafetyEncrypted
part := m.degradedState.partition("ubuntu-data")
if part.MountState == boot.PartitionMounted && isEncrypted {
// check that save and data match
// We want to avoid a chosen ubuntu-data
// (e.g. activated with a recovery key) to get access
// via its logins to the secrets in ubuntu-save (in
// particular the policy update auth key)
// TODO:UC20: we should try to be a bit more specific here in checking that
// data and save match, and not mark data as untrusted if we
// know that the real save is locked/protected (or doesn't exist
// in the case of bad corruption) because currently this code will
// mark data as untrusted, even if it was unlocked with the run
// object key and we failed to unlock ubuntu-save at all, which is
// undesirable. This effectively means that you need to have both
// ubuntu-data and ubuntu-save unlockable and have matching marker
// files in order to use the files from ubuntu-data to log-in,
// etc.
trustData, _ := checkDataAndSavePairing(boot.InitramfsHostWritableDir(m.model))
if !trustData {
part.MountState = boot.PartitionMountedUntrusted
m.degradedState.LogDegraded("cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install")
}
}
return nil
}
func (m *recoverModeStateMachine) trustData() bool {
return m.degradedState.partition("ubuntu-data").MountState == boot.PartitionMounted
}
// mountBoot is the first state to execute in the state machine, it can
// transition to the following states:
// - if ubuntu-boot is mounted successfully, execute unlockDataRunKey
// - if ubuntu-boot can't be mounted, execute unlockDataFallbackKey
// - if we mounted the wrong ubuntu-boot (or otherwise can't verify which one we
// mounted), return fatal error
func (m *recoverModeStateMachine) mountBoot() (stateFunc, error) {
part := m.degradedState.partition("ubuntu-boot")
// use the disk we mounted ubuntu-seed from as a reference to find
// ubuntu-seed and mount it
partUUID, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-boot")
const partitionMandatory = false
if err := m.setFindState("ubuntu-boot", partUUID, findErr, partitionMandatory); err != nil {
return nil, err
}
if part.findState != partitionFound {
// if we didn't find ubuntu-boot, we can't try to unlock data with the
// run key, and should instead just jump straight to attempting to
// unlock with the fallback key
return m.unlockDataFallbackKey, nil
}
// should we fsck ubuntu-boot? probably yes because on some platforms
// (u-boot for example) ubuntu-boot is vfat and it could have been unmounted
// dirtily, and we need to fsck it to ensure it is mounted safely before
// reading keys from it
systemdOpts := &systemdMountOptions{
NeedsFsck: true,
Private: true,
}
mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, systemdOpts)
if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil {
return nil, err
}
if part.MountState == boot.PartitionErrMounting {
// if we didn't mount data, then try to unlock data with the
// fallback key
return m.unlockDataFallbackKey, nil
}
// next step try to unlock data with run object
return m.unlockDataRunKey, nil
}
// stateUnlockDataRunKey will try to unlock ubuntu-data with the normal run-mode
// key, and if it fails, progresses to the next state, which is either:
// - failed to unlock data, but we know it's an encrypted device -> try to unlock with fallback key
// - failed to find data at all -> try to unlock save
// - unlocked data with run key -> mount data
func (m *recoverModeStateMachine) unlockDataRunKey() (stateFunc, error) {
runModeKey := device.DataSealedKeyUnder(boot.InitramfsBootEncryptionKeyDir)
unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{
// don't allow using the recovery key to unlock, we only try using the
// recovery key after we first try the fallback object
AllowRecoveryKey: false,
WhichModel: m.whichModel,
BootMode: m.mode,
}
unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", runModeKey, unlockOpts)
m.setUnlockStateWithRunKey("ubuntu-data", unlockRes, unlockErr)
if unlockErr != nil {
// we couldn't unlock ubuntu-data with the primary key, or we didn't
// find it in the unencrypted case
if unlockRes.IsEncrypted {
// we know the device is encrypted, so the next state is to try
// unlocking with the fallback key
return m.unlockDataFallbackKey, nil
}
// if we didn't even find the device to the point where it would have
// been identified as decrypted or unencrypted device, we could have
// just entirely lost ubuntu-data-enc, and we could still have an
// encrypted device, so instead try to unlock ubuntu-save with the
// fallback key, the logic there can also handle an unencrypted ubuntu-save
return m.unlockMaybeEncryptedAloneSaveFallbackKey, nil
}
// otherwise successfully unlocked it (or just found it if it was unencrypted)
// so just mount it
return m.mountData, nil
}
func (m *recoverModeStateMachine) unlockDataFallbackKey() (stateFunc, error) {
if m.noFallback {
return nil, fmt.Errorf("cannot unlock ubuntu-data (fallback disabled)")
}
// try to unlock data with the fallback key on ubuntu-seed, which must have
// been mounted at this point
unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{
// we want to allow using the recovery key if the fallback key fails as
// using the fallback object is the last chance before we give up trying
// to unlock data
AllowRecoveryKey: true,
WhichModel: m.whichModel,
BootMode: m.mode,
}
// TODO: this prompts for a recovery key
// TODO: we should somehow customize the prompt to mention what key we need
// the user to enter, and what we are unlocking (as currently the prompt
// says "recovery key" and the partition UUID for what is being unlocked)
dataFallbackKey := device.FallbackDataSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir)
unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts)
if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr); err != nil {
return nil, err
}
if unlockErr != nil {
// skip trying to mount data, since we did not unlock data we cannot
// open save with with the run key, so try the fallback one
return m.unlockEncryptedSaveFallbackKey, nil
}
// unlocked it, now go mount it
return m.mountData, nil
}
func (m *recoverModeStateMachine) mountData() (stateFunc, error) {
data := m.degradedState.partition("ubuntu-data")
// don't do fsck on the data partition, it could be corrupted
// however, data should always be mounted nosuid to prevent snaps from
// extracting suid executables there and trying to circumvent the sandbox
mountOpts := &systemdMountOptions{
NoSuid: true,
Private: true,
}
mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, mountOpts)
if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil {
return nil, err
}
if m.isEncryptedDev {
if mountErr == nil {
// if we succeeded in mounting data and we are encrypted, the next step
// is to unlock save with the run key from ubuntu-data
return m.unlockEncryptedSaveRunKey, nil
} else {
// we are encrypted and we failed to mount data successfully, meaning we
// don't have the bare key from ubuntu-data to use, and need to fall back
// to the sealed key from ubuntu-seed
return m.unlockEncryptedSaveFallbackKey, nil
}
}
// the data is not encrypted, in which case the ubuntu-save, if it
// exists, will be plain too
return m.openUnencryptedSave, nil
}
func (m *recoverModeStateMachine) unlockEncryptedSaveRunKey() (stateFunc, error) {
// to get to this state, we needed to have mounted ubuntu-data on host, so
// if encrypted, we can try to read the run key from host ubuntu-data
saveKey := device.SaveKeyUnder(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir(m.model)))
key, err := os.ReadFile(saveKey)
if err != nil {
// log the error and skip to trying the fallback key
m.degradedState.LogDegraded("cannot access run ubuntu-save key: %v", err)
return m.unlockEncryptedSaveFallbackKey, nil
}
unlockRes, unlockErr := secbootUnlockEncryptedVolumeUsingProtectorKey(m.disk, "ubuntu-save", key)
m.setUnlockStateWithRunKey("ubuntu-save", unlockRes, unlockErr)
if unlockErr != nil {
// failed to unlock with run key, try fallback key
return m.unlockEncryptedSaveFallbackKey, nil
}
// unlocked it properly, go mount it
return m.mountSave, nil
}
func (m *recoverModeStateMachine) unlockMaybeEncryptedAloneSaveFallbackKey() (stateFunc, error) {
// we can only get here by not finding ubuntu-data at all, meaning the
// system can still be encrypted and have an encrypted ubuntu-save,
// which we will determine now
// first check whether there is an encrypted save
_, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel(secboot.EncryptedPartitionName("ubuntu-save"))
if findErr == nil {
// well there is one, go try and unlock it
return m.unlockEncryptedSaveFallbackKey, nil
}
// encrypted ubuntu-save does not exist, there may still be an
// unencrypted one
return m.openUnencryptedSave, nil
}
func (m *recoverModeStateMachine) openUnencryptedSave() (stateFunc, error) {
// do we have ubuntu-save at all?
partSave := m.degradedState.partition("ubuntu-save")
const partitionOptional = true
partUUID, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-save")
if err := m.setFindState("ubuntu-save", partUUID, findErr, partitionOptional); err != nil {
return nil, err
}
if partSave.findState == partitionFound {
// we have ubuntu-save, go mount it
return m.mountSave, nil
}
// unencrypted ubuntu-save was not found, try to log something in case
// the early boot output can be collected for debugging purposes
if uuid, err := m.disk.FindMatchingPartitionUUIDWithFsLabel(secboot.EncryptedPartitionName("ubuntu-save")); err == nil {
// highly unlikely that encrypted save exists
logger.Noticef("ignoring unexpected encrypted ubuntu-save with UUID %q", uuid)
} else {
logger.Noticef("ubuntu-save was not found")
}
// save is optional in an unencrypted system
partSave.MountState = boot.PartitionAbsentOptional
// we're done, nothing more to try
return nil, nil
}
func (m *recoverModeStateMachine) unlockEncryptedSaveFallbackKey() (stateFunc, error) {
// try to unlock save with the fallback key on ubuntu-seed, which must have
// been mounted at this point
if m.noFallback {
return nil, fmt.Errorf("cannot unlock ubuntu-save (fallback disabled)")
}
unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{
// we want to allow using the recovery key if the fallback key fails as
// using the fallback object is the last chance before we give up trying
// to unlock save
AllowRecoveryKey: true,
WhichModel: m.whichModel,
BootMode: m.mode,
}
saveFallbackKey := device.FallbackSaveSealedKeyUnder(boot.InitramfsSeedEncryptionKeyDir)
// TODO: this prompts again for a recover key, but really this is the
// reinstall key we will prompt for
// TODO: we should somehow customize the prompt to mention what key we need
// the user to enter, and what we are unlocking (as currently the prompt
// says "recovery key" and the partition UUID for what is being unlocked)
unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-save", saveFallbackKey, unlockOpts)
if err := m.setUnlockStateWithFallbackKey("ubuntu-save", unlockRes, unlockErr); err != nil {
return nil, err
}
if unlockErr != nil {
// all done, nothing left to try and mount, mounting ubuntu-save is the
// last step but we couldn't find or unlock it
return nil, nil
}
// otherwise we unlocked it, so go mount it
return m.mountSave, nil
}
func (m *recoverModeStateMachine) mountSave() (stateFunc, error) {
save := m.degradedState.partition("ubuntu-save")
// TODO: should we fsck ubuntu-save ?
mountOpts := &systemdMountOptions{
Private: true,
NoDev: true,
NoSuid: true,
NoExec: true,
}
mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, mountOpts)
if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil {
return nil, err
}
// all done, nothing left to try and mount
return nil, nil
}
func (m *recoverModeStateMachine) writeRecoverUnlockState() error {
// write out degraded.json if we ended up falling back somewhere
if m.degraded() {
if err := m.degradedState.serializeTo(boot.DegradedStateFileName); err != nil {
return err
}
}
// we always output unlocked.json
return m.degradedState.serializeTo(boot.UnlockedStateFileName)
}
func (m *recoverModeStateMachine) writeFactoryResetUnlockState() error {
return m.degradedState.serializeTo(boot.UnlockedStateFileName)
}
func generateMountsModeRecover(mst *initramfsMountsState) error {
// steps 1 and 2 are shared with install mode
model, snaps, err := generateMountsRecoverOrFactoryReset(mst)
if err != nil {
return err
}
// get the disk that we mounted the ubuntu-seed partition from as a
// reference point for future mounts
disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
if err != nil {
return err
}
// for most cases we allow the use of fallback to unlock/mount things
allowFallback := true
tryingCurrentSystem, err := boot.InitramfsIsTryingRecoverySystem(mst.recoverySystem)
if err != nil {
if boot.IsInconsistentRecoverySystemState(err) {
// there is some try recovery system state in bootenv
// but it is inconsistent, make sure we clear it and
// return back to run mode
// finalize reboots or panics
logger.Noticef("try recovery system state is inconsistent: %v", err)
finalizeTryRecoverySystemAndReboot(model, boot.TryRecoverySystemOutcomeInconsistent)
}
return err
}
if tryingCurrentSystem {
// but in this case, use only the run keys
allowFallback = false
// make sure that if rebooted, the next boot goes into run mode
if err := boot.EnsureNextBootToRunMode(""); err != nil {
return err
}
}
// 3. run the state machine logic for mounting partitions, this involves
// trying to unlock then mount ubuntu-data, and then unlocking and
// mounting ubuntu-save
// see the state* functions for details of what each step does and
// possible transition points
machine, err := func() (machine *recoverModeStateMachine, err error) {
// first state to execute is to unlock ubuntu-data with the run key
machine = newRecoverModeStateMachine(model, "recover", disk, allowFallback)
for {
finished, err := machine.execute()
// TODO: consider whether certain errors are fatal or not
if err != nil {
return nil, err
}
if finished {
break
}
}
return machine, nil
}()
if tryingCurrentSystem {
// end of the line for a recovery system we are only trying out,
// this branch always ends with a reboot (or a panic)
var outcome boot.TryRecoverySystemOutcome
if err == nil && !machine.degraded() {
outcome = boot.TryRecoverySystemOutcomeSuccess
} else {
outcome = boot.TryRecoverySystemOutcomeFailure
if err == nil {
err = fmt.Errorf("in degraded state")
}
logger.Noticef("try recovery system %q failed: %v", mst.recoverySystem, err)
}
// finalize reboots or panics
finalizeTryRecoverySystemAndReboot(model, outcome)
}
if err != nil {
return err
}
// 3.1 write out unlock states (unlocked.json, and eventually degraded.json)
if err := machine.writeRecoverUnlockState(); err != nil {
return err
}
// 4. final step: copy the auth data and network config from
// the real ubuntu-data dir to the ephemeral ubuntu-data
// dir, write the modeenv to the tmpfs data, and disable
// cloud-init in recover mode
// if we have the host location, then we were able to successfully mount
// ubuntu-data, and as such we can proceed with copying files from there
// onto the tmpfs
// Proceed only if we trust ubuntu-data to be paired with ubuntu-save
if machine.trustData() {
// on hybrid systems, we take special care to import the root user and
// users from the "admin" and "sudo" groups into the ephemeral system.
// this is our best-effort for allowing an owner of a hybrid system to
// login to the created recovery system.
hybrid := model.Classic() && model.KernelSnap() != nil
hostSystemData := boot.InitramfsHostWritableDir(model)
recoverySystemData := boot.InitramfsWritableDir(model, false)
if hybrid {
// TODO: eventually, the base will be mounted directly on /sysroot.
// this will need to change once that happens.
if err := importHybridUserData(
hostSystemData,
filepath.Join(boot.InitramfsRunMntDir, "base"),
); err != nil {
return err
}
if err := copyHybridUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil {
return err
}
} else {
// TODO: erroring here should fallback to copySafeDefaultData and
// proceed on with degraded mode anyways
if err := copyCoreUbuntuAuthData(
boot.InitramfsHostUbuntuDataDir,
boot.InitramfsDataDir,
); err != nil {
return err
}
}
if err := copyNetworkConfig(hostSystemData, recoverySystemData); err != nil {
return err
}
if err := copyUbuntuDataMisc(hostSystemData, recoverySystemData); err != nil {
return err
}
} else {
// we don't have ubuntu-data host mountpoint, so we should setup safe
// defaults for i.e. console-conf in the running image to block
// attackers from accessing the system - just because we can't access
// ubuntu-data doesn't mean that attackers wouldn't be able to if they
// could login
if err := copySafeDefaultData(boot.InitramfsDataDir); err != nil {
return err
}
}
modeEnv, err := mst.EphemeralModeenvForModel(model, snaps)
if err != nil {
return err
}
isRunMode := false
if err := modeEnv.WriteTo(boot.InitramfsWritableDir(model, isRunMode)); err != nil {
return err
}
// finally we need to modify the bootenv to mark the system as successful,
// this ensures that when you reboot from recover mode without doing
// anything else, you are auto-transitioned back to run mode
// TODO:UC20: as discussed unclear we need to pass the recovery system here
if err := boot.EnsureNextBootToRunMode(mst.recoverySystem); err != nil {
return err
}
// done, no output, no error indicates to initramfs we are done with
// mounting stuff
return nil
}
func generateMountsModeFactoryReset(mst *initramfsMountsState) error {
// steps 1 and 2 are shared with install mode
model, snaps, err := generateMountsRecoverOrFactoryReset(mst)
if err != nil {
return err
}
// get the disk that we mounted the ubuntu-seed partition from as a
// reference point for future mounts
disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil)
if err != nil {
return err
}
// step 3: find ubuntu-save, unlock and mount, note that factory-reset
// mode only cares about ubuntu-save, as ubuntu-data and ubuntu-boot
// will be wiped anyway so we do not even bother looking up those
// partitions (which may be corrupted too, hence factory-reset was
// invoked)
machine, err := func() (machine *recoverModeStateMachine, err error) {
allowFallback := true
machine = newRecoverModeStateMachine(model, "factory-reset", disk, allowFallback)
// start from looking up encrypted ubuntu-save and unlocking with the fallback key
machine.current = machine.unlockMaybeEncryptedAloneSaveFallbackKey
for {
finished, err := machine.execute()
// TODO: consider whether certain errors are fatal or not
if err != nil {
return nil, err
}
if finished {
break
}
}
return machine, nil
}()
if err != nil {
return err
}
if err := machine.writeFactoryResetUnlockState(); err != nil {
return err
}
// disable console-conf as it won't be needed
if err := disableConsoleConf(boot.InitramfsDataDir); err != nil {
return err
}
modeEnv, err := mst.EphemeralModeenvForModel(model, snaps)
if err != nil {
return err
}
isRunMode := false
if err := modeEnv.WriteTo(boot.InitramfsWritableDir(model, isRunMode)); err != nil {
return err
}
// done, no output, no error indicates to initramfs we are done with
// mounting stuff
return nil
}
// checkDataAndSavePairing make sure that ubuntu-data and ubuntu-save
// come from the same install by comparing secret markers in them
func checkDataAndSavePairing(rootdir string) (bool, error) {
marker1, marker2, err := device.ReadEncryptionMarkers(dirs.SnapFDEDirUnder(rootdir), dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir))
if err != nil {
return false, err
}
return subtle.ConstantTimeCompare(marker1, marker2) == 1, nil
}
// waitFile waits for the given file/device-node/directory to appear.
var waitFile = func(path string, wait time.Duration, n int) error {
for i := 0; i < n; i++ {
if osutil.FileExists(path) {
return nil
}
time.Sleep(wait)
}
return fmt.Errorf("no %v after waiting for %v", path, time.Duration(n)*wait)
}
// TODO: those have to be waited by udev instead
func waitForDevice(path string) error {
if !osutil.FileExists(filepath.Join(dirs.GlobalRootDir, path)) {
pollWait := 50 * time.Millisecond
pollIterations := 1200
logger.Noticef("waiting up to %v for %v to appear", time.Duration(pollIterations)*pollWait, path)
if err := waitFile(filepath.Join(dirs.GlobalRootDir, path), pollWait, pollIterations); err != nil {
return fmt.Errorf("cannot find device: %v", err)
}
}
return nil
}
// Defined externally for faster unit tests
var pollWaitForLabel = 50 * time.Millisecond
var pollWaitForLabelIters = 1200
// TODO: those have to be waited by udev instead
func waitForCandidateByLabelPath(label string) (string, error) {
logger.Noticef("waiting up to %v for label %v to appear",
time.Duration(pollWaitForLabelIters)*pollWaitForLabel, label)
var err error
for i := 0; i < pollWaitForLabelIters; i++ {
var candidate string
// Ideally depending on the type of error we would return
// immediately or try again, but that would complicate code more
// than necessary and the extra wait will happen only when we
// will fail to boot anyway. Note also that this code is
// actually racy as we could get a not-best-possible-label (say,
// we get "Ubuntu-boot" while actually an exact "ubuntu-boot"
// label exists but the link has not been created yet): this is
// not a fully solvable problem although waiting by udev will
// help if the disk is present on boot.
if candidate, err = disks.CandidateByLabelPath(label); err == nil {
logger.Noticef("label %q found", candidate)
return candidate, nil
}
time.Sleep(pollWaitForLabel)
}
// This is the last error from CandidateByLabelPath
return "", err
}
func getNonUEFISystemDisk(fallbacklabel string) (string, error) {
values, err := kcmdline.KeyValues("snapd_system_disk")
if err != nil {
return "", err
}
if value, ok := values["snapd_system_disk"]; ok {
if err := waitForDevice(value); err != nil {
return "", err
}
systemdDisk, err := disks.DiskFromDeviceName(value)
if err != nil {
systemdDiskDevicePath, errDevicePath := disks.DiskFromDevicePath(value)
if errDevicePath != nil {
return "", fmt.Errorf("%q can neither be used as a device nor as a block: %v; %v", value, errDevicePath, err)
}
systemdDisk = systemdDiskDevicePath
}
partition, err := systemdDisk.FindMatchingPartitionWithFsLabel(fallbacklabel)
if err != nil {
return "", err
}
return partition.KernelDeviceNode, nil
}
candidate, err := waitForCandidateByLabelPath(fallbacklabel)
if err != nil {
return "", err
}
return candidate, nil
}
// mountNonDataPartitionMatchingKernelDisk will select the partition
// to mount at dir using the boot package function
// FindPartitionUUIDForBootedKernelDisk to determine what partition
// the booted kernel came from.
//
// If "snap-bootstrap scan-disk" was run as part of udev it will
// restrict the search of the partition from the boot disk it found.
//
// If "snap-bootstrap scan-disk" is not in use (legacy case),
// it will look for any partition that matches the boot.
//
// If which disk the kernel came from cannot be determined, then it
// will fallback to mounting via the specified disk label. If
// "snap-bootstrap scan-disk" was used, it will restrict the search to
// the boot disk.
func mountNonDataPartitionMatchingKernelDisk(dir, fallbacklabel string, opts *systemdMountOptions) error {
var partSrc string
if osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/dev/disk/snapd/disk")) {
disk, err := disks.DiskFromDeviceName("/dev/disk/snapd/disk")
if err != nil {
return err
}
partuuid, err := bootFindPartitionUUIDForBootedKernelDisk()
if err == nil {
partition, err := disk.FindMatchingPartitionWithPartUUID(partuuid)
if err != nil {
return err
}
partSrc = partition.KernelDeviceNode
} else {
partition, err := disk.FindMatchingPartitionWithFsLabel(fallbacklabel)
if err != nil {
return err
}
partSrc = partition.KernelDeviceNode
}
} else {
partuuid, err := bootFindPartitionUUIDForBootedKernelDisk()
if err == nil {
// TODO: the by-partuuid is only available on gpt disks, on mbr we need
// to use by-uuid or by-id
partSrc = filepath.Join("/dev/disk/by-partuuid", partuuid)
} else {
partSrc, err = getNonUEFISystemDisk(fallbacklabel)
if err != nil {
return err
}
}
// The partition uuid is read from the EFI variables. At this point
// the kernel may not have initialized the storage HW yet so poll
// here.
if err := waitForDevice(partSrc); err != nil {
return err
}
}
return doSystemdMount(partSrc, dir, opts)
}
func createSysrootMount() bool {
// This env var is set by snap-initramfs-mounts.service for 24+ initramfs. We
// prefer this to checking the model so 24+ kernels can run with models using
// older bases. Although this situation is not really supported as the
// initramfs systemd bits would not match those in the base, we allow it as
// it has been something done in the past and updates could break those
// systems.
isCore24plus := osGetenv("CORE24_PLUS_INITRAMFS")
return isCore24plus == "1" || isCore24plus == "true"
}
func generateMountsCommonInstallRecoverStart(mst *initramfsMountsState) (model *asserts.Model, sysSnaps map[snap.Type]*seed.Snap, err error) {
seedMountOpts := &systemdMountOptions{
// always fsck the partition when we are mounting it, as this is the
// first partition we will be mounting, we can't know if anything is
// corrupted yet
NeedsFsck: true,
Private: true,
NoSuid: true,
NoDev: true,
NoExec: true,
}
// 1. always ensure seed partition is mounted first before the others,
// since the seed partition is needed to mount the snap files there
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed", seedMountOpts); err != nil {
return nil, nil, err
}
// load model and verified essential snaps metadata
typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget}
theSeed, err := mst.LoadSeed("")
if err != nil {
return nil, nil, fmt.Errorf("cannot load seed: %v", err)
}
perf := timings.New(nil)
if err := theSeed.LoadEssentialMeta(typs, perf); err != nil {
return nil, nil, fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err)
}
model = theSeed.Model()
essSnaps := theSeed.EssentialSnaps()
// 2.1. measure model
err = stampedAction(fmt.Sprintf("%s-model-measured", mst.recoverySystem), func() error {
return secbootMeasureSnapModelWhenPossible(func() (*asserts.Model, error) {
return model, nil
})
})
if err != nil {
return nil, nil, err
}
// verified model from the seed is now measured
mst.SetVerifiedBootModel(model)
// at this point on a system with TPM-based encryption
// data can be open only if the measured model matches the actual
// expected recovery model we sealed against.
// TODO:UC20: on ARM systems and no TPM with encryption
// we need other ways to make sure that the disk is opened
// and we continue booting only for expected recovery models
// 2.2. (auto) select recovery system and mount seed snaps
// TODO:UC20: do we need more cross checks here?
systemSnaps := make(map[snap.Type]*seed.Snap)
for _, essentialSnap := range essSnaps {
systemSnaps[essentialSnap.EssentialType] = essentialSnap
if essentialSnap.EssentialType == snap.TypeBase && createSysrootMount() {
// Create unit to mount directly to /sysroot. We restrict
// this to UC24+ for the moment, until we backport necessary
// changes to the UC20/22 initramfs. Note that a transient
// unit is not used as it tries to be restarted after the
// switch root, and fails.
what := essentialSnap.Path
if err := writeSysrootMountUnit(what, "squashfs"); err != nil {
return nil, nil, fmt.Errorf(
"cannot write sysroot.mount (what: %s): %v", what, err)
}
// Do a daemon reload so systemd knows about the new sysroot mount unit
// (populate-writable.service depends on sysroot.mount, we need to make
// sure systemd knows this unit before snap-initramfs-mounts.service
// finishes)
sysd := systemd.New(systemd.SystemMode, nil)
if err := sysd.DaemonReload(); err != nil {
return nil, nil, err
}
// We need to restart initrd-root-fs.target so its dependencies are
// re-calculated considering the new sysroot.mount unit. See
// https://github.com/systemd/systemd/issues/23034 on why this is
// needed.
if err := sysd.StartNoBlock([]string{"initrd-root-fs.target"}); err != nil {
return nil, nil, err
}
if model.Classic() && model.KernelSnap() != nil {
// Mount ephemerally for recover mode to gain access to /etc data
dir := snapTypeToMountDir[essentialSnap.EssentialType]
if err := doSystemdMount(essentialSnap.Path,
filepath.Join(boot.InitramfsRunMntDir, dir),
&systemdMountOptions{
Ephemeral: true,
ReadOnly: true,
Private: true,
}); err != nil {
return nil, nil, err
}
}
} else if essentialSnap.EssentialType == snap.TypeSnapd {
// We write later a unit for this one, when the data
// partition is mounted
continue
} else {
dir := snapTypeToMountDir[essentialSnap.EssentialType]
// TODO:UC20: we need to cross-check the kernel path
// with snapd_recovery_kernel used by grub
if err := doSystemdMount(essentialSnap.Path,
filepath.Join(boot.InitramfsRunMntDir, dir),
mountReadOnlyOptions); err != nil {
return nil, nil, err
}
}
}
return model, systemSnaps, nil
}
func generateMountsCommonInstallRecoverContinue(model *asserts.Model, sysSnaps map[snap.Type]*seed.Snap) (err error) {
// TODO:UC20: after we have the kernel and base snaps mounted, we should do
// the bind mounts from the kernel modules on top of the base
// mount and delete the corresponding systemd units from the
// initramfs layout
// TODO:UC20: after the kernel and base snaps are mounted, we should setup
// writable here as well to take over from "the-modeenv" script
// in the initrd too
// TODO:UC20: after the kernel and base snaps are mounted and writable is
// mounted, we should also implement writable-paths here too as
// writing it in Go instead of shellscript is desirable
// 2.3. mount "ubuntu-data" on a tmpfs, and also mount with nosuid to prevent
// snaps from being able to bypass the sandbox by creating suid root files
// there and try to escape the sandbox
mntOpts := &systemdMountOptions{
Tmpfs: true,
NoSuid: true,
Private: true,
}
err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts)
if err != nil {
return err
}
// Now we can write the snapd mount unit (needed as this is the first boot)
isRunMode := false
rootfsDir := boot.InitramfsWritableDir(model, isRunMode)
snapdSeed := sysSnaps[snap.TypeSnapd]
if err := setupSeedSnapdSnap(rootfsDir, snapdSeed); err != nil {
return err
}
// finally get the gadget snap from the essential snaps and use it to
// configure the ephemeral system
// should only be one seed snap
gadgetSnap := squashfs.New(sysSnaps[snap.TypeGadget].Path)
// we need to configure the ephemeral system with defaults and such using
// from the seed gadget
configOpts := &sysconfig.Options{
// never allow cloud-init to run inside the ephemeral system, in the
// install case we don't want it to ever run, and in the recover case
// cloud-init will already have run in run mode, so things like network
// config and users should already be setup and we will copy those
// further down in the setup for recover mode
AllowCloudInit: false,
TargetRootDir: boot.InitramfsWritableDir(model, isRunMode),
GadgetSnap: gadgetSnap,
}
if err := sysconfig.ConfigureTargetSystem(model, configOpts); err != nil {
return err
}
return nil
}
func generateMountsRecoverOrFactoryReset(mst *initramfsMountsState) (model *asserts.Model, sysSnaps map[snap.Type]*seed.Snap, err error) {
model, snaps, err := generateMountsCommonInstallRecoverStart(mst)
if err != nil {
return nil, nil, err
}
if err := generateMountsCommonInstallRecoverContinue(model, snaps); err != nil {
return nil, nil, err
}
return model, snaps, nil
}
func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, unlockRes secboot.UnlockResult, err error) {
var saveDevice string
if encrypted {
saveKey := device.SaveKeyUnder(dirs.SnapFDEDirUnder(rootdir))
// if ubuntu-save exists and is encrypted, the key has been created during install
if !osutil.FileExists(saveKey) {
// ubuntu-data is encrypted, but we appear to be missing
// a key to open ubuntu-save
return false, unlockRes, fmt.Errorf("cannot find ubuntu-save encryption key at %v", saveKey)
}
// we have save.key, volume exists and is encrypted
key, err := os.ReadFile(saveKey)
if err != nil {
return true, unlockRes, err
}
unlockRes, err = secbootUnlockEncryptedVolumeUsingProtectorKey(disk, "ubuntu-save", key)
if err != nil {
return true, unlockRes, fmt.Errorf("cannot unlock ubuntu-save volume: %v", err)
}
saveDevice = unlockRes.FsDevice
} else {
partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-save")
if err != nil {
if _, ok := err.(disks.PartitionNotFoundError); ok {
// this is ok, ubuntu-save may not exist for
// non-encrypted device
return false, unlockRes, nil
}
return false, unlockRes, err
}
saveDevice = filepath.Join("/dev/disk/by-partuuid", partUUID)
}
if err := doSystemdMount(saveDevice, boot.InitramfsUbuntuSaveDir, mountOpts); err != nil {
return true, unlockRes, err
}
return true, unlockRes, nil
}
func createKernelMounts(runWritableDataDir, kernelName string, rev snap.Revision, isClassic bool) (bool, error) {
driversStandardDir := kernel.DriversTreeDir(runWritableDataDir, kernelName, rev)
// On UC first boot the drivers dir is initially under
// _writable_defaults, so we need to check that directory too. But the
// mount happens after handle-writable-paths has run, so the units
// mounting /lib/{modules,firmware} can use driversStandardDir always.
driversFirstBootDir := kernel.DriversTreeDir(
filepath.Join(runWritableDataDir, "_writable_defaults"), kernelName, rev)
var driversDir string
switch {
case osutil.IsDirectory(driversStandardDir):
driversDir = driversStandardDir
case osutil.IsDirectory(driversFirstBootDir):
driversDir = driversFirstBootDir
default:
logger.Noticef("no drivers tree at %s", driversStandardDir)
return false, nil
}
logger.Noticef("drivers tree found in %s", driversDir)
// 1. Mount unit for the kernel snap
cpi := snap.MinimalSnapContainerPlaceInfo(kernelName, rev)
squashfsPath := filepath.Join(runWritableDataDir, dirs.StripRootDir(cpi.MountFile()))
// snapRoot is where we will find the /snap directory where
// snaps/components will be mounted
// TODO this should use dirs.WritableUbuntuCoreSystemDataDir, but it is
// not possible as the moment due to how release.OnClassic is set
// (would be true if used here).
snapRoot := filepath.Join("sysroot", "writable", "system-data")
if isClassic {
snapRoot = "sysroot"
}
where := filepath.Join(dirs.GlobalRootDir, snapRoot, dirs.StripRootDir(cpi.MountDir()))
if err := writeInitramfsMountUnit(squashfsPath, where, squashfsUnit); err != nil {
return false, err
}
// 2. Mount units for kernel-modules components
if err := createKernelModulesMountUnits(
runWritableDataDir, snapRoot, driversDir, kernelName); err != nil {
return false, err
}
// 3. Mount units for /lib/{modules,firmware}
for _, subDir := range []string{"modules", "firmware"} {
what := filepath.Join(driversStandardDir, "lib", subDir)
where := filepath.Join(dirs.GlobalRootDir, "sysroot", "usr", "lib", subDir)
if err := writeInitramfsMountUnit(what, where, bindUnit); err != nil {
return false, fmt.Errorf("while creating mount for %s in %s: %v",
what, where, err)
}
}
return true, nil
}
func createKernelModulesMountUnits(writableRootDir, snapRoot, driversDir, kernelName string) error {
// Look for symlinks to kernel components. We care only about links to
// content in the squashfs, links to $SNAP_DATA will just work as
// /var/snap will be present before switch root.
// First in modules (we might not have a kernel version subdir if there
// are no kernel modules).
kversion, kver := kernel.KernelVersionFromModulesDir(filepath.Join(driversDir, "lib"))
compSet := map[snap.ComponentSideInfo]bool{}
if kver == nil {
modUpdatesDir := filepath.Join(driversDir, "lib", "modules", kversion, "updates")
if err := getCompsFromSymlinks(modUpdatesDir, kernelName, compSet); err != nil {
return err
}
}
// Then look in firmware
fwUpdatesDir := filepath.Join(driversDir, "lib", "firmware", "updates")
if err := getCompsFromSymlinks(fwUpdatesDir, kernelName, compSet); err != nil {
return err
}
// now create the component units
for comp := range compSet {
cpi := snap.MinimalComponentContainerPlaceInfo(
comp.Component.ComponentName, comp.Revision, kernelName)
squashfsPath := filepath.Join(writableRootDir, dirs.StripRootDir(cpi.MountFile()))
where := filepath.Join(dirs.GlobalRootDir, snapRoot, dirs.StripRootDir(cpi.MountDir()))
if err := writeInitramfsMountUnit(squashfsPath, where, squashfsUnit); err != nil {
return err
}
}
return nil
}
func getCompsFromSymlinks(symLinksDir, kernelName string, compSet map[snap.ComponentSideInfo]bool) error {
entries, err := os.ReadDir(symLinksDir)
if err != nil {
// No updates folder, so there are no kernel-modules comps installed
return nil
}
for _, node := range entries {
if node.Type() != fs.ModeSymlink {
continue
}
// Note that symlinks in drivers tree are absolute
dest, err := os.Readlink(filepath.Join(symLinksDir, node.Name()))
if err != nil {
return err
}
// find out component name from symlink
prefix := filepath.Join(snap.ComponentsBaseDir(kernelName), "mnt")
subdir := strings.TrimPrefix(dest, prefix+string(os.PathSeparator))
if subdir == dest {
// Possibly points to $SNAP_DATA instead of to $SNAP,
// or is a relative symlink to some fw file in the
// component.
continue
}
dirs := strings.Split(subdir, string(os.PathSeparator))
// dirs should still have as a minimum 4 elements
// <comp_name>/<comp_rev>/{modules/<kversion>,firmware/<filename>}
if len(dirs) < 4 {
logger.Noticef("warning: %s seems to be badly formed", dest)
continue
}
rev, err := snap.ParseRevision(dirs[1])
if err != nil {
logger.Noticef("warning: wrong revision in symlink %s: %v", dest, err)
continue
}
csi := snap.NewComponentSideInfo(naming.NewComponentRef(kernelName, dirs[0]), rev)
compSet[*csi] = true
}
return nil
}
func recalculateRootfsTarget() error {
// Do a daemon reload so systemd knows about the new sysroot mount unit
// (populate-writable.service depends on sysroot.mount, we need to make
// sure systemd knows this unit before snap-initramfs-mounts.service
// finishes) and about the drivers tree mounts (relevant on hybrid).
sysd := systemd.New(systemd.SystemMode, nil)
if err := sysd.DaemonReload(); err != nil {
return err
}
// We need to restart initrd-root-fs.target so its dependencies are
// re-calculated considering the new sysroot.mount unit. See
// https://github.com/systemd/systemd/issues/23034 on why this is
// needed.
return sysd.StartNoBlock([]string{"initrd-root-fs.target"})
}
func generateMountsModeRun(mst *initramfsMountsState) error {
bootMountOpts := &systemdMountOptions{
// always fsck the partition when we are mounting it, as this is the
// first partition we will be mounting, we can't know if anything is
// corrupted yet
NeedsFsck: true,
Private: true,
}
// 1. mount ubuntu-boot
if err := mountNonDataPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot", bootMountOpts); err != nil {
return err
}
// get the disk that we mounted the ubuntu-boot partition from as a
// reference point for future mounts
disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuBootDir, nil)
if err != nil {
return err
}
// 1.1. measure model
err = stampedAction("run-model-measured", func() error {
return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel)
})
if err != nil {
return err
}
// XXX: I wonder if secbootMeasureSnapModelWhenPossible()
// should return the model so that we don't need to run
// mst.UnverifiedBootModel() again
model, err := mst.UnverifiedBootModel()
if err != nil {
return err
}
isClassic := model.Classic()
if model.Classic() {
logger.Noticef("generating mounts for classic system, run mode")
} else {
logger.Noticef("generating mounts for Ubuntu Core system, run mode")
}
isRunMode := true
// 2. mount ubuntu-seed (optional for classic)
seedMountOpts := &systemdMountOptions{
NeedsFsck: true,
Private: true,
NoSuid: true,
NoDev: true,
NoExec: true,
}
// use the disk we mounted ubuntu-boot from as a reference to find
// ubuntu-seed and mount it
hasSeedPart := true
partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-seed")
if err != nil {
if isClassic {
// If there is no ubuntu-seed on classic, that's fine
if _, ok := err.(disks.PartitionNotFoundError); !ok {
return err
}
hasSeedPart = false
} else {
return err
}
}
// fsck is safe to run on ubuntu-seed as per the manpage, it should not
// meaningfully contribute to corruption if we fsck it every time we boot,
// and it is important to fsck it because it is vfat and mounted writable
// TODO:UC20: mount it as read-only here and remount as writable when we
// need it to be writable for i.e. transitioning to recover mode
if partUUID != "" {
if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID),
boot.InitramfsUbuntuSeedDir, seedMountOpts); err != nil {
return err
}
}
// 2.1 Update bootloader variables now that boot/seed are mounted
if err := boot.InitramfsRunModeUpdateBootloaderVars(); err != nil {
return err
}
diskState := &diskUnlockState{}
// at this point on a system with TPM-based encryption
// data can be open only if the measured model matches the actual
// run model.
// TODO:UC20: on ARM systems and no TPM with encryption
// we need other ways to make sure that the disk is opened
// and we continue booting only for expected models
// 3.1. mount Data
runModeKey := device.DataSealedKeyUnder(boot.InitramfsBootEncryptionKeyDir)
opts := &secboot.UnlockVolumeUsingSealedKeyOptions{
AllowRecoveryKey: true,
WhichModel: mst.UnverifiedBootModel,
BootMode: mst.mode,
}
unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts)
if err != nil {
return err
}
diskState.setUnlockStateWithRunKey("ubuntu-data", unlockRes, nil)
// TODO: do we actually need fsck if we are mounting a mapper device?
// probably not?
dataMountOpts := systemdMountOptions{
NeedsFsck: true,
}
if !isClassic {
dataMountOpts = setUbuntuCoreDataMountOptions(dataMountOpts)
}
if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, &dataMountOpts); err != nil {
return err
}
isEncryptedDev := unlockRes.IsEncrypted
// at this point data was opened so we can consider the model okay
mst.SetVerifiedBootModel(model)
rootfsDir := boot.InitramfsWritableDir(model, isRunMode)
// 3.2. mount ubuntu-save (if present)
saveMountOpts := &systemdMountOptions{
NeedsFsck: true,
Private: true,
NoDev: true,
NoSuid: true,
NoExec: true,
}
haveSave, saveUnlockRes, err := maybeMountSave(disk, rootfsDir, isEncryptedDev, saveMountOpts)
if err != nil {
return err
}
// 4.1 verify that ubuntu-data comes from where we expect it to
diskOpts := &disks.Options{}
if unlockRes.IsEncrypted {
// then we need to specify that the data mountpoint is expected to be a
// decrypted device, applies to both ubuntu-data and ubuntu-save
diskOpts.IsDecryptedDevice = true
}
matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts)
if err != nil {
return err
}
if !matches {
// failed to verify that ubuntu-data mountpoint comes from the same disk
// as ubuntu-boot
return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev())
}
if haveSave {
diskState.setUnlockStateWithRunKey("ubuntu-save", saveUnlockRes, nil)
// 4.1a we have ubuntu-save, verify it as well
matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts)
if err != nil {
return err
}
if !matches {
return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev())
}
if isEncryptedDev {
// in run mode the path to open an encrypted save is for
// data to be encrypted and the save key in it
// to be successfully used. This already should stop
// allowing to chose ubuntu-data to try to access
// save. as safety boot also stops if the keys cannot
// be locked.
// for symmetry with recover code and extra paranoia
// though also check that the markers match.
paired, err := checkDataAndSavePairing(rootfsDir)
if err != nil {
return err
}
if !paired {
return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install")
}
}
}
// All the required disks were unlocked. We now write down
// their unlock state.
diskState.serializeTo(boot.UnlockedStateFileName)
// 4.2. read modeenv
modeEnv, err := boot.ReadModeenv(rootfsDir)
if err != nil {
return err
}
// order in the list must not change as it determines the mount order
typs := []snap.Type{snap.TypeGadget, snap.TypeKernel}
if !isClassic {
typs = append([]snap.Type{snap.TypeBase}, typs...)
}
// 4.2 choose base, gadget and kernel snaps (this includes updating
// modeenv if needed to try the base snap)
mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv, rootfsDir)
if err != nil {
return err
}
// TODO:UC20: with grade > dangerous, verify the kernel snap hash against
// what we booted using the tpm log, this may need to be passed
// to the function above to make decisions there, or perhaps this
// code actually belongs in the bootloader implementation itself
typesToMount := typs
if createSysrootMount() {
// Create unit for sysroot (mounts either base or rootfs). We
// restrict this to UC24+ for the moment, until we backport necessary
// changes to the UC20/22 initramfs. Note that a transient unit is
// not used as it tries to be restarted after the switch root, and
// fails.
typesToMount = []snap.Type{snap.TypeGadget, snap.TypeKernel}
if isClassic {
if err := writeSysrootMountUnit(rootfsDir, ""); err != nil {
return fmt.Errorf("cannot write sysroot.mount (what: %s): %v", rootfsDir, err)
}
} else {
basePlaceInfo := mounts[snap.TypeBase]
what := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), basePlaceInfo.Filename())
if err := writeSysrootMountUnit(what, "squashfs"); err != nil {
return fmt.Errorf("cannot write sysroot.mount (what: %s): %v", what, err)
}
}
}
// Create mounts for kernel modules/firmware if we have a drivers tree.
// InitramfsRunModeSelectSnapsToMount guarantees we do have a kernel in the map.
kernPlaceInfo := mounts[snap.TypeKernel]
hasDriversTree, err := createKernelMounts(
rootfsDir, kernPlaceInfo.SnapName(), kernPlaceInfo.SnapRevision(), isClassic)
if err != nil {
return err
}
// 4.3 mount the gadget snap and, if there is no drivers tree, the kernel snap
for _, typ := range typesToMount {
if typ == snap.TypeKernel && hasDriversTree {
continue
}
sn, ok := mounts[typ]
if !ok {
continue
}
dir := snapTypeToMountDir[typ]
snapPath := filepath.Join(dirs.SnapBlobDirUnder(rootfsDir), sn.Filename())
snapMntPt := filepath.Join(boot.InitramfsRunMntDir, dir)
if err := doSystemdMount(snapPath, snapMntPt, mountReadOnlyOptions); err != nil {
return err
}
// On 24+ kernels, create /lib/{firmware,modules} mounts if
// there was no drivers tree. This is a fallback for not really
// supported but supported cases like having a 24+ kernel with
// a <24 model. For older initramfs this is done by a
// generator. Note also that for UC this is done by the
// extra-paths script, so we need this only for classic.
if typ == snap.TypeKernel && isClassic && createSysrootMount() {
logger.Noticef("warning: expected drivers tree not found, mounting /lib/{firmware,modules} directly from kernel snap")
for _, subDir := range []string{"modules", "firmware"} {
what := filepath.Join(snapMntPt, subDir)
where := filepath.Join(dirs.GlobalRootDir, "sysroot", "usr", "lib", subDir)
if err := writeInitramfsMountUnit(what, where, bindUnit); err != nil {
return fmt.Errorf("while creating mount for %s in %s: %v",
what, where, err)
}
}
}
}
// 4.4 check if we expected a ubuntu-seed partition from the gadget data
if isClassic {
gadgetDir := filepath.Join(boot.InitramfsRunMntDir, snapTypeToMountDir[snap.TypeGadget])
foundRole, err := gadget.HasRole(gadgetDir, []string{gadget.SystemSeed, gadget.SystemSeedNull})
if err != nil {
return err
}
seedDefinedInGadget := foundRole != ""
if hasSeedPart && !seedDefinedInGadget {
return fmt.Errorf("ubuntu-seed partition found but not defined in the gadget")
}
if !hasSeedPart && seedDefinedInGadget {
return fmt.Errorf("ubuntu-seed partition not found but defined in the gadget (%s)", foundRole)
}
}
// 4.5 mount snapd snap only on first boot
if modeEnv.RecoverySystem != "" && !isClassic {
// load the recovery system and generate mount for snapd
theSeed, err := mst.LoadSeed(modeEnv.RecoverySystem)
if err != nil {
return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err)
}
perf := timings.New(nil)
if err := theSeed.LoadEssentialMeta([]snap.Type{snap.TypeSnapd}, perf); err != nil {
return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err)
}
snapdSeed := theSeed.EssentialSnaps()[0]
if err := setupSeedSnapdSnap(rootfsDir, snapdSeed); err != nil {
return err
}
}
if createSysrootMount() {
if err := recalculateRootfsTarget(); err != nil {
return err
}
}
return nil
}
// setupSeedSnapdSnap makes sure that snapd from the snap is ready to be used
// after switch root when starting from a UC seed.
func setupSeedSnapdSnap(rootfsDir string, snapdSeedSnap *seed.Snap) error {
// We need to replicate the mount unit that snapd would create, but
// differently to other mounts we have to do here we do not need to
// start it from the initramfs. As this is first boot, do it in
// _writable_defaults to make sure we do not prevent files already
// there to be copied.
si := snapdSeedSnap.SideInfo
// Comes from the seed and it might be unasserted, set revision in that case
if si.Revision.Unset() {
si.Revision = snap.R(-1)
}
cpi := snap.MinimalSnapContainerPlaceInfo(si.RealName, si.Revision)
destRoot := sysconfig.WritableDefaultsDir(rootfsDir)
logger.Debugf("writing %s mount unit to %s", si.RealName, destRoot)
if err := writeSnapMountUnit(destRoot, snapdSeedSnap.Path, cpi.MountDir(),
systemd.RegularMountUnit, cpi.MountDescription()); err != nil {
return fmt.Errorf("while writing %s first boot mount unit: %v", si.RealName, err)
}
// We need to initialize /snap/snapd/current symlink so that the
// dynamic linker
// /snap/snapd/current/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 is
// available to run snapd on first boot.
mountDir := filepath.Join(rootfsDir, dirs.StripRootDir(dirs.SnapMountDir), si.RealName)
if err := os.MkdirAll(mountDir, 0755); err != nil {
return err
}
return osutil.AtomicSymlink(si.Revision.String(), filepath.Join(mountDir, "current"))
}
var tryRecoverySystemHealthCheck = func(model gadget.Model) error {
// check that writable is accessible by checking whether the
// state file exists
if !osutil.FileExists(dirs.SnapStateFileUnder(boot.InitramfsHostWritableDir(model))) {
return fmt.Errorf("host state file is not accessible")
}
return nil
}
func finalizeTryRecoverySystemAndReboot(model gadget.Model, outcome boot.TryRecoverySystemOutcome) (err error) {
// from this point on, we must finish with a system reboot
defer func() {
if rebootErr := boot.InitramfsReboot(); rebootErr != nil {
if err != nil {
err = fmt.Errorf("%v (cannot reboot to run system: %v)", err, rebootErr)
} else {
err = fmt.Errorf("cannot reboot to run system: %v", rebootErr)
}
}
// not reached, unless in tests
panic(fmt.Errorf("finalize try recovery system did not reboot, last error: %v", err))
}()
if outcome == boot.TryRecoverySystemOutcomeSuccess {
if err := tryRecoverySystemHealthCheck(model); err != nil {
// health checks failed, the recovery system is considered
// unsuccessful
outcome = boot.TryRecoverySystemOutcomeFailure
logger.Noticef("try recovery system health check failed: %v", err)
}
}
// that's it, we've tried booting a new recovery system to this point,
// whether things are looking good or bad we will reboot back to run
// mode and update the boot variables accordingly
if err := boot.EnsureNextBootToRunModeWithTryRecoverySystemOutcome(outcome); err != nil {
logger.Noticef("cannot update the try recovery system state: %v", err)
return fmt.Errorf("cannot mark recovery system successful: %v", err)
}
return nil
}
|