Skip to content

Commit

Permalink
Add Linux Variable Writing Script (#353)
Browse files Browse the repository at this point in the history
## Description

This adds a Python script to write UEFI variables to and from config
bins from Linux. The current support is only for Windows.

For each item, place an "x" in between `[` and `]` if true. Example:
`[x]`.
_(you can also check items in the GitHub UI)_

- [ ] Impacts functionality?
- **Functionality** - Does the change ultimately impact how firmware
functions?
- Examples: Add a new library, publish a new PPI, update an algorithm,
...
- [ ] Impacts security?
- **Security** - Does the change have a direct security impact on an
application,
    flow, or firmware?
  - Examples: Crypto algorithm change, buffer overflow fix, parameter
    validation improvement, ...
- [ ] Breaking change?
- **Breaking change** - Will anyone consuming this change experience a
break
    in build or boot behavior?
- Examples: Add a new library class, move a module to a different repo,
call
    a function in a new library class in a pre-existing module, ...
- [ ] Includes tests?
  - **Tests** - Does the change include any explicit test code?
  - Examples: Unit tests, integration tests, robot tests, ...
- [ ] Includes documentation?
- **Documentation** - Does the change contain explicit documentation
additions
    outside direct code modifications (and comments)?
- Examples: Update readme file, add feature readme file, link to
documentation
    on an a separate Web page, ...

## How This Was Tested

Tested in Ubuntu 24.04 with the QemuQ35 virtual platform using
mu_feature_config as well as on physical platforms using it.

## Integration Instructions

Run the same flows as for the Windows script, the scripts will
automatically detect that this is run from Linux and choose the right
script. It needs to be run as sudo.

---------

Co-authored-by: kuqin12 <42554914+kuqin12@users.noreply.github.com>
Co-authored-by: Aaron <105021049+apop5@users.noreply.github.com>
  • Loading branch information
3 people authored May 28, 2024
1 parent 319a618 commit 2fffa56
Show file tree
Hide file tree
Showing 6 changed files with 183 additions and 42 deletions.
5 changes: 4 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@
"profilenames",
"profileid",
"profileids",
"prettyname"
"prettyname",
"efivars",
"efivarfs",
"chattr"
]
}
5 changes: 4 additions & 1 deletion SetupDataPkg/SetupDataPkg.ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@
"dmpstore",
"mschange",
"DDTHH",
"prettyname"
"prettyname",
"efivars",
"efivarfs",
"chattr"
], # words to extend to the dictionary for this package
"IgnoreStandardPaths": [], # Standard Plugin defined paths that should be ignore
"AdditionalIncludePaths": [] # Additional paths to spell check (wildcards supported)
Expand Down
20 changes: 14 additions & 6 deletions SetupDataPkg/Tools/ReadUefiVarsToConfVarList.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
import struct
import uuid
import ctypes
from SettingSupport.UefiVariablesSupportLib import UefiVariable
if os.name == 'nt':
from SettingSupport.UefiVariablesSupportLib import UefiVariable
else:
from SettingSupport.UefiVariablesSupportLinuxLib import UefiVariable
from VariableList import Schema, UEFIVariable, create_vlist_buffer


Expand Down Expand Up @@ -53,7 +56,7 @@ def option_parser():
# array
#
def read_variable_into_variable_list(uefi_var, name, namespace):
(rc, var, _) = uefi_var.GetUefiVar(name, namespace)
(rc, var) = uefi_var.GetUefiVar(name, namespace)
if rc != 0:
if rc != UefiVariable.ERROR_ENVVAR_NOT_FOUND:
# only log the errors other than EFI_NOT_FOUND, because not found is normal in this case...
Expand All @@ -78,7 +81,7 @@ def main():
ret = b''
if arguments.configuration_file is None:
# Read all the variables
(rc, efi_var_names, error_string) = UefiVar.GetUefiAllVarNames()
(rc, efi_var_names) = UefiVar.GetUefiAllVarNames()
if rc != 0:
logging.error(f"Error returned from GetUefiAllVarNames: {rc}")

Expand Down Expand Up @@ -117,9 +120,14 @@ def main():
console.setLevel(logging.CRITICAL)

