Skip to content

Commit

Permalink
refactor: drop usage of objcopy to generate UKIs
Browse files Browse the repository at this point in the history
This brings native implementation without external dependencies.

Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
smira committed Jan 13, 2025
1 parent edf5c5e commit ed7e47d
Show file tree
Hide file tree
Showing 8 changed files with 518 additions and 87 deletions.
4 changes: 1 addition & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,6 @@ INSTALLER_ONLY_PKGS ?= \
zstd

IMAGER_EXTRA_PKGS ?= \
binutils-aarch64 \
binutils-x86_64 \
dosfstools \
e2fsprogs \
mtools \
Expand Down Expand Up @@ -460,7 +458,7 @@ secureboot-iso: image-secureboot-iso ## Builds UEFI only ISO which uses UKI and

.PHONY: secureboot-installer
secureboot-installer: ## Builds UEFI only installer which uses UKI and push it to the registry.
@$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)"
@$(MAKE) image-secureboot-installer IMAGER_ARGS="--base-installer-image $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG) $(IMAGER_ARGS)"
@for platform in $(subst $(,),$(space),$(PLATFORM)); do \
arch=$$(basename "$${platform}") && \
crane push $(ARTIFACTS)/installer-$${arch}-secureboot.tar $(REGISTRY_AND_USERNAME)/installer:$(IMAGE_TAG)-$${arch}-secureboot ; \
Expand Down
72 changes: 3 additions & 69 deletions internal/pkg/secureboot/uki/assemble.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,80 +5,14 @@
package uki

import (
"debug/pe"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"

"github.com/siderolabs/talos/internal/pkg/secureboot/uki/internal/pe"
)

