github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/boot/esxi/esxi.go (about) 1 // Copyright 2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package esxi contains an ESXi boot config parser for disks and CDROMs. 6 // 7 // For CDROMs, it parses the boot.cfg found in the root directory and tries to 8 // boot from it. 9 // 10 // For disks, there may be multiple boot partitions: 11 // 12 // - Locates both <device>5/boot.cfg and <device>6/boot.cfg. 13 // 14 // - If parsable, chooses partition with bootstate=(0|2|empty) and greater 15 // updated=N. 16 // 17 // Sometimes, an ESXi partition can contain a valid boot.cfg, but not actually 18 // any of the named modules. Hence it is important to try fully loading ESXi 19 // into memory, and only then falling back to the other partition. 20 // 21 // Only boots partitions with bootstate=0, bootstate=2, bootstate=(empty) will 22 // boot at all. 23 // 24 // Most of the parsing logic in this package comes from 25 // https://github.com/vmware/esx-boot/blob/master/safeboot/bootbank.c 26 package esxi 27 28 import ( 29 "bufio" 30 "encoding/hex" 31 "fmt" 32 "io" 33 "os" 34 "path/filepath" 35 "strconv" 36 "strings" 37 "unicode" 38 39 "golang.org/x/sys/unix" 40 41 "github.com/mvdan/u-root-coreutils/pkg/boot" 42 "github.com/mvdan/u-root-coreutils/pkg/boot/multiboot" 43 "github.com/mvdan/u-root-coreutils/pkg/mount" 44 "github.com/mvdan/u-root-coreutils/pkg/mount/gpt" 45 "github.com/mvdan/u-root-coreutils/pkg/uio" 46 ) 47 48 func partNo(device string, number int) (string, error) { 49 var name string 50 if unicode.IsDigit(rune(device[len(device)-1])) { 51 name = fmt.Sprintf("%sp%d", device, number) 52 } else { 53 name = fmt.Sprintf("%s%d", device, number) 54 } 55 if _, err := os.Stat(name); err != nil { 56 return "", err 57 } 58 return name, nil 59 } 60 61 // LoadDisk loads the right ESXi multiboot kernel from partitions 5 or 6 of the 62 // given device. 63 // 64 // The kernels are returned in the priority order according to the bootstate 65 // and updated values in their boot configurations. 66 // 67 // The caller should try loading all returned images in order, as some of them 68 // may not be valid. 69 // 70 // device5 and device6 will be mounted at temporary directories. 71 func LoadDisk(device string) ([]*boot.MultibootImage, []*mount.MountPoint, error) { 72 opts5, mp5, err5 := mountPartition(device, 5) 73 opts6, mp6, err6 := mountPartition(device, 6) 74 if err5 != nil && err6 != nil { 75 return nil, nil, fmt.Errorf("could not mount or read either partition 5 (%v) or partition 6 (%v)", err5, err6) 76 } 77 var mps []*mount.MountPoint 78 if mp5 != nil { 79 mps = append(mps, mp5) 80 } 81 if mp6 != nil { 82 mps = append(mps, mp6) 83 } 84 85 imgs, err := getImages(device, opts5, opts6) 86 if err != nil { 87 for _, mp := range mps { 88 mp.Unmount(mount.MNT_DETACH) 89 } 90 return nil, nil, err 91 } 92 return imgs, mps, nil 93 } 94 95 func getImages(device string, opts5, opts6 *options) ([]*boot.MultibootImage, error) { 96 var ( 97 img5, img6 *boot.MultibootImage 98 err5, err6 error 99 ) 100 if opts5 != nil { 101 name, _ := partNo(device, 5) 102 img5, err5 = getBootImage(*opts5, device, 5, name) 103 } 104 if opts6 != nil { 105 name, _ := partNo(device, 6) 106 img6, err6 = getBootImage(*opts6, device, 6, name) 107 } 108 if img5 == nil && img6 == nil { 109 return nil, fmt.Errorf("could not read boot configs on partition 5 (%v) or partition 6 (%v)", err5, err6) 110 } 111 112 if img5 != nil && img6 != nil { 113 if opts6.updated > opts5.updated { 114 return []*boot.MultibootImage{img6, img5}, nil 115 } 116 return []*boot.MultibootImage{img5, img6}, nil 117 } else if img5 != nil { 118 return []*boot.MultibootImage{img5}, nil 119 } 120 return []*boot.MultibootImage{img6}, nil 121 } 122 123 // LoadCDROM loads an ESXi multiboot kernel from a CDROM at device. 124 // 125 // device will be mounted at mountPoint. 126 func LoadCDROM(device string) (*boot.MultibootImage, *mount.MountPoint, error) { 127 mountPoint, err := os.MkdirTemp("", "esxi-mount-") 128 if err != nil { 129 return nil, nil, err 130 } 131 mp, err := mount.Mount(device, mountPoint, "iso9660", "", unix.MS_RDONLY|unix.MS_NOATIME) 132 if err != nil { 133 os.RemoveAll(mountPoint) 134 return nil, nil, err 135 } 136 137 opts, err := parse(filepath.Join(mountPoint, "boot.cfg")) 138 if err != nil { 139 mp.Unmount(mount.MNT_DETACH) 140 os.RemoveAll(mountPoint) 141 return nil, nil, fmt.Errorf("cannot parse config from %s: %v", device, err) 142 } 143 img, err := getBootImage(opts, "", 0, device) 144 if err != nil { 145 mp.Unmount(mount.MNT_DETACH) 146 os.RemoveAll(mountPoint) 147 return nil, nil, err 148 } 149 return img, mp, nil 150 } 151 152 // LoadConfig loads an ESXi configuration from configFile. 153 func LoadConfig(configFile string) (*boot.MultibootImage, error) { 154 opts, err := parse(configFile) 155 if err != nil { 156 return nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err) 157 } 158 return getBootImage(opts, "", 0, fmt.Sprintf("config file %s", configFile)) 159 } 160 161 func mountPartition(parentdev string, partition int) (*options, *mount.MountPoint, error) { 162 dev, err := partNo(parentdev, partition) 163 if err != nil { 164 return nil, nil, err 165 } 166 base := filepath.Base(dev) 167 mountPoint, err := os.MkdirTemp("", fmt.Sprintf("%s-", base)) 168 if err != nil { 169 return nil, nil, err 170 } 171 mp, err := mount.Mount(dev, mountPoint, "vfat", "", unix.MS_RDONLY|unix.MS_NOATIME) 172 if err != nil { 173 os.RemoveAll(mountPoint) 174 return nil, nil, err 175 } 176 177 configFile := filepath.Join(mountPoint, "boot.cfg") 178 opts, err := parse(configFile) 179 if err != nil { 180 mp.Unmount(mount.MNT_DETACH) 181 os.RemoveAll(mountPoint) 182 return nil, nil, fmt.Errorf("cannot parse config at %s: %v", configFile, err) 183 } 184 return &opts, mp, nil 185 } 186 187 // lazyOpenModules assigns modules to be opened as files. 188 // 189 // Each module is a path followed by optional command-line arguments, e.g. 190 // []string{"./module arg1 arg2", "./module2 arg3 arg4"}. 191 func lazyOpenModules(mods []module) multiboot.Modules { 192 modules := make([]multiboot.Module, 0, len(mods)) 193 for _, m := range mods { 194 modules = append(modules, multiboot.Module{ 195 Cmdline: m.cmdline, 196 Module: uio.NewLazyFile(m.path), 197 }) 198 } 199 return modules 200 } 201 202 func getBootImage(opts options, device string, partition int, name string) (*boot.MultibootImage, error) { 203 // Only valid and upgrading are bootable partitions. 204 // 205 // We are supposed to support the following two state transitions (only 206 // one transition every boot!): 207 // 208 // upgrading -> dirty 209 // dirty -> invalid 210 // 211 // A validly booted system will set its own bootstate to "valid" from 212 // "dirty". 213 // 214 // We currently don't support writing the state back to disk, which is 215 // fine in our manual testing. 216 if opts.bootstate != bootValid && opts.bootstate != bootUpgrading { 217 return nil, fmt.Errorf("boot state %d invalid", opts.bootstate) 218 } 219 220 if len(device) > 0 { 221 if err := opts.addUUID(device, partition); err != nil { 222 return nil, fmt.Errorf("cannot add boot uuid of %s: %v", device, err) 223 } 224 } 225 226 return &boot.MultibootImage{ 227 Name: fmt.Sprintf("%s from %s", opts.title, name), 228 Kernel: uio.NewLazyFile(opts.kernel), 229 Cmdline: opts.args, 230 Modules: lazyOpenModules(opts.modules), 231 }, nil 232 } 233 234 type module struct { 235 path string 236 cmdline string 237 } 238 239 type options struct { 240 title string 241 kernel string 242 args string 243 modules []module 244 updated int 245 bootstate bootstate 246 } 247 248 type bootstate int 249 250 // From safeboot.c 251 const ( 252 bootValid bootstate = 0 253 bootUpgrading bootstate = 1 254 bootDirty bootstate = 2 255 bootInvalid bootstate = 3 256 ) 257 258 // So tests can replace this and don't have to have actual block devices. 259 var getBlockSize = gpt.GetBlockSize 260 261 func getUUID(device string, partition int) (string, error) { 262 device = strings.TrimRight(device, "/") 263 blockSize, err := getBlockSize(device) 264 if err != nil { 265 return "", err 266 } 267 268 dev, err := partNo(device, partition) 269 if err != nil { 270 return "", err 271 } 272 273 f, err := os.Open(dev) 274 if err != nil { 275 return "", err 276 } 277 278 // Boot uuid is stored in the second block of the disk 279 // in the following format: 280 // 281 // VMWARE FAT16 <uuid> 282 // <---128 bit----><128 bit> 283 data := make([]byte, uuidSize) 284 n, err := f.ReadAt(data, int64(blockSize)) 285 if err != nil { 286 return "", err 287 } 288 if n != uuidSize { 289 return "", io.ErrUnexpectedEOF 290 } 291 292 if magic := string(data[:len(uuidMagic)]); magic != uuidMagic { 293 return "", fmt.Errorf("bad uuid magic %q, want %q", magic, uuidMagic) 294 } 295 296 uuid := hex.EncodeToString(data[len(uuidMagic):]) 297 return fmt.Sprintf("bootUUID=%s", uuid), nil 298 } 299 300 func (o *options) addUUID(device string, partition int) error { 301 uuid, err := getUUID(device, partition) 302 if err != nil { 303 return err 304 } 305 o.args += " " + uuid 306 return nil 307 } 308 309 const ( 310 comment = '#' 311 sep = "---" 312 313 uuidMagic = "VMWARE FAT16 " 314 uuidSize = 32 315 ) 316 317 func parse(configFile string) (options, error) { 318 dir := filepath.Dir(configFile) 319 320 f, err := os.Open(configFile) 321 if err != nil { 322 return options{}, err 323 } 324 defer f.Close() 325 326 // An empty or missing updated value is always 0, so we can let the 327 // ints be initialized to 0. 328 // 329 // see esx-boot/bootlib/parse.c:parse_config_file. 330 opt := options{ 331 title: "VMware ESXi", 332 // Default value taken from 333 // esx-boot/safeboot/bootbank.c:bank_scan. 334 bootstate: bootInvalid, 335 } 336 337 scanner := bufio.NewScanner(f) 338 for scanner.Scan() { 339 line := scanner.Text() 340 line = strings.TrimSpace(line) 341 342 if len(line) == 0 || line[0] == comment { 343 continue 344 } 345 346 tokens := strings.SplitN(line, "=", 2) 347 if len(tokens) != 2 { 348 return opt, fmt.Errorf("bad line %q", line) 349 } 350 key := strings.TrimSpace(tokens[0]) 351 val := strings.TrimSpace(tokens[1]) 352 switch key { 353 case "title": 354 opt.title = val 355 356 case "kernel": 357 opt.kernel = filepath.Join(dir, val) 358 359 // The kernel cmdline is expected to have the filename 360 // first, as in cmdlines[0] here: 361 // https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L870 362 // 363 // Note that the kernel is module 0 in the esx-boot 364 // code base, but it doesn't get loaded like that into 365 // the info structure; see -- so don't panic like I did 366 // when you read that! 367 // https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L578 368 opt.args = val + " " + opt.args 369 370 case "kernelopt": 371 opt.args += val 372 373 case "updated": 374 if len(val) == 0 { 375 // Explicitly setting to 0, as in 376 // esx-boot/bootlib/parse.c:parse_config_file, 377 // in case this value is specified twice. 378 opt.updated = 0 379 } else { 380 n, err := strconv.Atoi(val) 381 if err != nil { 382 return options{}, err 383 } 384 opt.updated = n 385 } 386 case "bootstate": 387 if len(val) == 0 { 388 // Explicitly setting to valid, as in 389 // esx-boot/bootlib/parse.c:parse_config_file, 390 // in case this value is specified twice. 391 opt.bootstate = bootValid 392 } else { 393 n, err := strconv.Atoi(val) 394 if err != nil { 395 return options{}, err 396 } 397 if n < 0 || n > 3 { 398 opt.bootstate = bootInvalid 399 } else { 400 opt.bootstate = bootstate(n) 401 } 402 } 403 case "modules": 404 for _, tok := range strings.Split(val, sep) { 405 // Each module is "filename arg0 arg1 arg2" and 406 // the filename is relative to the directory 407 // the module is in. 408 tok = strings.TrimSpace(tok) 409 if len(tok) > 0 { 410 entry := strings.Fields(tok) 411 opt.modules = append(opt.modules, module{ 412 path: filepath.Join(dir, entry[0]), 413 cmdline: tok, 414 }) 415 } 416 } 417 } 418 } 419 420 err = scanner.Err() 421 return opt, err 422 }