# check the privilege level and report error
if not ctypes.windll.shell32.IsUserAnAdmin():
print("Administrator privilege required. Please launch from an Administrator privilege level.")
sys.exit(1)
if os.name == 'nt':
if not ctypes.windll.shell32.IsUserAnAdmin():
print("Administrator privilege required. Please launch from an Administrator privilege level.")
sys.exit(1)
else:
if os.geteuid() != 0:
print("Root permission required, please run script with sudo.")
sys.exit(1)

# call main worker function
retcode = main()
Expand Down
34 changes: 5 additions & 29 deletions SetupDataPkg/Tools/SettingSupport/UefiVariablesSupportLib.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
c_wchar_p,
c_void_p,
c_int,
c_char,
create_string_buffer,
WinError,
pointer
Expand Down Expand Up @@ -90,27 +89,6 @@ def __init__(self):
)
pass

#
# Helper function to create buffer for var read/write
#
def CreateBuffer(self, init, size=None):
"""CreateBuffer(aString) -> character array
CreateBuffer(anInteger) -> character array
CreateBuffer(aString, anInteger) -> character array
"""
if isinstance(init, str):
if size is None:
size = len(init) + 1
buftype = c_char * size
buf = buftype()
buf.value = init
return buf
elif isinstance(init, int):
buftype = c_char * init
buf = buftype()
return buf
raise TypeError(init)

#
# Function to get variable
# return a tuple of error code and variable data as string
Expand All @@ -135,8 +113,8 @@ def GetUefiVar(self, name, guid):
)
logging.error(WinError())
if efi_var is None:
return (err, None, WinError(err))
return (err, efi_var[:length], WinError(err))
return (err, None)
return (err, efi_var[:length])

#
# Function to get all variable names
Expand Down Expand Up @@ -176,8 +154,8 @@ def GetUefiAllVarNames(self):
logging.error(
"EnumerateFirmwareEnvironmentVariable failed (GetLastError = 0x%x)" % status
)
return (status, None, WinError(status))
return (status, efi_var_names, None)
return (status, None)
return (status, efi_var_names)

#
# Function to set variable
Expand All @@ -186,7 +164,6 @@ def GetUefiAllVarNames(self):
def SetUefiVar(self, name, guid, var=None, attrs=None):
var_len = 0
err = 0
error_string = None
if var is None:
var = bytes(0)
else:
Expand Down Expand Up @@ -221,5 +198,4 @@ def SetUefiVar(self, name, guid, var=None, attrs=None):
"SetFirmwareEnvironmentVariable failed (GetLastError = 0x%x)" % err
)
logging.error(WinError())
error_string = WinError(err)
return (success, err, error_string)
return success
143 changes: 143 additions & 0 deletions SetupDataPkg/Tools/SettingSupport/UefiVariablesSupportLinuxLib.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
# @file
#
# Python lib to support Reading and writing UEFI variables from Linux
#
#
# Copyright (c), Microsoft Corporation
# SPDX-License-Identifier: BSD-2-Clause-Patent
#
# GetUefiAllVarNames is based on information from
# https://github.com/awslabs/python-uefivars/blob/main/pyuefivars/efivarfs.py
#
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
# SPDX-License-Identifier: MIT

import os
import uuid
import struct

from ctypes import (
create_string_buffer
)

EFI_VAR_MAX_BUFFER_SIZE = 1024 * 1024


class UefiVariable(object):
ERROR_ENVVAR_NOT_FOUND = 0xcb

def __init__(self):
pass

#
# Function to get variable
# return a tuple of error code and variable data as string
#
def GetUefiVar(self, name, guid):
# success
err = 0
# the variable name is VariableName-Guid
path = '/sys/firmware/efi/efivars/' + name + '-%s' % guid

if not os.path.exists(path):
err = UefiVariable.ERROR_ENVVAR_NOT_FOUND
return (err, None)

efi_var = create_string_buffer(EFI_VAR_MAX_BUFFER_SIZE)
with open(path, 'rb') as fd:
efi_var = fd.read()

return (err, efi_var)

