You must login to view /gokrazy/tools/src/commit/b8fc58bd9f3fc939026255c3fdb0cf446aa5b562/go.mod.
The GitHub option should be usable for most people, it only links via username.

Files
tools/packer/packer.go
Olivier Mengué 772c0a9421 packer: move call to unix.Sync behind Linux conditional compile (#84)
In RereadPartitions, move call to unix.Sync() in Linux specific code
as unix.Sync() is not portable (not available on Windows).
2025-02-21 20:39:03 +01:00

470 lines
13 KiB
Go

// Package packer is a gokrazy-internal package which provides functionality
// shared between gokr-packer and rtr7-recovery-init.
package packer
import (
"bytes"
"encoding/binary"
"fmt"
"hash/crc32"
"hash/fnv"
"io"
"log"
"os"
"unicode/utf16"
)
// Pack represents one pack process.
type Pack struct {
Partuuid uint32
UsePartuuid bool
UseGPTPartuuid bool
UseGPT bool
ExistingEEPROM struct {
PieepromSHA256 string // pieeprom.sig
VL805SHA256 string // vl805.sig
}
FirstPartitionOffsetSectors int64
}
func NewPackForHost(firstPartitionOffsetSectors int64, hostname string) Pack {
h := fnv.New32a()
h.Write([]byte(hostname))
return Pack{
Partuuid: h.Sum32(),
UsePartuuid: true,
UseGPTPartuuid: true,
UseGPT: true,
FirstPartitionOffsetSectors: firstPartitionOffsetSectors,
}
}
// ModifyCmdlineRoot() returns true if the -kernel_pkgs cmdline.txt file needs
// any modifications. This will be true on most gokrazy installations.
func (p *Pack) ModifyCmdlineRoot() bool {
return p.UsePartuuid || p.UseGPTPartuuid
}
// GPTPARTUUID derives a GPT partition GUID for the specified partition.
//
// All gokrazy GPT partition GUIDs start with the same prefix and contain the
// hostname-derived hash + the partition number in the last block (“node
// identifier”).
func (p *Pack) GPTPARTUUID(partition uint16) string {
const gokrazyGUIDPrefix = "60c24cc1-f3f9-427a-8199"
return fmt.Sprintf("%s-%08x00%02x",
gokrazyGUIDPrefix,
p.Partuuid,
partition)
}
func (p *Pack) Root() string {
if p.UseGPTPartuuid {
return fmt.Sprintf("PARTUUID=%s/PARTNROFF=1", p.GPTPARTUUID(1))
}
if p.UsePartuuid {
return fmt.Sprintf("PARTUUID=%08x-02", p.Partuuid)
}
return "" // should only be called if ModifyCmdlineRoot()
}
func (p *Pack) PermUUID() string {
if p.UseGPTPartuuid {
return p.GPTPARTUUID(4)
}
if p.UsePartuuid {
return fmt.Sprintf("%08x-04", p.Partuuid)
}
return "" // should only be called if ModifyCmdlineRoot()
}
var (
active = byte(0x80)
inactive = byte(0x00)
// invalidCHS results in using the sector values instead
invalidCHS = [3]byte{0xFE, 0xFF, 0xFF}
FAT = byte(0xc)
Linux = byte(0x83)
SquashFS = Linux // SquashFS does not have a dedicated type
signature = uint16(0xAA55)
)
const MB = 1024 * 1024
func permSize(firstPartitionOffsetSectors int64, devsize uint64) uint32 {
permStart := uint32(firstPartitionOffsetSectors + (1100 * MB / 512))
permSize := uint32((devsize / 512) - uint64(firstPartitionOffsetSectors) - (1100 * MB / 512))
// LBA -33 to LBA -1 need to remain unused for the secondary GPT header
lastAddressable := uint32((devsize / 512) - 1) // 0-indexed
if lastLBA := uint32(lastAddressable - 33); permStart+permSize >= lastLBA {
permSize -= (permStart + permSize) - lastLBA
}
return permSize
}
func PermSizeInKB(firstPartitionOffsetSectors int64, devsize uint64) uint32 {
permSizeLBA := permSize(firstPartitionOffsetSectors, devsize)
permSizeBytes := permSizeLBA * 512
return permSizeBytes / 1024
}
// writePartitionTable writes a Hybrid MBR: it contains the GPT protective
// partition so that the Linux kernel recognizes the disk as GPT, but it also
// contains the FAT32 partition so that the Raspberry Pi bootloader still works.
func writePartitionTable(firstPartitionOffsetSectors int64, w io.Writer) error {
for _, v := range []interface{}{
[446]byte{}, // boot code
// Partition 1 must be present for the Raspberry Pi bootloader
active,
invalidCHS,
FAT,
invalidCHS,
uint32(firstPartitionOffsetSectors),
uint32(100 * MB / 512), // 100MB in size
// Partition 2 is the protective GPT partition so that the Linux kernel
// will recognize the disk as GPT.
inactive,
invalidCHS,
byte(0xEE),
invalidCHS,
uint32(1),
uint32(firstPartitionOffsetSectors - 1),
[16]byte{}, // partition 3
[16]byte{}, // partition 4
signature,
} {
if err := binary.Write(w, binary.LittleEndian, v); err != nil {
return err
}
}
return nil
}
// writeMBRPartitionTable writes an MBR-only partition table. This is useful
// when the device requires blobs to be present in disk sectors otherwise occupied
// by GPT metadata. For example, Odroid HC2 clobbers sectors 1-2046 with binary blobs
// required for booting - these devices are incompatible with GPT. See
// https://wiki.odroid.com/odroid-xu4/software/partition_table#ubuntu_partition_table.
func writeMBRPartitionTable(firstPartitionOffsetSectors int64, w io.Writer, devsize uint64) error {
for _, v := range []interface{}{
[446]byte{}, // boot code
// Partition 1 must be present for the Raspberry Pi bootloader
active,
invalidCHS,
FAT,
invalidCHS,
uint32(firstPartitionOffsetSectors),
uint32(100 * MB / 512), // 100MB in size
// Partition 2 is squash partition 1.
inactive,
invalidCHS,
Linux,
invalidCHS,
uint32(firstPartitionOffsetSectors + 100*MB/512),
uint32(500 * MB / 512),
// Partition 3 is squash partition 2.
inactive,
invalidCHS,
Linux,
invalidCHS,
uint32(firstPartitionOffsetSectors + 600*MB/512),
uint32(500 * MB / 512),
// Partition 4 is the perm partition.
inactive,
invalidCHS,
Linux,
invalidCHS,
uint32(firstPartitionOffsetSectors + 1100*MB/512),
uint32(devsize/512 - uint64(firstPartitionOffsetSectors) - 1100*MB/512),
signature,
} {
if err := binary.Write(w, binary.LittleEndian, v); err != nil {
return err
}
}
return nil
}
func mustParseGUID(guid string) [16]byte {
// See Intel EFI specification, Appendix A: GUID and Time Formats
// https://www.intel.de/content/dam/doc/product-specification/efi-v1-10-specification.pdf
var (
timeLow uint32
timeMid uint16
timeHighAndVersion uint16
clockSeqHighAndReserved uint8
clockSeqLow uint8
node []byte
)
_, err := fmt.Sscanf(guid,
"%08x-%04x-%04x-%02x%02x-%012x",
&timeLow,
&timeMid,
&timeHighAndVersion,
&clockSeqHighAndReserved,
&clockSeqLow,
&node)
if err != nil {
panic(err)
}
var result [16]byte
binary.LittleEndian.PutUint32(result[0:4], timeLow)
binary.LittleEndian.PutUint16(result[4:6], timeMid)
binary.LittleEndian.PutUint16(result[6:8], timeHighAndVersion)
result[8] = clockSeqHighAndReserved
result[9] = clockSeqLow
copy(result[10:], node)
return result
}
func partitionName(name string) [72]byte {
// adapted from https://github.com/diskfs/go-diskfs/blob/8a6b8b88d14a164cb914108d3cd829d9a67595a0/partition/gpt/partiton.go#L67
// now the partition name - it is UTF16LE encoded, max 36 code units for 72 bytes
r := make([]rune, 0, len(name))
// first convert to runes
for _, s := range name {
r = append(r, rune(s))
}
if len(r) > 36 {
panic(fmt.Sprintf("Cannot use %s as partition name, has %d Unicode code units, maximum size is 36", name, len(r)))
}
// next convert the runes to uint16
nameb := utf16.Encode(r)
// and then convert to little-endian bytes
var result [72]byte
for i, u := range nameb {
pos := i * 2
binary.LittleEndian.PutUint16(result[pos:pos+2], u)
}
return result
}
func (p *Pack) writeGPT(w io.Writer, devsize uint64, primary bool) error {
const (
partitionTypeEFISystemPartition = "C12A7328-F81F-11D2-BA4B-00A0C93EC93B"
partitionTypeLinuxFilesystemData = "0FC63DAF-8483-4772-8E79-3D69D8477DE4"
partitionTypeLinuxRootPartitionAMD64 = "4F68BCE3-E8CD-4DB1-96E7-FBCAF984B709"
partitionTypeLinuxRootPartitionARM64 = "B921B045-1DF0-41C3-AF44-4C6F280D3FAE"
partitionTypeLinuxRootPartitionX86 = "44479540-f297-41b2-9af7-d131d5f0458a"
)
type partitionEntry struct {
TypeGUID [16]byte
GUID [16]byte
FirstLBA uint64
LastLBA uint64
Attributes uint64
Name [72]byte
}
partition0First := uint64(p.FirstPartitionOffsetSectors)
partition0Last := partition0First + (100 * MB / 512) - 1
partition1First := partition0Last + 1
partition1Last := partition1First + (500 * MB / 512) - 1
partition2First := partition1Last + 1
partition2Last := partition2First + (500 * MB / 512) - 1
partition3First := partition2Last + 1
partition3Last := partition3First + uint64(permSize(p.FirstPartitionOffsetSectors, devsize)) - 1
var rootType [16]byte
switch os.Getenv("GOARCH") {
case "386":
rootType = mustParseGUID(partitionTypeLinuxRootPartitionX86)
case "amd64":
rootType = mustParseGUID(partitionTypeLinuxRootPartitionAMD64)
default:
rootType = mustParseGUID(partitionTypeLinuxRootPartitionARM64)
}
partitionEntries := []partitionEntry{
{
TypeGUID: mustParseGUID(partitionTypeEFISystemPartition),
GUID: mustParseGUID(p.GPTPARTUUID(1)),
FirstLBA: partition0First,
LastLBA: partition0Last,
Attributes: 0,
Name: partitionName("Microsoft basic data"),
},
{
TypeGUID: rootType,
GUID: mustParseGUID(p.GPTPARTUUID(2)),
FirstLBA: partition1First,
LastLBA: partition1Last,
Attributes: 0,
Name: partitionName("Linux filesystem"),
},
{
TypeGUID: mustParseGUID(partitionTypeLinuxFilesystemData),
GUID: mustParseGUID(p.GPTPARTUUID(3)),
FirstLBA: partition2First,
LastLBA: partition2Last,
Attributes: 0,
Name: partitionName("Linux filesystem"),
},
{
TypeGUID: mustParseGUID(partitionTypeLinuxFilesystemData),
GUID: mustParseGUID(p.GPTPARTUUID(4)),
FirstLBA: partition3First,
LastLBA: partition3Last,
Attributes: 0,
Name: partitionName("Linux filesystem"),
},
}
var pbuf bytes.Buffer
if err := binary.Write(&pbuf, binary.LittleEndian, partitionEntries); err != nil {
return err
}
if _, err := pbuf.Write(bytes.Repeat([]byte{0}, (128-len(partitionEntries))*128)); err != nil {
return err
}
entriesChecksum := crc32.ChecksumIEEE(pbuf.Bytes())
lastAddressable := (devsize / 512) - 1 // 0-indexed
currentLBA := uint64(1)
backupLBA := lastAddressable
entriesStart := uint64(2)
if !primary {
currentLBA = backupLBA
entriesStart = backupLBA - 32
backupLBA = 1
}
partitionHeader := struct {
Signature [8]byte
Revision uint32
HeaderSize uint32
CRC32Header uint32
Reserved uint32
CurrentLBA uint64
BackupLBA uint64
FirstUsableLBA uint64
LastUsableLBA uint64
DiskGUID [16]byte
EntriesStart uint64
EntriesCount uint32
EntriesSize uint32
CRC32Array uint32
}{
Signature: [8]byte{0x45, 0x46, 0x49, 0x20, 0x50, 0x41, 0x52, 0x54},
Revision: 0x00010000, // Revision 1.0
HeaderSize: 92, // bytes
CurrentLBA: currentLBA,
BackupLBA: backupLBA,
FirstUsableLBA: 34,
LastUsableLBA: lastAddressable - 32 - 1,
DiskGUID: mustParseGUID(p.GPTPARTUUID(0)),
EntriesStart: entriesStart,
// From https://wiki.osdev.org/GPT:
//
// Note: it is somewhat vague what the Number of Partition Entries field
// contain. For many applications that is the number of actually used
// entries, while many partitioning tools (most notably fdisk and gdisk)
// handles that as the number of maximum available entries, using full
// zero Partition Type to mark empty entries. Unfortunately both
// interpretation is suggested by the EFI spec, so this is unclear. One
// thing is certain, there should be no more entries (empty or not) than
// this field.
EntriesCount: 128,
EntriesSize: 128, // bytes
CRC32Array: entriesChecksum,
}
// Write to memory first to calculate CRC32
var hbuf bytes.Buffer
if err := binary.Write(&hbuf, binary.LittleEndian, partitionHeader); err != nil {
return err
}
if got, want := hbuf.Len(), int(partitionHeader.HeaderSize); got != want {
return fmt.Errorf("BUG: header size: got %d, want %d", got, want)
}
partitionHeader.CRC32Header = crc32.ChecksumIEEE(hbuf.Bytes())
if !primary {
// Write Partition entries (LBA 2-33):
if _, err := io.Copy(w, &pbuf); err != nil {
return err
}
}
// Then write the Partition table header (LBA 1):
if err := binary.Write(w, binary.LittleEndian, partitionHeader); err != nil {
return err
}
for _, v := range []interface{}{
[420]byte{}, // padding
} {
if err := binary.Write(w, binary.LittleEndian, v); err != nil {
return err
}
}
if primary {
// Write Partition entries (LBA 2-33):
if _, err := io.Copy(w, &pbuf); err != nil {
return err
}
}
return nil
}
func (p *Pack) Partition(o *os.File, devsize uint64) error {
minsize := uint64(1100 * MB)
if devsize < minsize {
return fmt.Errorf("device is too small (at least %d MB needed, %d MB available)", minsize/MB, devsize/MB)
}
if !p.UseGPT {
return writeMBRPartitionTable(p.FirstPartitionOffsetSectors, o, devsize)
}
if err := writePartitionTable(p.FirstPartitionOffsetSectors, o); err != nil {
return err
}
// Write Primary GPT Header:
if err := p.writeGPT(o, devsize, true /* primary */); err != nil {
return err
}
// Write Secondary GPT Header:
lastAddressable := (devsize / 512) - 1 // 0-indexed
lbaMinus33 := lastAddressable - 32
if _, err := o.Seek(int64(lbaMinus33*512), io.SeekStart); err != nil {
return err
}
if err := p.writeGPT(o, devsize, false /* backup */); err != nil {
return err
}
return nil
}
func (p *Pack) RereadPartitions(o *os.File) error {
if err := rereadPartitions(o); err != nil {
log.Printf("Re-reading partition table failed: %v. Remember to unplug and re-plug the SD card before creating a file system for persistent data, if desired.", err)
}
return nil
}