util: add smmstoretool for editing SMMSTORE
Offline SMMSTORE variable modification tool. Can be used to pre-configure ROM image or debug EFI state stored in a dump. Change-Id: I6c1c06f1d0c39c13b5be76a3070f09b715aca6e0 Signed-off-by: Sergii Dmytruk <sergii.dmytruk@3mdeb.com> Reviewed-on: https://review.coreboot.org/c/coreboot/+/79080 Tested-by: build bot (Jenkins) <no-reply@coreboot.org> Reviewed-by: Michał Żygowski <michal.zygowski@3mdeb.com> Reviewed-by: Arthur Heymans <arthur@aheymans.xyz>
This commit is contained in:
committed by
Martin L Roth
parent
7a51acfbe9
commit
04bd965143
@ -144,6 +144,7 @@ from the local git repository for auditing or release `Bash`
|
||||
Does not show variants. `Shell`
|
||||
* _ucode_h_to_bin.sh_ - Microcode conversion tool `Bash`
|
||||
* _update_submodules_ - Check all submodules for updates `Bash`
|
||||
* __smmstoretool__ - Offline SMMSTORE variable modification tool `C`
|
||||
* __spdtool__ - Dumps SPD ROMs from a given blob to separate files
|
||||
using known patterns and reserved bits. Useful for analysing firmware
|
||||
that holds SPDs on boards that have soldered down DRAM. `python`
|
||||
@ -171,6 +172,7 @@ the documentation `Bash`
|
||||
* [cbfstool](util/cbfstool/index.md)
|
||||
* [ifdtool](util/ifdtool/index.md)
|
||||
* [intelp2m](util/intelp2m/index.md)
|
||||
* [smmstoretool](util/smmstoretool/index.md)
|
||||
|
||||
## Generated documentation
|
||||
|
||||
|
126
Documentation/util/smmstoretool/index.md
Normal file
126
Documentation/util/smmstoretool/index.md
Normal file
@ -0,0 +1,126 @@
|
||||
# smmstoretool
|
||||
|
||||
Offline SMMSTORE variable modification tool.
|
||||
|
||||
## Operation
|
||||
|
||||
### Storage initialization
|
||||
|
||||
If SMMSTORE presence isn't detected and an update operation is requested, the
|
||||
store spanning the whole file is created automatically. Size of the store file
|
||||
must be a multiple of 64 KiB (block size in version 2 of SMMSTORE protocol),
|
||||
the variable storage itself will be 64 KiB in size. That's the way EDK2 makes
|
||||
use of it.
|
||||
|
||||
Unlike online editing which mostly appends new variable entries each storage
|
||||
update with this tool drops all deleted or incomplete entries.
|
||||
|
||||
### Unicode
|
||||
|
||||
There is no actual support for it. ASCII bytes (or UTF-8 bytes if that was
|
||||
passed in) is just extended to 16 bits. And Unicode chars that are larger than
|
||||
8 bit are turned into `?`. Need UTF-8 to/from UTF-16 conversion functions for
|
||||
proper support.
|
||||
|
||||
## Help
|
||||
|
||||
Start with:
|
||||
|
||||
```
|
||||
$ smmstoretool -h
|
||||
Usage: smmstoretool smm-store-file sub-command
|
||||
smmstoretool -h|--help
|
||||
|
||||
Sub-commands:
|
||||
* get - display current value of a variable
|
||||
* guids - show GUID to alias mapping
|
||||
* help - provide built-in help
|
||||
* list - list variables present in the store
|
||||
* remove - remove a variable from the store
|
||||
* set - add or updates a variable in the store
|
||||
```
|
||||
|
||||
Then run `smmstoretool rom help sub-command-name` to get more details.
|
||||
|
||||
## Data types
|
||||
|
||||
EFI variables in the storage don't have an associated data type and it needs to
|
||||
be specified on reading/writing variables. Several basic types that correspond
|
||||
to typical configuration values are supported:
|
||||
|
||||
* `bool` (`true`, `false`)
|
||||
* `uint8` (0-255)
|
||||
* `uint16` (0-65535)
|
||||
* `uint32` (0-4294967295)
|
||||
* `ascii` (NUL-terminated)
|
||||
* `unicode` (widened and NUL-terminated)
|
||||
* `raw` (output only; raw bytes on output)
|
||||
|
||||
## Examples
|
||||
|
||||
`SMMSTORE` is the name of a file containing SMMSTORE data.
|
||||
|
||||
### Variable listing
|
||||
|
||||
```
|
||||
$ smmstoretool SMMSTORE list
|
||||
dasharo :NetworkBoot (1 byte)
|
||||
c076ec0c-7028-4399-a07271ee5c448b9f:CustomMode (1 byte)
|
||||
d9bee56e-75dc-49d9-b4d7b534210f637a:certdb (4 bytes)
|
||||
9073e4e0-60ec-4b6e-99034c223c260f3c:VendorKeysNv (1 byte)
|
||||
6339d487-26ba-424b-9a5d687e25d740bc:TCG2_DEVICE_DETECTION (1 byte)
|
||||
6339d487-26ba-424b-9a5d687e25d740bc:TCG2_CONFIGURATION (1 byte)
|
||||
6339d487-26ba-424b-9a5d687e25d740bc:TCG2_VERSION (16 bytes)
|
||||
global :Boot0000 (66 bytes)
|
||||
global :Timeout (2 bytes)
|
||||
global :PlatformLang (3 bytes)
|
||||
global :Lang (4 bytes)
|
||||
global :Key0000 (14 bytes)
|
||||
global :Boot0001 (102 bytes)
|
||||
global :Key0001 (14 bytes)
|
||||
04b37fe8-f6ae-480b-bdd537d98c5e89aa:VarErrorFlag (1 byte)
|
||||
dasharo :Type1UUID (16 bytes)
|
||||
dasharo :Type2SN (10 bytes)
|
||||
global :Boot0002 (90 bytes)
|
||||
global :BootOrder (8 bytes)
|
||||
global :Boot0003 (76 bytes)
|
||||
...
|
||||
```
|
||||
|
||||
### Variable reading
|
||||
|
||||
```
|
||||
$ smmstoretool SMMSTORE get -g dasharo -n UsbDriverStack -t bool
|
||||
false
|
||||
```
|
||||
|
||||
### Variable writing
|
||||
|
||||
```
|
||||
$ smmstoretool SMMSTORE set -g dasharo -n UsbDriverStack -t bool -v true
|
||||
```
|
||||
|
||||
### Variable deletion
|
||||
|
||||
```
|
||||
$ smmstoretool SMMSTORE remove -g dasharo -n NetworkBoot
|
||||
```
|
||||
|
||||
### Real-world usage
|
||||
|
||||
If one edits a newly generated Dasharo `coreboot.rom`:
|
||||
|
||||
```bash
|
||||
cbfstool coreboot.rom read -r SMMSTORE -f SMMSTORE
|
||||
smmstoretool SMMSTORE set -g dasharo -n NetworkBoot -t bool -v true
|
||||
cbfstool coreboot.rom write -r SMMSTORE -f SMMSTORE
|
||||
```
|
||||
|
||||
On the first boot, "Network Boot" setting will already be enabled.
|
||||
|
||||
Can also read SMMSTORE from a flash and examine some of its contents for
|
||||
debugging purposes without adding new logging code or powering on the hardware:
|
||||
|
||||
```bash
|
||||
smmstoretool SMMSTORE get -g global -n BootOrder -t raw | hexdump -C
|
||||
```
|
@ -133,6 +133,7 @@ from the local git repository for auditing or release `Bash`
|
||||
Does not show variants. `Shell`
|
||||
* _ucode_h_to_bin.sh_ - Microcode conversion tool `Bash`
|
||||
* _update_submodules_ - Check all submodules for updates `Bash`
|
||||
* __smmstoretool__ - Offline SMMSTORE variable modification tool `C`
|
||||
* __spdtool__ - Dumps SPD ROMs from a given blob to separate files
|
||||
using known patterns and reserved bits. Useful for analysing firmware
|
||||
that holds SPDs on boards that have soldered down DRAM. `python`
|
||||
|
1
util/smmstoretool/.gitignore
vendored
Normal file
1
util/smmstoretool/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
smmstoretool
|
56
util/smmstoretool/Makefile
Normal file
56
util/smmstoretool/Makefile
Normal file
@ -0,0 +1,56 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
PRG := smmstoretool
|
||||
TOP ?= $(abspath ../..)
|
||||
ROOT := $(TOP)/src
|
||||
MDE := $(ROOT)/vendorcode/intel/edk2/UDK2017/MdePkg/Include/
|
||||
|
||||
CC ?= $(CROSS_COMPILE)gcc
|
||||
HOSTCC ?= $(CC)
|
||||
INSTALL ?= /usr/bin/env install
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
HOSTCFLAGS ?= $(CFLAGS)
|
||||
HOSTCFLAGS += -Wall -Wextra -MMD -MP -O3
|
||||
HOSTCFLAGS += -I $(ROOT)/commonlib/bsd/include
|
||||
HOSTCFLAGS += -I $(ROOT)/vendorcode/intel/edk2/
|
||||
HOSTCFLAGS += -I $(MDE)
|
||||
|
||||
HOSTLDFLAGS ?= $(LDFLAGS)
|
||||
|
||||
MACHINE := $(shell uname -m)
|
||||
ifeq ($(MACHINE),x86_64)
|
||||
HOSTCFLAGS += -I $(MDE)/X64
|
||||
else ifeq ($(patsubst i%86,Ia32,$(MACHINE)),Ia32)
|
||||
HOSTCFLAGS += -I $(MDE)/Ia32
|
||||
else
|
||||
$(error Unsupported machine: '$(MACHINE)')
|
||||
endif
|
||||
|
||||
SRC := data.c fv.c guids.c main.c storage.c utils.c vs.c
|
||||
|
||||
OBJ := $(SRC:.c=.o)
|
||||
DEP := $(SRC:.c=.o.d)
|
||||
|
||||
.PHONY: all debug clean install
|
||||
|
||||
all: $(PRG)
|
||||
|
||||
debug: HOSTCFLAGS += -O0 -g
|
||||
debug: HOSTLDFLAGS += -g
|
||||
debug: all
|
||||
|
||||
install: $(PRG)
|
||||
$(INSTALL) -d $(DESTDIR)$(PREFIX)/bin/
|
||||
$(INSTALL) $^ $(DESTDIR)$(PREFIX)/bin/
|
||||
|
||||
clean:
|
||||
-$(RM) $(PRG) $(OBJ) $(DEP)
|
||||
|
||||
$(PRG): $(OBJ)
|
||||
$(HOSTCC) -o $@ $^ $(HOSTLDFLAGS)
|
||||
|
||||
%.o: %.c
|
||||
$(HOSTCC) $(HOSTCFLAGS) -c -o $@ -MF $@.d $<
|
||||
|
||||
-include $(DEP)
|
180
util/smmstoretool/data.c
Normal file
180
util/smmstoretool/data.c
Normal file
@ -0,0 +1,180 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "data.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
void print_data(const uint8_t data[], size_t data_size, enum data_type type)
|
||||
{
|
||||
if (data_size == 0)
|
||||
return;
|
||||
|
||||
switch (type) {
|
||||
case DATA_TYPE_BOOL:
|
||||
bool value = false;
|
||||
for (size_t i = 0; i < data_size; ++i) {
|
||||
if (data[i] != 0) {
|
||||
value = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
printf("%s\n", value ? "true" : "false");
|
||||
break;
|
||||
case DATA_TYPE_UINT8:
|
||||
if (data_size != 1) {
|
||||
fprintf(stderr,
|
||||
"warning: expected size of 1, got %zu\n",
|
||||
data_size);
|
||||
}
|
||||
|
||||
if (data_size >= 1)
|
||||
printf("%u\n", *(uint8_t *)data);
|
||||
break;
|
||||
case DATA_TYPE_UINT16:
|
||||
if (data_size != 2) {
|
||||
fprintf(stderr,
|
||||
"warning: expected size of 2, got %zu\n",
|
||||
data_size);
|
||||
}
|
||||
|
||||
if (data_size >= 2)
|
||||
printf("%u\n", *(uint16_t *)data);
|
||||
break;
|
||||
case DATA_TYPE_UINT32:
|
||||
if (data_size != 4) {
|
||||
fprintf(stderr,
|
||||
"warning: expected size of 4, got %zu\n",
|
||||
data_size);
|
||||
}
|
||||
|
||||
if (data_size >= 4)
|
||||
printf("%u\n", *(uint32_t *)data);
|
||||
break;
|
||||
case DATA_TYPE_ASCII:
|
||||
for (size_t i = 0; i < data_size; ++i) {
|
||||
char c = data[i];
|
||||
if (isprint(c))
|
||||
printf("%c", c);
|
||||
}
|
||||
printf("\n");
|
||||
break;
|
||||
case DATA_TYPE_UNICODE:
|
||||
char *chars = to_chars((const CHAR16 *)data, data_size);
|
||||
printf("%s\n", chars);
|
||||
free(chars);
|
||||
break;
|
||||
case DATA_TYPE_RAW:
|
||||
fwrite(data, 1, data_size, stdout);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static uint64_t parse_uint(const char source[],
|
||||
const char type[],
|
||||
unsigned long long max)
|
||||
{
|
||||
char *end;
|
||||
unsigned long long uint = strtoull(source, &end, /*base=*/0);
|
||||
if (*end != '\0') {
|
||||
fprintf(stderr, "Trailing characters in \"%s\": %s\n",
|
||||
source, end);
|
||||
return UINT64_MAX;
|
||||
}
|
||||
if (uint > max) {
|
||||
fprintf(stderr, "Invalid %s value: %llu\n", type, uint);
|
||||
return UINT64_MAX;
|
||||
}
|
||||
|
||||
return uint;
|
||||
}
|
||||
|
||||
void *make_data(const char source[], size_t *data_size, enum data_type type)
|
||||
{
|
||||
switch (type) {
|
||||
void *data;
|
||||
bool boolean;
|
||||
unsigned long long uint;
|
||||
|
||||
case DATA_TYPE_BOOL:
|
||||
if (str_eq(source, "true")) {
|
||||
boolean = true;
|
||||
} else if (str_eq(source, "false")) {
|
||||
boolean = false;
|
||||
} else {
|
||||
fprintf(stderr, "Invalid boolean value: \"%s\"\n",
|
||||
source);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
*data_size = 1;
|
||||
data = xmalloc(*data_size);
|
||||
*(uint8_t *)data = boolean;
|
||||
return data;
|
||||
case DATA_TYPE_UINT8:
|
||||
uint = parse_uint(source, "uint8", UINT8_MAX);
|
||||
if (uint == UINT64_MAX)
|
||||
return NULL;
|
||||
|
||||
*data_size = 1;
|
||||
data = xmalloc(*data_size);
|
||||
*(uint8_t *)data = uint;
|
||||
return data;
|
||||
case DATA_TYPE_UINT16:
|
||||
uint = parse_uint(source, "uint16", UINT16_MAX);
|
||||
if (uint == UINT64_MAX)
|
||||
return NULL;
|
||||
|
||||
*data_size = 2;
|
||||
data = xmalloc(*data_size);
|
||||
*(uint16_t *)data = uint;
|
||||
return data;
|
||||
case DATA_TYPE_UINT32:
|
||||
uint = parse_uint(source, "uint32", UINT32_MAX);
|
||||
if (uint == UINT64_MAX)
|
||||
return NULL;
|
||||
|
||||
*data_size = 4;
|
||||
data = xmalloc(*data_size);
|
||||
*(uint32_t *)data = uint;
|
||||
return data;
|
||||
case DATA_TYPE_ASCII:
|
||||
*data_size = strlen(source) + 1;
|
||||
return strdup(source);
|
||||
case DATA_TYPE_UNICODE:
|
||||
return to_uchars(source, data_size);
|
||||
case DATA_TYPE_RAW:
|
||||
fprintf(stderr, "Raw data type is output only\n");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
bool parse_data_type(const char str[], enum data_type *type)
|
||||
{
|
||||
if (str_eq(str, "bool"))
|
||||
*type = DATA_TYPE_BOOL;
|
||||
else if (str_eq(str, "uint8"))
|
||||
*type = DATA_TYPE_UINT8;
|
||||
else if (str_eq(str, "uint16"))
|
||||
*type = DATA_TYPE_UINT16;
|
||||
else if (str_eq(str, "uint32"))
|
||||
*type = DATA_TYPE_UINT32;
|
||||
else if (str_eq(str, "ascii"))
|
||||
*type = DATA_TYPE_ASCII;
|
||||
else if (str_eq(str, "unicode"))
|
||||
*type = DATA_TYPE_UNICODE;
|
||||
else if (str_eq(str, "raw"))
|
||||
*type = DATA_TYPE_RAW;
|
||||
else
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
26
util/smmstoretool/data.h
Normal file
26
util/smmstoretool/data.h
Normal file
@ -0,0 +1,26 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__DATA_H__
|
||||
#define SMMSTORETOOL__DATA_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
enum data_type {
|
||||
DATA_TYPE_BOOL,
|
||||
DATA_TYPE_UINT8,
|
||||
DATA_TYPE_UINT16,
|
||||
DATA_TYPE_UINT32,
|
||||
DATA_TYPE_ASCII,
|
||||
DATA_TYPE_UNICODE,
|
||||
DATA_TYPE_RAW,
|
||||
};
|
||||
|
||||
void print_data(const uint8_t data[], size_t data_size, enum data_type type);
|
||||
|
||||
void *make_data(const char source[], size_t *data_size, enum data_type type);
|
||||
|
||||
bool parse_data_type(const char str[], enum data_type *type);
|
||||
|
||||
#endif // SMMSTORETOOL__DATA_H__
|
1
util/smmstoretool/description.md
Normal file
1
util/smmstoretool/description.md
Normal file
@ -0,0 +1 @@
|
||||
Offline SMMSTORE variable modification tool `C`
|
175
util/smmstoretool/fv.c
Normal file
175
util/smmstoretool/fv.c
Normal file
@ -0,0 +1,175 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "fv.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
|
||||
// The same as in `smmstore.h` header, which isn't in `commonlib`.
|
||||
#define SMM_BLOCK_SIZE (64*1024)
|
||||
|
||||
static const EFI_GUID EfiVariableGuid = EFI_VARIABLE_GUID;
|
||||
|
||||
static const EFI_GUID EfiAuthenticatedVariableGuid =
|
||||
EFI_AUTHENTICATED_VARIABLE_GUID;
|
||||
|
||||
static const EFI_GUID EfiSystemNvDataFvGuid = {
|
||||
0xfff12b8d, 0x7696, 0x4c8b,
|
||||
{ 0xa9, 0x85, 0x27, 0x47, 0x07, 0x5b, 0x4f, 0x50 }
|
||||
};
|
||||
|
||||
static uint16_t calc_checksum(const uint16_t *hdr, size_t size)
|
||||
{
|
||||
assert(size % 2 == 0 && "Header can't have odd length.");
|
||||
|
||||
uint16_t checksum = 0;
|
||||
for (size_t i = 0; i < size / 2; ++i)
|
||||
checksum += hdr[i];
|
||||
return checksum;
|
||||
}
|
||||
|
||||
bool
|
||||
fv_init(struct mem_range_t fv)
|
||||
{
|
||||
if (fv.length % SMM_BLOCK_SIZE != 0) {
|
||||
fprintf(stderr,
|
||||
"Firmware Volume size is not a multiple of 64KiB\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
memset(fv.start, 0xff, fv.length);
|
||||
|
||||
const EFI_FIRMWARE_VOLUME_HEADER vol_hdr = {
|
||||
.FileSystemGuid = EfiSystemNvDataFvGuid,
|
||||
.FvLength = fv.length,
|
||||
.Signature = EFI_FVH_SIGNATURE,
|
||||
.Attributes = EFI_FVB2_READ_ENABLED_CAP
|
||||
| EFI_FVB2_READ_STATUS
|
||||
| EFI_FVB2_WRITE_ENABLED_CAP
|
||||
| EFI_FVB2_WRITE_STATUS
|
||||
| EFI_FVB2_STICKY_WRITE
|
||||
| EFI_FVB2_MEMORY_MAPPED
|
||||
| EFI_FVB2_ERASE_POLARITY,
|
||||
.HeaderLength = sizeof(vol_hdr)
|
||||
+ sizeof(EFI_FV_BLOCK_MAP_ENTRY),
|
||||
.Revision = EFI_FVH_REVISION,
|
||||
.BlockMap[0] = {
|
||||
.NumBlocks = fv.length / SMM_BLOCK_SIZE,
|
||||
.Length = SMM_BLOCK_SIZE,
|
||||
},
|
||||
};
|
||||
|
||||
EFI_FIRMWARE_VOLUME_HEADER *vol_hdr_dst = (void *)fv.start;
|
||||
*vol_hdr_dst = vol_hdr;
|
||||
vol_hdr_dst->BlockMap[1].NumBlocks = 0;
|
||||
vol_hdr_dst->BlockMap[1].Length = 0;
|
||||
|
||||
vol_hdr_dst->Checksum =
|
||||
~calc_checksum((const void *)vol_hdr_dst, vol_hdr.HeaderLength);
|
||||
++vol_hdr_dst->Checksum;
|
||||
|
||||
const VARIABLE_STORE_HEADER var_store_hdr = {
|
||||
// Authentication-related fields will be filled with 0xff.
|
||||
.Signature = EfiAuthenticatedVariableGuid,
|
||||
// Actual size of the storage is block size, the rest is
|
||||
// Fault Tolerant Write (FTW) space and the FTW spare space.
|
||||
.Size = SMM_BLOCK_SIZE - vol_hdr.HeaderLength,
|
||||
.Format = VARIABLE_STORE_FORMATTED,
|
||||
.State = VARIABLE_STORE_HEALTHY,
|
||||
};
|
||||
|
||||
VARIABLE_STORE_HEADER *var_store_hdr_dst =
|
||||
(void *)(fv.start + vol_hdr.HeaderLength);
|
||||
*var_store_hdr_dst = var_store_hdr;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool guid_eq(const EFI_GUID *lhs, const EFI_GUID *rhs)
|
||||
{
|
||||
return memcmp(lhs, rhs, sizeof(*lhs)) == 0;
|
||||
}
|
||||
|
||||
static bool check_fw_vol_hdr(const EFI_FIRMWARE_VOLUME_HEADER *hdr,
|
||||
size_t max_size)
|
||||
{
|
||||
if (hdr->Revision != EFI_FVH_REVISION ||
|
||||
hdr->Signature != EFI_FVH_SIGNATURE ||
|
||||
hdr->FvLength > max_size ||
|
||||
hdr->HeaderLength > max_size ||
|
||||
hdr->HeaderLength % 2 != 0) {
|
||||
fprintf(stderr, "No firmware volume header present\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!guid_eq(&hdr->FileSystemGuid, &EfiSystemNvDataFvGuid)) {
|
||||
fprintf(stderr, "Firmware volume GUID non-compatible\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t checksum = calc_checksum((const void *)hdr, hdr->HeaderLength);
|
||||
if (checksum != 0) {
|
||||
fprintf(stderr,
|
||||
"Firmware Volume checksum is non-zero: 0x%04X\n",
|
||||
checksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool check_var_store_hdr(const VARIABLE_STORE_HEADER *hdr,
|
||||
size_t max_size,
|
||||
bool *auth_vars)
|
||||
{
|
||||
*auth_vars = guid_eq(&hdr->Signature, &EfiAuthenticatedVariableGuid);
|
||||
if (!*auth_vars && !guid_eq(&hdr->Signature, &EfiVariableGuid)) {
|
||||
fprintf(stderr, "Variable store has unexpected GUID\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hdr->Size > max_size) {
|
||||
fprintf(stderr, "Variable store size is too large: %zu > %zu\n",
|
||||
(size_t)hdr->Size, max_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hdr->Format != VARIABLE_STORE_FORMATTED) {
|
||||
fprintf(stderr, "Variable store is not formatted\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hdr->State != VARIABLE_STORE_HEALTHY) {
|
||||
fprintf(stderr, "Variable store is not in a healthy state\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
fv_parse(struct mem_range_t fv, struct mem_range_t *var_store, bool *auth_vars)
|
||||
{
|
||||
const EFI_FIRMWARE_VOLUME_HEADER *vol_hdr = (void *)fv.start;
|
||||
if (!check_fw_vol_hdr(vol_hdr, fv.length)) {
|
||||
fprintf(stderr, "No valid firmware volume was found\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t *fw_vol_data = fv.start + vol_hdr->HeaderLength;
|
||||
size_t volume_size = fv.length - vol_hdr->HeaderLength;
|
||||
const VARIABLE_STORE_HEADER *var_store_hdr = (void *)fw_vol_data;
|
||||
if (!check_var_store_hdr(var_store_hdr, volume_size, auth_vars)) {
|
||||
fprintf(stderr, "No valid variable store was found");
|
||||
return false;
|
||||
}
|
||||
|
||||
var_store->start = fw_vol_data + sizeof(*var_store_hdr);
|
||||
var_store->length = volume_size - sizeof(*var_store_hdr);
|
||||
return true;
|
||||
}
|
19
util/smmstoretool/fv.h
Normal file
19
util/smmstoretool/fv.h
Normal file
@ -0,0 +1,19 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__FV_H__
|
||||
#define SMMSTORETOOL__FV_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
// Firmware volume is what's stored in SMMSTORE region of CBFS. It wraps
|
||||
// variable store.
|
||||
|
||||
bool fv_init(struct mem_range_t fv);
|
||||
|
||||
bool fv_parse(struct mem_range_t fv,
|
||||
struct mem_range_t *var_store,
|
||||
bool *auth_vars);
|
||||
|
||||
#endif // SMMSTORETOOL__FV_H__
|
89
util/smmstoretool/guids.c
Normal file
89
util/smmstoretool/guids.c
Normal file
@ -0,0 +1,89 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "guids.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <commonlib/bsd/helpers.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
#include "utils.h"
|
||||
|
||||
const struct guid_alias_t known_guids[] = {
|
||||
{
|
||||
"coreboot",
|
||||
{
|
||||
0xceae4c1d, 0x335b, 0x4685,
|
||||
{ 0xa4, 0xa0, 0xfc, 0x4a, 0x94, 0xee, 0xa0, 0x85 }
|
||||
},
|
||||
},
|
||||
{
|
||||
"dasharo",
|
||||
{
|
||||
0xd15b327e, 0xff2d, 0x4fc1,
|
||||
{ 0xab, 0xf6, 0xc1, 0x2b, 0xd0, 0x8c, 0x13, 0x59 }
|
||||
},
|
||||
},
|
||||
{
|
||||
"global",
|
||||
{
|
||||
0x8be4df61, 0x93ca, 0x11d2,
|
||||
{ 0xaa, 0x0d, 0x00, 0xe0, 0x98, 0x03, 0x2b, 0x8c }
|
||||
},
|
||||
},
|
||||
{
|
||||
"microsoft",
|
||||
{
|
||||
0x77fa9abd, 0x0359, 0x4d32,
|
||||
{ 0xbd, 0x60, 0x28, 0xf4, 0xe7, 0x8f, 0x78, 0x4b }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const int known_guid_count = ARRAY_SIZE(known_guids);
|
||||
|
||||
char *format_guid(const EFI_GUID *guid, bool use_alias)
|
||||
{
|
||||
if (use_alias) {
|
||||
for (int i = 0; i < known_guid_count; ++i) {
|
||||
const struct guid_alias_t *known_guid = &known_guids[i];
|
||||
if (memcmp(&known_guid->guid, guid, sizeof(*guid)) == 0)
|
||||
return strdup(known_guid->alias);
|
||||
}
|
||||
}
|
||||
|
||||
char *str = xmalloc(GUID_LEN + 1);
|
||||
snprintf(str, GUID_LEN + 1,
|
||||
"%08x-%04x-%04x-%02x%02x%02x%02x%02x%02x%02x%02x",
|
||||
guid->Data1, guid->Data2, guid->Data3,
|
||||
guid->Data4[0], guid->Data4[1],
|
||||
guid->Data4[2], guid->Data4[3],
|
||||
guid->Data4[4], guid->Data4[5],
|
||||
guid->Data4[6], guid->Data4[7]);
|
||||
return str;
|
||||
}
|
||||
|
||||
bool parse_guid(const char str[], EFI_GUID *guid)
|
||||
{
|
||||
for (int i = 0; i < known_guid_count; ++i) {
|
||||
const struct guid_alias_t *known_guid = &known_guids[i];
|
||||
if (str_eq(known_guid->alias, str)) {
|
||||
*guid = known_guid->guid;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(str) != GUID_LEN)
|
||||
return false;
|
||||
|
||||
int n = sscanf(str,
|
||||
"%08x-%04hx-%04hx-"
|
||||
"%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx%02hhx",
|
||||
&guid->Data1, &guid->Data2, &guid->Data3,
|
||||
&guid->Data4[0], &guid->Data4[1],
|
||||
&guid->Data4[2], &guid->Data4[3],
|
||||
&guid->Data4[4], &guid->Data4[5],
|
||||
&guid->Data4[6], &guid->Data4[7]);
|
||||
return n == 11;
|
||||
}
|
25
util/smmstoretool/guids.h
Normal file
25
util/smmstoretool/guids.h
Normal file
@ -0,0 +1,25 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__GUIDS_H__
|
||||
#define SMMSTORETOOL__GUIDS_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
|
||||
#define GUID_LEN 35
|
||||
|
||||
struct guid_alias_t {
|
||||
const char *alias;
|
||||
EFI_GUID guid;
|
||||
};
|
||||
|
||||
extern const struct guid_alias_t known_guids[];
|
||||
|
||||
extern const int known_guid_count;
|
||||
|
||||
char *format_guid(const EFI_GUID *guid, bool use_alias);
|
||||
|
||||
bool parse_guid(const char str[], EFI_GUID *guid);
|
||||
|
||||
#endif // SMMSTORETOOL__GUIDS_H__
|
487
util/smmstoretool/main.c
Normal file
487
util/smmstoretool/main.c
Normal file
@ -0,0 +1,487 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <commonlib/bsd/helpers.h>
|
||||
|
||||
#include "data.h"
|
||||
#include "guids.h"
|
||||
#include "storage.h"
|
||||
#include "udk2017.h"
|
||||
#include "vs.h"
|
||||
|
||||
struct subcommand_t {
|
||||
const char *name;
|
||||
const char *description;
|
||||
void (*print_help)(FILE *f, const struct subcommand_t *info);
|
||||
int (*process)(int argc, char *argv[], const char store_file[]);
|
||||
};
|
||||
|
||||
static void help_get(FILE *f, const struct subcommand_t *info);
|
||||
static void help_guids(FILE *f, const struct subcommand_t *info);
|
||||
static void help_help(FILE *f, const struct subcommand_t *info);
|
||||
static void help_list(FILE *f, const struct subcommand_t *info);
|
||||
static void help_remove(FILE *f, const struct subcommand_t *info);
|
||||
static void help_set(FILE *f, const struct subcommand_t *info);
|
||||
static int process_get(int argc, char *argv[], const char store_file[]);
|
||||
static int process_guids(int argc, char *argv[], const char store_file[]);
|
||||
static int process_help(int argc, char *argv[], const char store_file[]);
|
||||
static int process_list(int argc, char *argv[], const char store_file[]);
|
||||
static int process_remove(int argc, char *argv[], const char store_file[]);
|
||||
static int process_set(int argc, char *argv[], const char store_file[]);
|
||||
|
||||
static const struct subcommand_t sub_commands[] = {
|
||||
{
|
||||
.name = "get",
|
||||
.description = "display current value of a variable",
|
||||
.print_help = &help_get,
|
||||
.process = &process_get,
|
||||
},
|
||||
{
|
||||
.name = "guids",
|
||||
.description = "show GUID to alias mapping",
|
||||
.print_help = &help_guids,
|
||||
.process = &process_guids,
|
||||
},
|
||||
{
|
||||
.name = "help",
|
||||
.description = "provide built-in help",
|
||||
.print_help = &help_help,
|
||||
.process = &process_help,
|
||||
},
|
||||
{
|
||||
.name = "list",
|
||||
.description = "list variables present in the store",
|
||||
.print_help = &help_list,
|
||||
.process = &process_list,
|
||||
},
|
||||
{
|
||||
.name = "remove",
|
||||
.description = "remove a variable from the store",
|
||||
.print_help = &help_remove,
|
||||
.process = &process_remove,
|
||||
},
|
||||
{
|
||||
.name = "set",
|
||||
.description = "add or updates a variable in the store",
|
||||
.print_help = &help_set,
|
||||
.process = &process_set,
|
||||
},
|
||||
};
|
||||
|
||||
static const int sub_command_count = ARRAY_SIZE(sub_commands);
|
||||
|
||||
static const char *USAGE_FMT = "Usage: %s smm-store-file sub-command\n"
|
||||
" %s -h|--help\n";
|
||||
|
||||
static const char *program_name;
|
||||
|
||||
static void print_program_usage(void)
|
||||
{
|
||||
fprintf(stderr, USAGE_FMT, program_name, program_name);
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void print_sub_command_usage(const char sub_command[])
|
||||
{
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, USAGE_FMT, program_name, program_name);
|
||||
fprintf(stderr, "\n");
|
||||
|
||||
for (int i = 0; i < sub_command_count; ++i) {
|
||||
const struct subcommand_t *cmd = &sub_commands[i];
|
||||
if (!str_eq(cmd->name, sub_command))
|
||||
continue;
|
||||
|
||||
cmd->print_help(stderr, cmd);
|
||||
break;
|
||||
}
|
||||
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
static void print_help(void)
|
||||
{
|
||||
printf(USAGE_FMT, program_name, program_name);
|
||||
|
||||
printf("\n");
|
||||
printf("Sub-commands:\n");
|
||||
for (int i = 0; i < sub_command_count; ++i) {
|
||||
const struct subcommand_t *cmd = &sub_commands[i];
|
||||
printf(" * %-6s - %s\n", cmd->name, cmd->description);
|
||||
}
|
||||
}
|
||||
|
||||
static void print_types(FILE *f)
|
||||
{
|
||||
fprintf(f, "Types and their values:\n");
|
||||
fprintf(f, " * bool (true, false)\n");
|
||||
fprintf(f, " * uint8 (0-255)\n");
|
||||
fprintf(f, " * ascii (NUL-terminated)\n");
|
||||
fprintf(f, " * unicode (widened and NUL-terminated)\n");
|
||||
fprintf(f, " * raw (output only; raw bytes on output)\n");
|
||||
}
|
||||
|
||||
static void help_set(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "Create or update a variable:\n");
|
||||
fprintf(f, " %s smm-store-file %s \\\n", program_name, info->name);
|
||||
fprintf(f, " -g vendor-guid \\\n");
|
||||
fprintf(f, " -n variable-name \\\n");
|
||||
fprintf(f, " -t variable-type \\\n");
|
||||
fprintf(f, " -v value\n");
|
||||
fprintf(f, "\n");
|
||||
print_types(f);
|
||||
}
|
||||
|
||||
static int process_set(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
const char *name = NULL;
|
||||
const char *value = NULL;
|
||||
const char *type_str = NULL;
|
||||
const char *guid_str = NULL;
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "n:t:v:g:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
name = optarg;
|
||||
break;
|
||||
case 't':
|
||||
type_str = optarg;
|
||||
break;
|
||||
case 'v':
|
||||
value = optarg;
|
||||
break;
|
||||
case 'g':
|
||||
guid_str = optarg;
|
||||
break;
|
||||
|
||||
case '?': /* parsing error */
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (argv[optind] != NULL) {
|
||||
fprintf(stderr, "First unexpected positional argument: %s\n",
|
||||
argv[optind]);
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
if (name == NULL || value == NULL || type_str == NULL ||
|
||||
guid_str == NULL) {
|
||||
fprintf(stderr, "All options are required\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
if (name[0] == '\0') {
|
||||
fprintf(stderr, "Variable name can't be empty\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
EFI_GUID guid;
|
||||
if (!parse_guid(guid_str, &guid)) {
|
||||
fprintf(stderr, "Failed to parse GUID: %s\n", guid_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
enum data_type type;
|
||||
if (!parse_data_type(type_str, &type)) {
|
||||
fprintf(stderr, "Failed to parse type: %s\n", type_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
size_t data_size;
|
||||
void *data = make_data(value, &data_size, type);
|
||||
if (data == NULL) {
|
||||
fprintf(stderr, "Failed to parse value \"%s\" as %s\n",
|
||||
value, type_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
struct storage_t storage;
|
||||
if (!storage_open(store_file, &storage, /*rw=*/true)) {
|
||||
free(data);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
struct var_t *var = vs_find(&storage.vs, name, &guid);
|
||||
if (var == NULL) {
|
||||
var = vs_new_var(&storage.vs);
|
||||
var->name = to_uchars(name, &var->name_size);
|
||||
var->data = data;
|
||||
var->data_size = data_size;
|
||||
var->guid = guid;
|
||||
} else {
|
||||
free(var->data);
|
||||
var->data = data;
|
||||
var->data_size = data_size;
|
||||
}
|
||||
|
||||
return storage_write_back(&storage) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
||||
|
||||
static void help_list(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "List variables in the store:\n");
|
||||
fprintf(f, " %s smm-store-file %s\n", program_name, info->name);
|
||||
}
|
||||
|
||||
static int process_list(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
if (argc != 1) {
|
||||
fprintf(stderr, "Invalid invocation\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
struct storage_t storage;
|
||||
if (!storage_open(store_file, &storage, /*rw=*/false))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
for (struct var_t *v = storage.vs.vars; v != NULL; v = v->next) {
|
||||
char *name = to_chars(v->name, v->name_size);
|
||||
char *guid = format_guid(&v->guid, /*use_alias=*/true);
|
||||
|
||||
printf("%-*s:%s (%zu %s)\n", GUID_LEN, guid, name, v->data_size,
|
||||
v->data_size == 1 ? "byte" : "bytes");
|
||||
|
||||
free(name);
|
||||
free(guid);
|
||||
}
|
||||
|
||||
storage_drop(&storage);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
static void help_get(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "Read variable's value:\n");
|
||||
fprintf(f, " %s smm-store-file %s \\\n", program_name, info->name);
|
||||
fprintf(f, " -g vendor-guid \\\n");
|
||||
fprintf(f, " -n variable-name \\\n");
|
||||
fprintf(f, " -t variable-type\n");
|
||||
fprintf(f, "\n");
|
||||
print_types(f);
|
||||
}
|
||||
|
||||
static int process_get(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
const char *name = NULL;
|
||||
const char *type_str = NULL;
|
||||
const char *guid_str = NULL;
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "n:g:t:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
name = optarg;
|
||||
break;
|
||||
case 'g':
|
||||
guid_str = optarg;
|
||||
break;
|
||||
case 't':
|
||||
type_str = optarg;
|
||||
break;
|
||||
|
||||
case '?': /* parsing error */
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (name == NULL || type_str == NULL || guid_str == NULL) {
|
||||
fprintf(stderr, "All options are required\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
EFI_GUID guid;
|
||||
if (!parse_guid(guid_str, &guid)) {
|
||||
fprintf(stderr, "Failed to parse GUID: %s\n", guid_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
enum data_type type;
|
||||
if (!parse_data_type(type_str, &type)) {
|
||||
fprintf(stderr, "Failed to parse type: %s\n", type_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
struct storage_t storage;
|
||||
if (!storage_open(store_file, &storage, /*rw=*/false))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
int result = EXIT_SUCCESS;
|
||||
|
||||
struct var_t *var = vs_find(&storage.vs, name, &guid);
|
||||
if (var == NULL) {
|
||||
result = EXIT_FAILURE;
|
||||
fprintf(stderr, "Couldn't find variable \"%s:%s\"\n",
|
||||
guid_str, name);
|
||||
} else if (var->data_size == 0) {
|
||||
fprintf(stderr, "There is no data to show.\n");
|
||||
result = EXIT_FAILURE;
|
||||
} else {
|
||||
print_data(var->data, var->data_size, type);
|
||||
}
|
||||
|
||||
storage_drop(&storage);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void help_help(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "Display generic help:\n");
|
||||
fprintf(f, " %s smm-store-file %s\n", program_name, info->name);
|
||||
fprintf(f, "\n");
|
||||
fprintf(f, "Display sub-command help:\n");
|
||||
fprintf(f, " %s smm-store-file %s sub-command\n",
|
||||
program_name, info->name);
|
||||
}
|
||||
|
||||
static int process_help(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
(void)store_file;
|
||||
|
||||
if (argc == 1) {
|
||||
print_help();
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "Invalid invocation\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
const char *sub_command = argv[1];
|
||||
|
||||
for (int i = 0; i < sub_command_count; ++i) {
|
||||
const struct subcommand_t *cmd = &sub_commands[i];
|
||||
if (!str_eq(cmd->name, sub_command))
|
||||
continue;
|
||||
|
||||
cmd->print_help(stdout, cmd);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
fprintf(stderr, "Unknown sub-command: %s\n", sub_command);
|
||||
print_help();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
static void help_remove(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "Remove a variable:\n");
|
||||
fprintf(f, " %s smm-store-file %s \\\n", program_name, info->name);
|
||||
fprintf(f, " -g vendor-guid \\\n");
|
||||
fprintf(f, " -n variable-name\n");
|
||||
}
|
||||
|
||||
static int process_remove(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
const char *name = NULL;
|
||||
const char *guid_str = NULL;
|
||||
int opt;
|
||||
while ((opt = getopt(argc, argv, "n:g:")) != -1) {
|
||||
switch (opt) {
|
||||
case 'n':
|
||||
name = optarg;
|
||||
break;
|
||||
case 'g':
|
||||
guid_str = optarg;
|
||||
break;
|
||||
|
||||
case '?': /* parsing error */
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
if (name == NULL || guid_str == NULL) {
|
||||
fprintf(stderr, "All options are required\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
EFI_GUID guid;
|
||||
if (!parse_guid(guid_str, &guid)) {
|
||||
fprintf(stderr, "Failed to parse GUID: %s\n", guid_str);
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
struct storage_t storage;
|
||||
if (!storage_open(store_file, &storage, /*rw=*/true))
|
||||
return EXIT_FAILURE;
|
||||
|
||||
int result = EXIT_SUCCESS;
|
||||
|
||||
struct var_t *var = vs_find(&storage.vs, name, &guid);
|
||||
if (var == NULL) {
|
||||
result = EXIT_FAILURE;
|
||||
fprintf(stderr, "Couldn't find variable \"%s:%s\"\n",
|
||||
guid_str, name);
|
||||
} else {
|
||||
vs_delete(&storage.vs, var);
|
||||
}
|
||||
|
||||
storage_write_back(&storage);
|
||||
return result;
|
||||
}
|
||||
|
||||
static void help_guids(FILE *f, const struct subcommand_t *info)
|
||||
{
|
||||
fprintf(f, "List recognized GUIDS:\n");
|
||||
fprintf(f, " %s smm-store-file %s\n", program_name, info->name);
|
||||
}
|
||||
|
||||
static int process_guids(int argc, char *argv[], const char store_file[])
|
||||
{
|
||||
(void)store_file;
|
||||
|
||||
if (argc != 1) {
|
||||
fprintf(stderr, "Invalid invocation\n");
|
||||
print_sub_command_usage(argv[0]);
|
||||
}
|
||||
|
||||
for (int i = 0; i < known_guid_count; ++i) {
|
||||
char *guid = format_guid(&known_guids[i].guid,
|
||||
/*use_alias=*/false);
|
||||
printf("%-10s -> %s\n", known_guids[i].alias, guid);
|
||||
free(guid);
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
program_name = argv[0];
|
||||
|
||||
if (argc > 1 && (str_eq(argv[1], "-h") || str_eq(argv[1], "--help"))) {
|
||||
print_help();
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
|
||||
if (argc < 3)
|
||||
print_program_usage();
|
||||
|
||||
const char *store_file = argv[1];
|
||||
const char *sub_command = argv[2];
|
||||
|
||||
int sub_command_argc = argc - 2;
|
||||
char **sub_command_argv = argv + 2;
|
||||
|
||||
for (int i = 0; i < sub_command_count; ++i) {
|
||||
const struct subcommand_t *cmd = &sub_commands[i];
|
||||
if (!str_eq(cmd->name, sub_command))
|
||||
continue;
|
||||
|
||||
return cmd->process(sub_command_argc,
|
||||
sub_command_argv,
|
||||
store_file);
|
||||
}
|
||||
|
||||
fprintf(stderr, "Unknown sub-command: %s\n", sub_command);
|
||||
print_help();
|
||||
return EXIT_FAILURE;
|
||||
}
|
73
util/smmstoretool/storage.c
Normal file
73
util/smmstoretool/storage.c
Normal file
@ -0,0 +1,73 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "storage.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "fv.h"
|
||||
#include "utils.h"
|
||||
|
||||
bool storage_open(const char store_file[], struct storage_t *storage, bool rw)
|
||||
{
|
||||
storage->rw = rw;
|
||||
|
||||
storage->file = map_file(store_file, rw);
|
||||
if (storage->file.start == NULL) {
|
||||
fprintf(stderr, "Failed to load smm-store-file \"%s\"\n",
|
||||
store_file);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool auth_vars;
|
||||
if (!fv_parse(storage->file, &storage->store_area, &auth_vars)) {
|
||||
if (!rw) {
|
||||
fprintf(stderr,
|
||||
"Failed to find variable store in \"%s\"\n",
|
||||
store_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (!fv_init(storage->file)) {
|
||||
fprintf(stderr,
|
||||
"Failed to create variable store in \"%s\"\n",
|
||||
store_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
if (!fv_parse(storage->file, &storage->store_area, &auth_vars)) {
|
||||
fprintf(stderr,
|
||||
"Failed to parse newly formatted store in \"%s\"\n",
|
||||
store_file);
|
||||
goto error;
|
||||
}
|
||||
|
||||
fprintf(stderr,
|
||||
"Successfully created variable store in \"%s\"\n",
|
||||
store_file);
|
||||
}
|
||||
|
||||
storage->vs = vs_load(storage->store_area, auth_vars);
|
||||
return true;
|
||||
|
||||
error:
|
||||
unmap_file(storage->file);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool storage_write_back(struct storage_t *storage)
|
||||
{
|
||||
assert(storage->rw && "Only RW storage can be updated.");
|
||||
|
||||
bool success = vs_store(&storage->vs, storage->store_area);
|
||||
if (!success)
|
||||
fprintf(stderr, "Failed to update variable store\n");
|
||||
storage_drop(storage);
|
||||
return success;
|
||||
}
|
||||
|
||||
void storage_drop(struct storage_t *storage)
|
||||
{
|
||||
unmap_file(storage->file);
|
||||
vs_free(&storage->vs);
|
||||
}
|
22
util/smmstoretool/storage.h
Normal file
22
util/smmstoretool/storage.h
Normal file
@ -0,0 +1,22 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__STORAGE_H__
|
||||
#define SMMSTORETOOL__STORAGE_H__
|
||||
|
||||
#include "vs.h"
|
||||
#include "utils.h"
|
||||
|
||||
struct storage_t {
|
||||
bool rw;
|
||||
struct mem_range_t file;
|
||||
struct mem_range_t store_area;
|
||||
struct var_store_t vs;
|
||||
};
|
||||
|
||||
bool storage_open(const char store_file[], struct storage_t *storage, bool rw);
|
||||
|
||||
bool storage_write_back(struct storage_t *storage);
|
||||
|
||||
void storage_drop(struct storage_t *storage);
|
||||
|
||||
#endif // SMMSTORETOOL__STORAGE_H__
|
30
util/smmstoretool/udk2017.h
Normal file
30
util/smmstoretool/udk2017.h
Normal file
@ -0,0 +1,30 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__UDK2017_H__
|
||||
#define SMMSTORETOOL__UDK2017_H__
|
||||
|
||||
#include <UDK2017/MdePkg/Include/Uefi/UefiBaseType.h>
|
||||
#include <UDK2017/MdePkg/Include/Uefi/UefiMultiPhase.h>
|
||||
#include <UDK2017/MdePkg/Include/Pi/PiFirmwareVolume.h>
|
||||
#include <UDK2017/MdeModulePkg/Include/Guid/VariableFormat.h>
|
||||
|
||||
/*
|
||||
* ProcessorBind.h contains `#pragma GCC visibility push(hidden)` guarded by an
|
||||
* identical condition, but there is no corresponding `pop` pragma. This can
|
||||
* cause trouble for code following headers above including libc headers because
|
||||
* linker suddenly considers declarations from them (e.g., `strcmp()`) to be
|
||||
* hidden.
|
||||
*
|
||||
* In order to address this situation all UDK2017 headers used by this tool
|
||||
* must be listed above and included indirectly through this header which
|
||||
* restores default visibility.
|
||||
*
|
||||
* Mind that this issue appears only if the following conditions are satisfied
|
||||
* and not all toolchains are configured to build position-independent code by
|
||||
* default (as if -fpic or -fpie appears on compilation command-line).
|
||||
*/
|
||||
#if defined(__GNUC__) && defined(__pic__) && !defined(USING_LTO)
|
||||
#pragma GCC visibility pop
|
||||
#endif
|
||||
|
||||
#endif // SMMSTORETOOL__UDK2017_H__
|
111
util/smmstoretool/utils.c
Normal file
111
util/smmstoretool/utils.c
Normal file
@ -0,0 +1,111 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "utils.h"
|
||||
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
void *xmalloc(size_t size)
|
||||
{
|
||||
void *p = malloc(size);
|
||||
if (p == NULL) {
|
||||
fprintf(stderr, "Failed to allocate memory\n");
|
||||
abort();
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
char *to_chars(const CHAR16 uchars[], size_t size)
|
||||
{
|
||||
char *chars = xmalloc(size / 2 + 1);
|
||||
|
||||
const CHAR16 *from = uchars;
|
||||
char *to = chars;
|
||||
while (*from != 0) {
|
||||
CHAR16 uc = *from++;
|
||||
if (uc < CHAR_MAX)
|
||||
*to++ = uc;
|
||||
else
|
||||
*to++ = '?';
|
||||
}
|
||||
|
||||
// In case there was no terminating NUL.
|
||||
if (to != chars && to[-1] != '\0')
|
||||
*to = '\0';
|
||||
|
||||
return chars;
|
||||
}
|
||||
|
||||
CHAR16 *to_uchars(const char chars[], size_t *size)
|
||||
{
|
||||
*size = (strlen(chars) + 1) * 2;
|
||||
CHAR16 *uchars = xmalloc(*size);
|
||||
|
||||
const char *from = chars;
|
||||
CHAR16 *to = uchars;
|
||||
while (*from != '\0')
|
||||
*to++ = *from++;
|
||||
*to = 0;
|
||||
|
||||
return uchars;
|
||||
}
|
||||
|
||||
bool str_eq(const char lhs[], const char rhs[])
|
||||
{
|
||||
return strcmp(lhs, rhs) == 0;
|
||||
}
|
||||
|
||||
struct mem_range_t map_file(const char path[], bool rw)
|
||||
{
|
||||
struct mem_range_t range = {0};
|
||||
|
||||
int open_flags = rw ? O_RDWR : O_RDONLY;
|
||||
int mmap_flags = rw ? PROT_READ | PROT_WRITE : PROT_READ;
|
||||
|
||||
int fd = open(path, open_flags);
|
||||
if (fd == -1) {
|
||||
fprintf(stderr, "Failed to open(): %s\n", strerror(errno));
|
||||
return range;
|
||||
}
|
||||
|
||||
struct stat stat_buf;
|
||||
if (fstat(fd, &stat_buf) != 0) {
|
||||
(void)close(fd);
|
||||
fprintf(stderr, "Failed to fstat(): %s\n", strerror(errno));
|
||||
return range;
|
||||
}
|
||||
|
||||
if (stat_buf.st_size == 0) {
|
||||
(void)close(fd);
|
||||
fprintf(stderr, "Can't map an empty \"%s\" file\n", path);
|
||||
return range;
|
||||
}
|
||||
|
||||
uint8_t *mem = mmap(/*addr=*/NULL, stat_buf.st_size, mmap_flags,
|
||||
MAP_SHARED | MAP_POPULATE, fd, /*offset=*/0);
|
||||
(void)close(fd);
|
||||
if (mem == MAP_FAILED) {
|
||||
fprintf(stderr, "Failed to mmap(): %s\n", strerror(errno));
|
||||
return range;
|
||||
}
|
||||
|
||||
range.start = mem;
|
||||
range.length = stat_buf.st_size;
|
||||
return range;
|
||||
}
|
||||
|
||||
void unmap_file(struct mem_range_t store)
|
||||
{
|
||||
if (munmap(store.start, store.length) != 0)
|
||||
fprintf(stderr, "Failed to munmap(): %s\n", strerror(errno));
|
||||
}
|
29
util/smmstoretool/utils.h
Normal file
29
util/smmstoretool/utils.h
Normal file
@ -0,0 +1,29 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__UTILS_H__
|
||||
#define SMMSTORETOOL__UTILS_H__
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
|
||||
struct mem_range_t {
|
||||
uint8_t *start;
|
||||
size_t length;
|
||||
};
|
||||
|
||||
void *xmalloc(size_t size);
|
||||
|
||||
char *to_chars(const CHAR16 uchars[], size_t size);
|
||||
|
||||
CHAR16 *to_uchars(const char chars[], size_t *size);
|
||||
|
||||
bool str_eq(const char lhs[], const char rhs[]);
|
||||
|
||||
struct mem_range_t map_file(const char path[], bool rw);
|
||||
|
||||
void unmap_file(struct mem_range_t store);
|
||||
|
||||
#endif // SMMSTORETOOL__UTILS_H__
|
232
util/smmstoretool/vs.c
Normal file
232
util/smmstoretool/vs.c
Normal file
@ -0,0 +1,232 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "vs.h"
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
#include "utils.h"
|
||||
|
||||
static size_t get_var_hdr_size(bool auth_vars)
|
||||
{
|
||||
if (auth_vars)
|
||||
return sizeof(AUTHENTICATED_VARIABLE_HEADER);
|
||||
return sizeof(VARIABLE_HEADER);
|
||||
}
|
||||
|
||||
struct var_store_t vs_load(struct mem_range_t vs_data, bool auth_vars)
|
||||
{
|
||||
uint8_t *var_hdr = vs_data.start;
|
||||
|
||||
struct var_store_t vs = {
|
||||
.auth_vars = auth_vars,
|
||||
.vars = NULL,
|
||||
};
|
||||
|
||||
struct var_t *last_var = NULL;
|
||||
|
||||
const size_t var_hdr_size = get_var_hdr_size(auth_vars);
|
||||
while (var_hdr + var_hdr_size < vs_data.start + vs_data.length) {
|
||||
uint16_t start_id;
|
||||
uint8_t state;
|
||||
struct var_t var = {0};
|
||||
uint8_t *var_data = var_hdr;
|
||||
|
||||
if (auth_vars) {
|
||||
const AUTHENTICATED_VARIABLE_HEADER *auth_hdr =
|
||||
(void *)var_data;
|
||||
|
||||
start_id = auth_hdr->StartId;
|
||||
state = auth_hdr->State;
|
||||
|
||||
var.reserved = auth_hdr->Reserved;
|
||||
var.attrs = auth_hdr->Attributes;
|
||||
var.name_size = auth_hdr->NameSize;
|
||||
var.data_size = auth_hdr->DataSize;
|
||||
var.guid = auth_hdr->VendorGuid;
|
||||
} else {
|
||||
const VARIABLE_HEADER *no_auth_hdr = (void *)var_data;
|
||||
|
||||
start_id = no_auth_hdr->StartId;
|
||||
state = no_auth_hdr->State;
|
||||
|
||||
var.reserved = no_auth_hdr->Reserved;
|
||||
var.attrs = no_auth_hdr->Attributes;
|
||||
var.name_size = no_auth_hdr->NameSize;
|
||||
var.data_size = no_auth_hdr->DataSize;
|
||||
var.guid = no_auth_hdr->VendorGuid;
|
||||
}
|
||||
|
||||
var_hdr += HEADER_ALIGN(var_hdr_size +
|
||||
var.name_size +
|
||||
var.data_size);
|
||||
|
||||
if (start_id != VARIABLE_DATA)
|
||||
break;
|
||||
|
||||
if (state != VAR_ADDED)
|
||||
continue;
|
||||
|
||||
if (var.data_size == UINT32_MAX ||
|
||||
var.name_size == UINT32_MAX ||
|
||||
var.attrs == UINT32_MAX)
|
||||
continue;
|
||||
|
||||
CHAR16 *name = (void *)(var_data + var_hdr_size);
|
||||
var.name = xmalloc(var.name_size);
|
||||
memcpy(var.name, name, var.name_size);
|
||||
|
||||
uint8_t *data =
|
||||
(void *)(var_data + var_hdr_size + var.name_size);
|
||||
var.data = xmalloc(var.data_size);
|
||||
memcpy(var.data, data, var.data_size);
|
||||
|
||||
struct var_t *var_node = xmalloc(sizeof(*var_node));
|
||||
*var_node = var;
|
||||
if (last_var != NULL)
|
||||
last_var->next = var_node;
|
||||
else if (vs.vars == NULL)
|
||||
vs.vars = var_node;
|
||||
last_var = var_node;
|
||||
}
|
||||
|
||||
return vs;
|
||||
}
|
||||
|
||||
static void store_var(const struct var_t *var, bool auth_vars, uint8_t *data)
|
||||
{
|
||||
if (auth_vars) {
|
||||
AUTHENTICATED_VARIABLE_HEADER hdr;
|
||||
memset(&hdr, 0xff, sizeof(hdr));
|
||||
|
||||
hdr.StartId = VARIABLE_DATA;
|
||||
hdr.State = VAR_ADDED;
|
||||
hdr.Reserved = var->reserved;
|
||||
hdr.Attributes = var->attrs;
|
||||
hdr.VendorGuid = var->guid;
|
||||
hdr.NameSize = var->name_size;
|
||||
hdr.DataSize = var->data_size;
|
||||
|
||||
memcpy(data, &hdr, sizeof(hdr));
|
||||
data += sizeof(hdr);
|
||||
} else {
|
||||
VARIABLE_HEADER hdr;
|
||||
memset(&hdr, 0xff, sizeof(hdr));
|
||||
|
||||
hdr.StartId = VARIABLE_DATA;
|
||||
hdr.State = VAR_ADDED;
|
||||
hdr.Reserved = var->reserved;
|
||||
hdr.Attributes = var->attrs;
|
||||
hdr.VendorGuid = var->guid;
|
||||
hdr.NameSize = var->name_size;
|
||||
hdr.DataSize = var->data_size;
|
||||
|
||||
memcpy(data, &hdr, sizeof(hdr));
|
||||
data += sizeof(hdr);
|
||||
}
|
||||
|
||||
memcpy(data, var->name, var->name_size);
|
||||
memcpy(data + var->name_size, var->data, var->data_size);
|
||||
}
|
||||
|
||||
bool vs_store(struct var_store_t *vs, struct mem_range_t vs_data)
|
||||
{
|
||||
uint8_t *out_data = vs_data.start;
|
||||
|
||||
const size_t var_hdr_size = get_var_hdr_size(vs->auth_vars);
|
||||
for (struct var_t *var = vs->vars; var != NULL; var = var->next) {
|
||||
const size_t var_size =
|
||||
var_hdr_size + var->name_size + var->data_size;
|
||||
if (out_data + var_size > vs_data.start + vs_data.length) {
|
||||
fprintf(stderr,
|
||||
"Not enough space to serialize Variable Store.\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
store_var(var, vs->auth_vars, out_data);
|
||||
out_data += HEADER_ALIGN(var_size);
|
||||
}
|
||||
|
||||
// The rest is "uninitialized".
|
||||
memset(out_data, 0xff, vs_data.length - (out_data - vs_data.start));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
struct var_t *vs_new_var(struct var_store_t *vs)
|
||||
{
|
||||
struct var_t *new_var = xmalloc(sizeof(*new_var));
|
||||
|
||||
memset(new_var, 0, sizeof(*new_var));
|
||||
new_var->attrs = EFI_VARIABLE_NON_VOLATILE
|
||||
| EFI_VARIABLE_BOOTSERVICE_ACCESS
|
||||
| EFI_VARIABLE_RUNTIME_ACCESS;
|
||||
|
||||
struct var_t *var = vs->vars;
|
||||
if (var == NULL) {
|
||||
vs->vars = new_var;
|
||||
} else {
|
||||
while (var->next != NULL)
|
||||
var = var->next;
|
||||
var->next = new_var;
|
||||
}
|
||||
|
||||
return new_var;
|
||||
}
|
||||
|
||||
struct var_t *vs_find(struct var_store_t *vs,
|
||||
const char name[],
|
||||
const EFI_GUID *guid)
|
||||
{
|
||||
size_t name_size;
|
||||
CHAR16 *uchar_name = to_uchars(name, &name_size);
|
||||
|
||||
struct var_t *var;
|
||||
for (var = vs->vars; var != NULL; var = var->next) {
|
||||
if (var->name_size != name_size)
|
||||
continue;
|
||||
if (memcmp(var->name, uchar_name, name_size) != 0)
|
||||
continue;
|
||||
if (memcmp(&var->guid, guid, sizeof(*guid)) != 0)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
free(uchar_name);
|
||||
return var;
|
||||
}
|
||||
|
||||
static void free_var(struct var_t *var)
|
||||
{
|
||||
free(var->name);
|
||||
free(var->data);
|
||||
free(var);
|
||||
}
|
||||
|
||||
void vs_delete(struct var_store_t *vs, struct var_t *var)
|
||||
{
|
||||
if (vs->vars == var) {
|
||||
vs->vars = var->next;
|
||||
free_var(var);
|
||||
return;
|
||||
}
|
||||
|
||||
for (struct var_t *v = vs->vars; v != NULL; v = v->next) {
|
||||
if (v->next == var) {
|
||||
v->next = var->next;
|
||||
free_var(var);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void vs_free(struct var_store_t *vs)
|
||||
{
|
||||
for (struct var_t *next, *var = vs->vars; var != NULL; var = next) {
|
||||
next = var->next;
|
||||
free_var(var);
|
||||
}
|
||||
}
|
44
util/smmstoretool/vs.h
Normal file
44
util/smmstoretool/vs.h
Normal file
@ -0,0 +1,44 @@
|
||||
/* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#ifndef SMMSTORETOOL__VS_H__
|
||||
#define SMMSTORETOOL__VS_H__
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "udk2017.h"
|
||||
#include "utils.h"
|
||||
|
||||
// Variable store is part of firmware volume. This unit doesn't deal with its
|
||||
// header only with data that follows.
|
||||
|
||||
struct var_t {
|
||||
uint8_t reserved;
|
||||
uint32_t attrs;
|
||||
EFI_GUID guid;
|
||||
CHAR16 *name;
|
||||
size_t name_size; // in bytes
|
||||
uint8_t *data;
|
||||
size_t data_size; // in bytes
|
||||
struct var_t *next;
|
||||
};
|
||||
|
||||
struct var_store_t {
|
||||
struct var_t *vars;
|
||||
bool auth_vars;
|
||||
};
|
||||
|
||||
struct var_store_t vs_load(struct mem_range_t vs_data, bool auth_vars);
|
||||
|
||||
bool vs_store(struct var_store_t *vs, struct mem_range_t vs_data);
|
||||
|
||||
struct var_t *vs_new_var(struct var_store_t *vs);
|
||||
|
||||
struct var_t *vs_find(struct var_store_t *vs,
|
||||
const char name[],
|
||||
const EFI_GUID *guid);
|
||||
|
||||
void vs_delete(struct var_store_t *vs, struct var_t *var);
|
||||
|
||||
void vs_free(struct var_store_t *vs);
|
||||
|
||||
#endif // SMMSTORETOOL__VS_H__
|
@ -4,6 +4,7 @@
|
||||
* [cbfstool](util/cbfstool/index.md)
|
||||
* [ifdtool](util/ifdtool/index.md)
|
||||
* [intelp2m](util/intelp2m/index.md)
|
||||
* [smmstoretool](util/smmstoretool/index.md)
|
||||
|
||||
## Generated documentation
|
||||
|
||||
|
Reference in New Issue
Block a user