#
# Function to get all variable names
# return a tuple of error code and variable names byte array formatted as:
#
# typedef struct _VARIABLE_NAME {
# ULONG NextEntryOffset;
# GUID VendorGuid;
# WCHAR Name[ANYSIZE_ARRAY];
# } VARIABLE_NAME, *PVARIABLE_NAME;
#
def GetUefiAllVarNames(self):
# success
status = 0

# implementation borrowed from https://github.com/awslabs/python-uefivars/blob/main/pyuefivars/efivarfs.py
path = '/sys/firmware/efi/efivars'
if not os.path.exists(path):
status = UefiVariable.ERROR_ENVVAR_NOT_FOUND
return (status, None)

vars = os.listdir(path)

# get the total buffer length, converting to unicode
length = 0
offset = 0
for var in vars:
split_string = var.split('-')
name = '-'.join(split_string[:-5])
name = name.encode('utf-16-le')
name_len = len(name)
length += (4 + 16 + name_len)

efi_var_names = create_string_buffer(length)

for var in vars:
# efivarfs stores vars as NAME-GUID
split_string = var.split('-')
try:
# GUID is last 5 elements of split_string
guid = uuid.UUID('-'.join(split_string[-5:])).bytes_le
except ValueError:
raise Exception(f'Could not parse "{var}"')

# the other part is the name
name = '-'.join(split_string[:-5])
name = name.encode('utf-16-le')
name_len = len(name)

# NextEntryOffset
struct.pack_into('<I', efi_var_names, offset, 4 + 16 + name_len)
offset += 4

# VendorGuid
struct.pack_into('=16s', efi_var_names, offset, guid)
offset += 16

# Name
struct.pack_into(f'={name_len}s', efi_var_names, offset, name)
offset += name_len

return (status, efi_var_names)

#
# Function to set variable
# return a tuple of boolean status, error_code, error_string (None if not error)
#
def SetUefiVar(self, name, guid, var=None, attrs=None):
success = 0 # Fail

# There is a null terminator at the end of the name
path = '/sys/firmware/efi/efivars/' + name[:-1] + '-' + str(guid)
if var is None:
# we are deleting the variable
if (os.path.exists(path)):
os.remove(path)
success = 1 # expect non-zero success
return success

if attrs is None:
attrs = 0x7

# if the file exists, remove the immutable flag
if (os.path.exists(path)):
os.system('sudo chattr -i ' + path)

with open(path, 'wb') as fd:
# var data is attribute (UINT32) followed by data
packed = struct.pack('=I', attrs)
packed += var
fd.write(packed)

return 1
18 changes: 13 additions & 5 deletions SetupDataPkg/Tools/WriteConfVarListToUefiVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
import struct
import uuid
import ctypes
from SettingSupport.UefiVariablesSupportLib import UefiVariable
if os.name == 'nt':
from SettingSupport.UefiVariablesSupportLib import UefiVariable
else:
from SettingSupport.UefiVariablesSupportLinuxLib import UefiVariable

gEfiGlobalVariableGuid = "8BE4DF61-93CA-11D2-AA0D-00E098032B8C"

Expand Down Expand Up @@ -93,7 +96,7 @@ def extract_single_var_from_file_and_write_nvram(var):
logging.debug(f"Found Variable: {VarName} {Guid} {Attributes}")

UefiVar = UefiVariable()
(rc, err, error_string) = UefiVar.SetUefiVar(
rc = UefiVar.SetUefiVar(
VarName,
Guid,
Data,
Expand Down Expand Up @@ -136,9 +139,14 @@ def main():
console.setLevel(logging.CRITICAL)

# check the privilege level and report error
if not ctypes.windll.shell32.IsUserAnAdmin():
print("Administrator privilege required. Please launch from an Administrator privilege level.")
sys.exit(1)
if os.name == 'nt':
if not ctypes.windll.shell32.IsUserAnAdmin():
print("Administrator privilege required. Please launch from an Administrator privilege level.")
sys.exit(1)
else:
if os.geteuid() != 0:
print("Root permission required, please run script with sudo.")
sys.exit(1)

# call main worker function
retcode = main()
Expand Down

0 comments on commit 2fffa56

Please sign in to comment.