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