// assemble the UKI file out of sections.
func (builder *Builder) assemble() error {
peFile, err := pe.Open(builder.SdStubPath)
if err != nil {
return err
}

defer peFile.Close() //nolint: errcheck

// find the first VMA address
lastSection := peFile.Sections[len(peFile.Sections)-1]

// align the VMA to 512 bytes
// https://github.com/saferwall/pe/blob/main/helper.go#L22-L26
const alignment = 0x1ff

header, ok := peFile.OptionalHeader.(*pe.OptionalHeader64)
if !ok {
return errors.New("failed to get optional header")
}

baseVMA := header.ImageBase + uint64(lastSection.VirtualAddress) + uint64(lastSection.VirtualSize)
baseVMA = (baseVMA + alignment) &^ alignment

// calculate sections size and VMA
for i := range builder.sections {
if !builder.sections[i].Append {
continue
}

st, err := os.Stat(builder.sections[i].Path)
if err != nil {
return err
}

builder.sections[i].Size = uint64(st.Size())
builder.sections[i].VMA = baseVMA

baseVMA += builder.sections[i].Size
baseVMA = (baseVMA + alignment) &^ alignment
}

// create the output file
args := make([]string, 0, len(builder.sections)+2)

for _, section := range builder.sections {
if !section.Append {
continue
}

args = append(args, "--add-section", fmt.Sprintf("%s=%s", section.Name, section.Path), "--change-section-vma", fmt.Sprintf("%s=0x%x", section.Name, section.VMA))
}

builder.unsignedUKIPath = filepath.Join(builder.scratchDir, "unsigned.uki")

args = append(args, builder.SdStubPath, builder.unsignedUKIPath)

objcopy := "/usr/x86_64-alpine-linux-musl/bin/objcopy"

if builder.Arch == "arm64" {
objcopy = "/usr/aarch64-alpine-linux-musl/bin/objcopy"
}

cmd := exec.Command(objcopy, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

return cmd.Run()
return pe.AssembleNative(builder.SdStubPath, builder.unsignedUKIPath, builder.sections)
}
280 changes: 280 additions & 0 deletions internal/pkg/secureboot/uki/internal/pe/native.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package pe

import (
"debug/pe"
"encoding/binary"
"errors"
"fmt"
"io"
"os"
"slices"
"time"

"github.com/siderolabs/gen/xslices"

"github.com/siderolabs/talos/internal/pkg/secureboot"
"github.com/siderolabs/talos/pkg/imager/utils"
)

const (
dosHeaderLength = 0x40
dosHeaderPadding = 0x40
)

// AssembleNative is a helper function to assemble the PE file without external programs.
//
//nolint:gocyclo,cyclop
func AssembleNative(srcPath, dstPath string, sections []Section) error {
in, err := os.Open(srcPath)
if err != nil {
return err
}

peFile, err := pe.NewFile(in)
if err != nil {
return err
}

defer in.Close() //nolint: errcheck

if peFile.COFFSymbols != nil {
return errors.New("COFF symbols are not supported")
}

if peFile.StringTable != nil {
return errors.New("COFF string table is not supported")
}

if peFile.Symbols != nil {
return errors.New("symbols are not supported")
}

_, err = in.Seek(0, io.SeekStart)
if err != nil {
return err
}

out, err := os.Create(dstPath)
if err != nil {
return fmt.Errorf("failed to create output: %w", err)
}

defer out.Close() //nolint: errcheck

// 1. DOS header
var dosheader [dosHeaderLength]byte

_, err = in.ReadAt(dosheader[:], 0)
if err != nil {
return fmt.Errorf("failed to read DOS header: %w", err)
}

binary.LittleEndian.PutUint32(dosheader[dosHeaderLength-4:], dosHeaderLength+dosHeaderPadding)

_, err = out.Write(append(append(dosheader[:], make([]byte, dosHeaderPadding)...), []byte("PE\x00\x00")...))
if err != nil {
return fmt.Errorf("failed to write DOS header: %w", err)
}

// 2. PE header and optional header
newFileHeader := peFile.FileHeader

timestamp, ok, err := utils.SourceDateEpoch()
if err != nil {
return fmt.Errorf("failed to get SOURCE_DATE_EPOCH: %w", err)
}

if !ok {
timestamp = time.Now().Unix()
}

newFileHeader.TimeDateStamp = uint32(timestamp)

// find the first VMA address
lastSection := peFile.Sections[len(peFile.Sections)-1]

header, ok := peFile.OptionalHeader.(*pe.OptionalHeader64)
if !ok {
return errors.New("failed to get optional header")
}

sectionAlignment := uint64(header.SectionAlignment - 1)
fileAlignment := uint64(header.FileAlignment - 1)

baseVirtualAddress := uint64(lastSection.VirtualAddress) + uint64(lastSection.VirtualSize)
baseVirtualAddress = (baseVirtualAddress + sectionAlignment) &^ sectionAlignment

newHeader := *header
newHeader.MajorLinkerVersion = 0
newHeader.MinorLinkerVersion = 0
newHeader.CheckSum = 0

newSections := slices.Clone(peFile.Sections)

// calculate sections size and VMA
for i := range sections {
if !sections[i].Append {
continue
}

st, err := os.Stat(sections[i].Path)
if err != nil {
return err
}

sections[i].virtualSize = uint64(st.Size())
sections[i].virtualAddress = baseVirtualAddress

baseVirtualAddress += sections[i].virtualSize
baseVirtualAddress = (baseVirtualAddress + sectionAlignment) &^ sectionAlignment

newFileHeader.NumberOfSections++

newSections = append(newSections, &pe.Section{
SectionHeader: pe.SectionHeader{
Name: string(sections[i].Name),
VirtualSize: uint32(sections[i].virtualSize),
VirtualAddress: uint32(sections[i].virtualAddress),
Size: uint32((sections[i].virtualSize + fileAlignment) &^ fileAlignment),
Characteristics: pe.IMAGE_SCN_CNT_INITIALIZED_DATA | pe.IMAGE_SCN_MEM_READ,
},
})
}

newHeader.SizeOfInitializedData = 0
newHeader.SizeOfCode = 0
newHeader.SizeOfHeaders = 0x80 /* DOS header */ + uint32(binary.Size(pe.FileHeader{})+binary.Size(pe.OptionalHeader64{})+binary.Size(pe.SectionHeader32{})*len(newSections))
newHeader.SizeOfHeaders = (newHeader.SizeOfHeaders + uint32(fileAlignment)) &^ uint32(fileAlignment)

lastNewSection := newSections[len(newSections)-1]

lastSectionPointer := uint64(lastNewSection.VirtualAddress) + uint64(lastNewSection.VirtualSize) + newHeader.ImageBase
lastSectionPointer = (lastSectionPointer + sectionAlignment) &^ sectionAlignment

newHeader.SizeOfImage = uint32(lastSectionPointer - newHeader.ImageBase)

for _, section := range newSections {
if section.Characteristics&pe.IMAGE_SCN_CNT_INITIALIZED_DATA != 0 {
newHeader.SizeOfInitializedData += section.Size
} else {
newHeader.SizeOfCode += section.Size
}
}

// write the new file header
if err = binary.Write(out, binary.LittleEndian, newFileHeader); err != nil {
return fmt.Errorf("failed to write file header: %w", err)
}

if err = binary.Write(out, binary.LittleEndian, newHeader); err != nil {
return fmt.Errorf("failed to write optional header: %w", err)
}

// 3. Section headers
rawSections := xslices.Map(newSections, func(section *pe.Section) pe.SectionHeader32 {
var rawName [8]byte

copy(rawName[:], section.Name)

return pe.SectionHeader32{
Name: rawName,
VirtualSize: section.VirtualSize,
VirtualAddress: section.VirtualAddress,
SizeOfRawData: section.Size,
Characteristics: section.Characteristics,
}
},
)

sectionOffset := newHeader.SizeOfHeaders

for i := range rawSections {
rawSections[i].PointerToRawData = sectionOffset

sectionOffset += rawSections[i].SizeOfRawData
}

for _, rawSection := range rawSections {
if err = binary.Write(out, binary.LittleEndian, rawSection); err != nil {
return fmt.Errorf("failed to write section header: %w", err)
}
}

// 4. Section data
for i, rawSection := range rawSections {
name := newSections[i].Name

if err := func(rawSection pe.SectionHeader32, name string) error {
// the section might come either from the input PE file or from a separate file
var sectionData io.ReadCloser

for _, section := range sections {
if section.Append && section.Name == secureboot.Section(name) {
sectionData, err = os.Open(section.Path)
if err != nil {
return fmt.Errorf("failed to open section data: %w", err)
}

defer sectionData.Close() //nolint: errcheck

break
}
}

if sectionData == nil {
for _, section := range peFile.Sections {
if section.Name == name {
sectionData = io.NopCloser(section.Open())

break
}
}
}

if sectionData == nil {
return fmt.Errorf("failed to find section data for %q", name)
}

_, err = out.Seek(int64(rawSection.PointerToRawData), io.SeekStart)
if err != nil {
return fmt.Errorf("failed to seek to section data: %w", err)
}

n, err := io.Copy(out, sectionData)
if err != nil {
return fmt.Errorf("failed to copy section data: %w", err)
}

if n > int64(rawSection.SizeOfRawData) {
return fmt.Errorf("section data is too large: %d > %d", n, rawSection.SizeOfRawData)
}

if n < int64(rawSection.SizeOfRawData) {
_, err = io.CopyN(out, zeroReader{}, int64(rawSection.SizeOfRawData)-n)
if err != nil {
return fmt.Errorf("failed to zero-fill section data: %w", err)
}
}

return nil
}(rawSection, name); err != nil {
return fmt.Errorf("failed to write section data %s: %w", name, err)
}
}

return nil
}

type zeroReader struct{}

func (zeroReader) Read(p []byte) (int, error) {
for i := range p {
p[i] = 0
}

return len(p), nil
}
Loading

0 comments on commit ed7e47d

Please sign in to comment.