github.com/linuxboot/fiano@v1.2.0/pkg/visitors/dxecleaner.go (about)

     1  // Copyright 2018 the LinuxBoot 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 visitors
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"os/exec"
    14  	"os/signal"
    15  	"path/filepath"
    16  	"regexp"
    17  	"strings"
    18  	"syscall"
    19  
    20  	"github.com/linuxboot/fiano/pkg/guid"
    21  	"github.com/linuxboot/fiano/pkg/uefi"
    22  )
    23  
    24  // DXECleaner removes DXEs sequentially in multiple rounds. Each round, an
    25  // attempt is made to remove each DXE. The Test function determines if the
    26  // removal was successful. Additional rounds are performed until all DXEs are
    27  // removed.
    28  type DXECleaner struct {
    29  	// This function tests whether the firmware boots. The return values can be:
    30  	//
    31  	//     - (false, nil): The firmware was tested and failed to boot.
    32  	//     - (false, err): The firmware was tested and failed to boot due to err.
    33  	//     - (true, nil):  The firmware was tested and booted.
    34  	//     - (true, err):  Failed to test the firmware due to err.
    35  	Test func(f uefi.Firmware) (bool, error)
    36  
    37  	// Predicate to determine whether a DXE can be removed.
    38  	Predicate FindPredicate
    39  
    40  	// List of GUIDs which were removed.
    41  	Removals []guid.GUID
    42  
    43  	// Logs are written to this writer.
    44  	W io.Writer
    45  }
    46  
    47  // Run wraps Visit and performs some setup and teardown tasks.
    48  func (v *DXECleaner) Run(f uefi.Firmware) error {
    49  	var printf = func(format string, a ...interface{}) {
    50  		if v.W != nil {
    51  			fmt.Fprintf(v.W, format, a...)
    52  		}
    53  	}
    54  
    55  	// Find list of DXEs.
    56  	find := (&Find{Predicate: v.Predicate})
    57  	if err := find.Run(f); err != nil {
    58  		return err
    59  	}
    60  	var dxes []guid.GUID
    61  	for i := range find.Matches {
    62  		dxes = append(dxes, find.Matches[i].(*uefi.File).Header.GUID)
    63  	}
    64  	if len(dxes) == 0 {
    65  		return errors.New("found no DXEs in firmware image")
    66  	}
    67  
    68  	// Print list of removals in a format which can be passed back into UTK.
    69  	defer func() {
    70  		printf("Summary of removed DXEs:\n")
    71  		if len(v.Removals) == 0 {
    72  			printf("  Could not remove any DXEs\n")
    73  		} else {
    74  			for _, r := range v.Removals {
    75  				printf("  remove %s \\\n", r)
    76  			}
    77  		}
    78  	}()
    79  
    80  	// Main algorithm to remove DXEs.
    81  	moreRoundsNeeded := true
    82  	for i := 0; moreRoundsNeeded; i++ {
    83  		printf("Beginning of round %d\n", i+1)
    84  		moreRoundsNeeded = false
    85  		for i := 0; i < len(dxes); i++ {
    86  			// Remove the DXE from the image.
    87  			printf("Trying to remove %v\n", dxes[i])
    88  			remove := &Remove{Predicate: FindFileGUIDPredicate(dxes[i])}
    89  			if err := remove.Run(f); err != nil {
    90  				return err
    91  			}
    92  
    93  			if removedSuccessfully, err := v.Test(f); err == context.Canceled {
    94  				printf("Canceled by user %v!\n", dxes[i])
    95  				return nil
    96  			} else if removedSuccessfully && err != nil {
    97  				return err
    98  			} else if removedSuccessfully {
    99  				printf("  Success %v!\n", dxes[i])
   100  				v.Removals = append(v.Removals, dxes[i])
   101  				dxes = append(dxes[:i], dxes[i+1:]...)
   102  				i--
   103  				moreRoundsNeeded = true
   104  			} else {
   105  				printf("  Failed %v!\n", dxes[i])
   106  				remove.Undo()
   107  			}
   108  		}
   109  	}
   110  	return nil
   111  }
   112  
   113  // Visit applies the DXEClearn visitor to any Firmware type.
   114  func (v *DXECleaner) Visit(f uefi.Firmware) error {
   115  	return nil
   116  }
   117  
   118  // readBlackList returns a regex to filter DXEs according to the black list
   119  // file. Each line in the black list is the GUID or name of a firmware file.
   120  // Empty lines and lines beginning with '#' are ignored.
   121  func parseBlackList(fileName, fileContents string) (string, error) {
   122  	blackList := ""
   123  	for i, line := range strings.Split(fileContents, "\n") {
   124  		line = strings.TrimSpace(line)
   125  		if line == "" || strings.HasPrefix(line, "#") {
   126  			continue
   127  		}
   128  		// skip multiple words, We need this for linuxboot file parsing
   129  		fw := strings.Split(line, " ")[0]
   130  		_, err := regexp.Compile(fw)
   131  		if err != nil {
   132  			return "", fmt.Errorf("cannot compile regex %q from blacklist file %q on line %v: %v",
   133  				fw, fileName, i, err)
   134  		}
   135  		blackList += "|(" + fw + ")"
   136  	}
   137  	if blackList != "" {
   138  		blackList = blackList[1:]
   139  	}
   140  	return blackList, nil
   141  }
   142  
   143  func init() {
   144  	register := func(args []string) (uefi.Visitor, error) {
   145  		// When the user enters CTRL-C, the DXECleaner should stop, but
   146  		// also output the current progress.
   147  		ctx, cancel := context.WithCancel(context.Background())
   148  		c := make(chan os.Signal, 1)
   149  		signal.Notify(c, os.Interrupt)
   150  		go func() {
   151  			<-c
   152  			cancel()
   153  		}()
   154  
   155  		predicate := FindFileTypePredicate(uefi.FVFileTypeDriver)
   156  
   157  		// Create blacklist for DXEs which can be skipped.
   158  		useBlackList := len(args) == 2
   159  		if useBlackList {
   160  			fileName := args[1]
   161  			fileContents, err := os.ReadFile(fileName)
   162  			if err != nil {
   163  				return nil, fmt.Errorf("cannot read blacklist file %q: %v", fileName, err)
   164  			}
   165  			blackListRegex, err := parseBlackList(fileName, string(fileContents))
   166  			if err != nil {
   167  				return nil, err
   168  			}
   169  			if blackListRegex != "" {
   170  				blackListPredicate, err := FindFilePredicate(blackListRegex)
   171  				if err != nil {
   172  					return nil, err
   173  				}
   174  				predicate = FindAndPredicate(predicate, FindNotPredicate(blackListPredicate))
   175  			}
   176  		}
   177  
   178  		return &DXECleaner{
   179  			Test: func(f uefi.Firmware) (bool, error) {
   180  				tmpDir, err := os.MkdirTemp("", "dxecleaner")
   181  				if err != nil {
   182  					return true, err
   183  				}
   184  				defer os.RemoveAll(tmpDir)
   185  				tmpFile := filepath.Join(tmpDir, "bios.bin")
   186  
   187  				if err := (&Save{tmpFile}).Run(f); err != nil {
   188  					return true, err
   189  				}
   190  				cmd := exec.CommandContext(ctx, args[0], tmpFile)
   191  				cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
   192  				if err := cmd.Run(); err != nil {
   193  					if _, ok := err.(*exec.ExitError); !ok {
   194  						return true, err
   195  					}
   196  					status, ok := err.(*exec.ExitError).Sys().(syscall.WaitStatus)
   197  					if !ok {
   198  						return true, err
   199  					}
   200  					switch status.ExitStatus() {
   201  					case 1:
   202  						return true, err
   203  					case 2:
   204  						return false, err
   205  					default:
   206  						return true, fmt.Errorf("unexpected exit status %d", status.ExitStatus())
   207  					}
   208  				}
   209  				return true, nil
   210  			},
   211  			Predicate: predicate,
   212  			W:         os.Stdout,
   213  		}, nil
   214  	}
   215  
   216  	RegisterCLI("dxecleaner", "automates removal of UEFI drivers", 1, register)
   217  	RegisterCLI("dxecleaner_blacklist", "automates removal of UEFI drivers with a blacklist file", 2, register)
   218  }