github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/bootloader/efi/efi.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 // Package efi supports reading EFI variables. 21 package efi 22 23 import ( 24 "bytes" 25 "encoding/binary" 26 "errors" 27 "fmt" 28 "io" 29 "io/ioutil" 30 "os" 31 "path/filepath" 32 "unicode/utf16" 33 34 "github.com/snapcore/snapd/dirs" 35 "github.com/snapcore/snapd/osutil" 36 ) 37 38 var ErrNoEFISystem = errors.New("not a supported EFI system") 39 40 type VariableAttr uint32 41 42 // see https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/efi.h?h=v5.4.32 43 const ( 44 VariableNonVolatile VariableAttr = 0x00000001 45 VariableBootServiceAccess VariableAttr = 0x00000002 46 VariableRuntimeAccess VariableAttr = 0x00000004 47 ) 48 49 var ( 50 openEFIVar = openEFIVarImpl 51 ) 52 53 const expectedEFIvarfsDir = "/sys/firmware/efi/efivars" 54 55 func openEFIVarImpl(name string) (r io.ReadCloser, attr VariableAttr, size int64, err error) { 56 mounts, err := osutil.LoadMountInfo() 57 if err != nil { 58 return nil, 0, 0, err 59 } 60 found := false 61 for _, mnt := range mounts { 62 if mnt.MountDir == expectedEFIvarfsDir { 63 if mnt.FsType == "efivarfs" { 64 found = true 65 break 66 } 67 } 68 } 69 if !found { 70 return nil, 0, 0, ErrNoEFISystem 71 } 72 varf, err := os.Open(filepath.Join(dirs.GlobalRootDir, expectedEFIvarfsDir, name)) 73 if err != nil { 74 return nil, 0, 0, err 75 } 76 defer func() { 77 if err != nil { 78 varf.Close() 79 } 80 }() 81 fi, err := varf.Stat() 82 if err != nil { 83 return nil, 0, 0, err 84 } 85 sz := fi.Size() 86 if sz < 4 { 87 return nil, 0, 0, fmt.Errorf("unexpected size: %d", sz) 88 } 89 90 if err = binary.Read(varf, binary.LittleEndian, &attr); err != nil { 91 return nil, 0, 0, err 92 } 93 return varf, attr, sz - 4, nil 94 } 95 96 func cannotReadError(name string, err error) error { 97 return fmt.Errorf("cannot read EFI var %q: %v", name, err) 98 } 99 100 // ReadVarBytes will attempt to read the bytes of the value of the 101 // specified EFI variable, specified by its full name composed of the 102 // variable name and vendor ID. It also returns the attribute value 103 // attached to it. It expects to use the efivars filesystem at 104 // /sys/firmware/efi/efivars. 105 // https://www.kernel.org/doc/Documentation/filesystems/efivarfs.txt 106 // for more details. 107 func ReadVarBytes(name string) ([]byte, VariableAttr, error) { 108 varf, attr, _, err := openEFIVar(name) 109 if err != nil { 110 if err == ErrNoEFISystem { 111 return nil, 0, err 112 } 113 return nil, 0, cannotReadError(name, err) 114 } 115 defer varf.Close() 116 b, err := ioutil.ReadAll(varf) 117 if err != nil { 118 return nil, 0, cannotReadError(name, err) 119 } 120 return b, attr, nil 121 } 122 123 // ReadVarString will attempt to read the string value of the 124 // specified EFI variable, specified by its full name composed of the 125 // variable name and vendor ID. The string value is expected to be 126 // encoded as UTF16. It also returns the attribute value attached to 127 // it. It expects to use the efivars filesystem at 128 // /sys/firmware/efi/efivars. 129 // https://www.kernel.org/doc/Documentation/filesystems/efivarfs.txt 130 // for more details. 131 func ReadVarString(name string) (string, VariableAttr, error) { 132 varf, attr, sz, err := openEFIVar(name) 133 if err != nil { 134 if err == ErrNoEFISystem { 135 return "", 0, err 136 } 137 return "", 0, cannotReadError(name, err) 138 } 139 defer varf.Close() 140 // TODO: consider using golang.org/x/text/encoding/unicode here 141 if sz%2 != 0 { 142 return "", 0, fmt.Errorf("EFI var %q is not a valid UTF16 string, it has an extra byte", name) 143 } 144 n := int(sz / 2) 145 if n == 0 { 146 return "", attr, nil 147 } 148 r16 := make([]uint16, n) 149 if err := binary.Read(varf, binary.LittleEndian, r16); err != nil { 150 return "", 0, cannotReadError(name, err) 151 } 152 if r16[n-1] == 0 { 153 n-- 154 } 155 b := &bytes.Buffer{} 156 for _, r := range utf16.Decode(r16[:n]) { 157 b.WriteRune(r) 158 } 159 return b.String(), attr, nil 160 } 161 162 // MockVars mocks EFI variables as read by ReadVar*, only to be used 163 // from tests. Set vars to nil to mock a non-EFI system. 164 func MockVars(vars map[string][]byte, attrs map[string]VariableAttr) (restore func()) { 165 osutil.MustBeTestBinary("MockVars only to be used from tests") 166 old := openEFIVar 167 openEFIVar = func(name string) (io.ReadCloser, VariableAttr, int64, error) { 168 if vars == nil { 169 return nil, 0, 0, ErrNoEFISystem 170 } 171 if val, ok := vars[name]; ok { 172 attr, ok := attrs[name] 173 if !ok { 174 attr = VariableRuntimeAccess | VariableBootServiceAccess 175 } 176 return ioutil.NopCloser(bytes.NewBuffer(val)), attr, int64(len(val)), nil 177 } 178 return nil, 0, 0, fmt.Errorf("EFI variable %s not mocked", name) 179 } 180 181 return func() { 182 openEFIVar = old 183 } 184 }