summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--include/inject.sh6
-rw-r--r--util/nvmutil/.gitignore3
-rw-r--r--util/nvmutil/ChangeLog.md8
-rw-r--r--util/nvmutil/Makefile32
-rw-r--r--util/nvmutil/README.md4
-rw-r--r--util/nvmutil/nvmutil.c2388
-rw-r--r--util/spkmodem_decode/.gitignore2
-rw-r--r--util/spkmodem_decode/Makefile30
-rw-r--r--util/spkmodem_decode/spkmodem-decode.c725
-rw-r--r--util/spkmodem_recv/.gitignore1
-rw-r--r--util/spkmodem_recv/Makefile14
-rw-r--r--util/spkmodem_recv/spkmodem-recv.c117
13 files changed, 2663 insertions, 668 deletions
diff --git a/.gitignore b/.gitignore
index deb2d255..43285fbc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -30,7 +30,6 @@
*me.bin
*sch5545ec.bin
/mrc/
-/util/nvmutil/nvm
/src/
/CHANGELOG
/todo.txt
diff --git a/include/inject.sh b/include/inject.sh
index 6a1b0768..783e06ed 100644
--- a/include/inject.sh
+++ b/include/inject.sh
@@ -6,7 +6,7 @@
cbcfgsdir="config/coreboot"
tmpromdel="$XBMK_CACHE/DO_NOT_FLASH"
-nvm="util/nvmutil/nvm"
+nvmutil="util/nvmutil/nvmutil"
ifdtool="elf/coreboot/default/ifdtool"
checkvars="CONFIG_GBE_BIN_PATH"
@@ -200,13 +200,13 @@ modify_mac()
x_ make -C util/nvmutil clean
x_ make -C util/nvmutil
- x_ "$nvm" "$xbtmp/gbe" setmac "$new_mac"
+ x_ "$nvmutil" "$xbtmp/gbe" setmac "$new_mac"
fi
fx_ newmac x_ find "$tmpromdir" -maxdepth 1 -type f -name "*.rom"
printf "\nThe following GbE NVM data will be written:\n"
- x_ "$nvm" "$xbtmp/gbe" dump | grep -v "bytes read from file" || :
+ x_ "$nvmutil" "$xbtmp/gbe" dump | grep -v "bytes read from file" || :
}
newmac()
diff --git a/util/nvmutil/.gitignore b/util/nvmutil/.gitignore
new file mode 100644
index 00000000..802202a4
--- /dev/null
+++ b/util/nvmutil/.gitignore
@@ -0,0 +1,3 @@
+/nvm
+/nvmutil
+*.bin
diff --git a/util/nvmutil/ChangeLog.md b/util/nvmutil/ChangeLog.md
deleted file mode 100644
index e1ed5754..00000000
--- a/util/nvmutil/ChangeLog.md
+++ /dev/null
@@ -1,8 +0,0 @@
-This change log has moved. Please refer here for historical pre-osboot-merge
-changes:
-
-<https://libreboot.org/docs/install/nvmutilimport.html>
-
-Osboot merged with Libreboot on November 17th, 2022. For nvmutil changes after
-this date, please check regular Libreboot release announcements which shall
-now specify any such changes.
diff --git a/util/nvmutil/Makefile b/util/nvmutil/Makefile
index b8ec2ad3..719e1c1e 100644
--- a/util/nvmutil/Makefile
+++ b/util/nvmutil/Makefile
@@ -1,24 +1,32 @@
# SPDX-License-Identifier: MIT
-# SPDX-FileCopyrightText: 2022,2025 Leah Rowe <leah@libreboot.org>
-# SPDX-FileCopyrightText: 2023 Riku Viitanen <riku.viitanen@protonmail.com>
+# Copyright (c) 2022,2026 Leah Rowe <leah@libreboot.org>
+# Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com>
CC?=cc
-CFLAGS?=-Os -Wall -Wextra -Werror -pedantic
+CFLAGS?=-Os -Wall -Wextra -Werror -pedantic -std=c90
+LDFLAGS?=
DESTDIR?=
PREFIX?=/usr/local
INSTALL?=install
-nvm: nvmutil.c
- $(CC) $(CFLAGS) nvmutil.c -o nvm
+PROG=nvmutil
-install:
- $(INSTALL) nvm $(DESTDIR)$(PREFIX)/bin/nvm
+all: $(PROG)
-uninstall:
- rm -f $(DESTDIR)$(PREFIX)/bin/nvm
+$(PROG): nvmutil.c
+ $(CC) $(CFLAGS) $(LDFLAGS) nvmutil.c -o $(PROG)
+
+install: $(PROG)
+ $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin
+ $(INSTALL) $(PROG) $(DESTDIR)$(PREFIX)/bin/$(PROG)
+ chmod 755 $(DESTDIR)$(PREFIX)/bin/$(PROG)
-distclean:
- rm -f nvm
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG)
clean:
- rm -f nvm
+ rm -f $(PROG)
+
+distclean: clean
+
+.PHONY: all install uninstall clean distclean
diff --git a/util/nvmutil/README.md b/util/nvmutil/README.md
deleted file mode 100644
index 03a25bc4..00000000
--- a/util/nvmutil/README.md
+++ /dev/null
@@ -1,4 +0,0 @@
-
-This documentation has become part of lbwww. See:
-
-<https://libreboot.org/docs/install/nvmutil.html>
diff --git a/util/nvmutil/nvmutil.c b/util/nvmutil/nvmutil.c
index c609ca36..fe8364f7 100644
--- a/util/nvmutil/nvmutil.c
+++ b/util/nvmutil/nvmutil.c
@@ -1,77 +1,439 @@
-/* SPDX-License-Identifier: MIT */
-/* Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org> */
-/* Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com> */
+/* SPDX-License-Identifier: MIT
+ *
+ * Copyright (c) 2022-2026 Leah Rowe <leah@libreboot.org>
+ * Copyright (c) 2023 Riku Viitanen <riku.viitanen@protonmail.com>
+ *
+ * This tool lets you modify Intel GbE NVM (Gigabit Ethernet
+ * Non-Volatile Memory) images, e.g. change the MAC address.
+ * These images configure your Intel Gigabit Ethernet adapter.
+ *
+ * This code is designed to be portable, running on as many
+ * Unix and Unix-like systems as possible (mainly BSD/Linux).
+ *
+ * Recommended CFLAGS for Clang/GCC:
+ *
+ * -Os -Wall -Wextra -Werror -pedantic -std=c90
+ */
+
+#define OFF_ERR 0
+#ifndef OFF_RESET
+#define OFF_RESET 1
+#endif
+
+/*
+ * NOTE: older Linux lacked arc4random.
+ * added in glibc 2.36. Just pass HAVE_ARC4RANDOM_BUF=0
+ * at build time if you need old Linux / other libc.
+ */
+#if defined(__OpenBSD__) || defined(__FreeBSD__) || \
+ defined(__NetBSD__) || defined(__APPLE__) || \
+ defined(__linux__)
+#ifndef HAVE_ARC4RANDOM_BUF
+#define HAVE_ARC4RANDOM_BUF 1
+#endif
+#endif
+
+/*
+ * I/O config (build-time)
+ *
+ * Regarding:
+ * Retries on zero-return.
+ *
+ * 5 retries is generous,
+ * but also conservative.
+ * This is enough for e.g.
+ * slow USB flash drives,
+ * busy NFS servers, etc.
+ * Any more is too much
+ * and not of much benefit.
+ *
+ * 3-5 will tolerate buggy
+ * USB drives for example,
+ * but won't spin as long
+ * on really buggy and slow
+ * networks e.g. slow NFS.
+ *
+ * At least 3-5 recommended.
+ * Pass this at build time.
+ */
+#ifndef MAX_ZERO_RW_RETRY
+#define MAX_ZERO_RW_RETRY 5
+#endif
+/*
+ * 0: portable pread/pwrite
+ * 1: real pread/pwrite (thread-safe)
+ * Pass this at build-time
+ */
+#ifndef HAVE_REAL_PREAD_PWRITE
+#define HAVE_REAL_PREAD_PWRITE 0
+#endif
+/*
+ * Configure whether to wait on
+ * EINTR on files, or EAGAIN on
+ * cmd cat (stdout).
+ *
+ * Pass these at build time.
+ */
+#ifndef LOOP_EAGAIN
+#define LOOP_EAGAIN 1
+#endif
+#ifndef LOOP_EINTR
+#define LOOP_EINTR 1
+#endif
+
+/*
+ * Major TODO: split this into multiple files.
+ * This program has become quite large now, mostly
+ * due to all the extra sanity checks / portability.
+ * Make most of nvmutil a *library* for re-use
+ *
+ * TODO: gettimeofday not posible - use portable functions.
+ * TODO: ux fallback: modify the program instead
+ * to run on 16-bit systems: smaller buffers, and do
+ * operations byte-based instead of word-based.
+ *
+ * TODO: _XOPEN_SOURCE 500 probably not needed anymore.
+ * the portable fallbacks alone are likely enough.
+ * e.g. i don't need stdint, and i don't use pwrite/pread
+ * anymore.
+ *
+ * TODO: version detection of various BSDs to detect
+ * arc4random, use that if available. but also work on
+ * older versions of those BSDs (also MacOS) that lack it.
+ *
+ * TODO: portability/testing on non-Unix systems:
+ * old DOS. all windows versions (probably irrelevant
+ * because you can use cygwin/wsl, whatever), classic MacOS,
+ * also test really old unix e.g. sunos and irix. Be/Haiku too!
+ *
+ * TODO: reliance on global variables for status. make
+ * functions use structs passed as args instead, make
+ * functions re-useable (including libraries), etc.
+ *
+ * TODO: bound checks for files per-command, e.g. only
+ * first 6 bytes for CMD_SETMAC
+ *
+ * TODO: in command sanitizer: verify that each given
+ * entry corresponds to the correct function, in the
+ * pointer (this check is currently missing)
+ *
+ * TODO: general modularisierung of the entire codebase.
+ * TODO: better explain copy/swap read inversion trick
+ * by improving existing comments
+ * TODO: lots of overwritten comments in code. tidy it up.
+ *
+ * TODO: use getopt for nvmutil args, so that multiple
+ * operations can be performed, and also on many
+ * files at once (noting limitations with cat)
+ * BONUS: implement own getopt(), for portability
+ *
+ * TODO: document fuzzing / static analysis methods
+ * for the code, and:
+ * TODO: implement rigorous unit tests (separate util)
+ * NOTE: this would *include* known good test files
+ * in various configurations, also invalid files.
+ * the tests would likely be portable posix shell
+ * scripts rather than a new C program, but a modularisiert
+ * codebase would allow me to write a separate C
+ * program to test some finer intricacies
+ * TODO: the unit tests would basically test regressions
+ * TODO: after writing back a gbe to file, close() and
+ * open() it again, read it again, and check that
+ * the contents were written correctly, providing
+ * a warning if they were. do this in the main
+ * program.
+ * TODO: the unit tests would include an aggressive set
+ * of fuzz tests, under controlled conditions
+ *
+ * TODO: also document the layout of Intel GbE files, so
+ * that wily individuals can easily expand the
+ * featureset of nvmutil.
+ * TODO: write a manpage
+ * TODO: simplify the command sanitization, implement more
+ * of it as build time checks, e.g. static asserts.
+ * generally remove cleverness from the code, instead
+ * prefyerring readibility
+ * TODO: also document nvmutil's coding style, which is
+ * its own style at this point!
+ * TODO: when all the above (and possibly more) is done,
+ * submit this tool to coreboot with a further change
+ * to their build system that lets users modify
+ * GbE images, especially set MAC addresses, when
+ * including GbE files in coreboot configs.
+ */
+/*
+ BONUS TODO:
+ CI/CD. woodpecker is good enough, sourcehut also has one.
+ tie this in with other things mentioned here,
+ e.g. fuzzer / unit tests
+*/
+
+/* Major TODO: reproducible builds
+Test with and without these:
+
+CFLAGS += -fno-record-gcc-switches
+CFLAGS += -ffile-prefix-map=$(PWD)=.
+CFLAGS += -fdebug-prefix-map=$(PWD)=.
+
+I already avoid unique timestamps per-build,
+by not using them, e.g. not reporting build
+time in the program.
+
+When splitting the nvmutil.c file later, do e.g.:
+
+SRC = main.c io.c nvm.c cmd.c
+OBJ = $(SRC:.c=.o)
+
+^ explicitly declare the order in which to build
+*/
+
+/*
+TODO:
+further note when fuzzing is implemented:
+use deterministic randomisation, with a
+guaranteed seed - so e.g. don't use /dev/urandom
+in test builds. e.g. just use normal rand()
+but with a static seed e.g. 1234
+*/
+/*
+TODO: stricter build flags, e.g.
+CFLAGS += -fstack-protector-strong
+CFLAGS += -fno-common
+CFLAGS += -D_FORTIFY_SOURCE=2
+CFLAGS += -fPIE
+
+also consider:
+-fstack-clash-protection
+-Wl,-z,relro
+-Wl,-z,now
+*/
+
+#ifndef _FILE_OFFSET_BITS
+#define _FILE_OFFSET_BITS 64
+#endif
+#ifdef __OpenBSD__
+#include <sys/param.h>
+#endif
+#include <sys/types.h>
+#include <sys/time.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
+#include <limits.h>
#include <stdarg.h>
-#include <stdint.h>
+#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
+#include <time.h>
#include <unistd.h>
+typedef unsigned char u8;
+typedef unsigned short ushort;
+typedef unsigned int uint;
+typedef unsigned long ulong;
+
+/* type asserts */
+typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1];
+typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1];
+typedef char static_assert_u8_is_1[
+ (sizeof(u8) == 1) ? 1 : -1];
+typedef char static_assert_ushort_is_2[
+ (sizeof(ushort) >= 2) ? 1 : -1];
+typedef char static_assert_short_is_2[(sizeof(short) >= 2) ? 1 : -1];
+typedef char static_assert_uint_is_4[
+ (sizeof(uint) >= 4) ? 1 : -1];
+typedef char static_assert_ulong_is_4[
+ (sizeof(ulong) >= 4) ? 1 : -1];
+typedef char static_assert_int_ge_32[(sizeof(int) >= 4) ? 1 : -1];
+typedef char static_assert_twos_complement[
+ ((-1 & 3) == 3) ? 1 : -1
+];
+typedef char assert_ulong_ptr[
+ (sizeof(ulong) >= sizeof(void *)) ? 1 : -1
+];
+typedef char assert_size_t_ptr[
+ (sizeof(size_t) >= sizeof(void *)) ? 1 : -1
+];
+
/*
- * On the platforms below, we will use arc4random
- * for random MAC address generation.
+ * We set _FILE_OFFSET_BITS 64, but we only handle
+ * files that are 128KB in size at a maximum, so we
+ * realistically only need 32-bit at a minimum.
*
- * Later on, the code has fallbacks for other systems.
+ * We set 64 anyway, because there's no reason not
+ * to, but some systems may ignore _FILE_OFFSET_BITS
*/
-#if defined(__OpenBSD__) || defined(__FreeBSD__) || \
- defined(__NetBSD__) || defined(__APPLE__) || \
- defined(__DragonFly__)
-#ifndef HAVE_ARC4RANDOM_BUF
-#define HAVE_ARC4RANDOM_BUF
+typedef char static_assert_off_t_is_32[(sizeof(off_t) >= 4) ? 1 : -1];
+
+/*
+ * Older versions of BSD to the early 2000s
+ * could compile nvmutil, but pledge was
+ * added in the 2010s. Therefore, for extra
+ * portability, we will only pledge/unveil
+ * on OpenBSD versions that have it.
+ */
+#if defined(__OpenBSD__) && defined(OpenBSD)
+#if OpenBSD >= 604
+#ifndef NVMUTIL_UNVEIL
+#define NVMUTIL_UNVEIL 1
#endif
#endif
+#if OpenBSD >= 509
+#ifndef NVMUTIL_PLEDGE
+#define NVMUTIL_PLEDGE 1
+#endif
+#endif
+#endif
+
+#ifndef EXIT_FAILURE
+#define EXIT_FAILURE 1
+#endif
+
+#ifndef EXIT_SUCCESS
+#define EXIT_SUCCESS 0
+#endif
+
+#ifndef O_BINARY
+#define O_BINARY 0
+#endif
+
+#ifndef O_NOFOLLOW
+#define O_NOFOLLOW 0
+#endif
+/*
+ * Sanitize command tables.
+ */
+static void sanitize_command_list(void);
+static void sanitize_command_index(size_t c);
+
+/*
+ * Argument handling (user input)
+ */
static void set_cmd(int argc, char *argv[]);
-static void check_cmd_args(int argc, char *argv[]);
+static void set_cmd_args(int argc, char *argv[]);
static size_t conv_argv_part_num(const char *part_str);
-static void set_io_flags(int argc, char *argv[]);
+static int xstrxcmp(const char *a, const char *b, size_t maxlen);
+
+/*
+ * Prep files for reading
+ */
static void open_gbe_file(void);
-#ifndef HAVE_ARC4RANDOM_BUF
-static void open_dev_urandom(void);
-#endif
+static void lock_gbe_file(void);
static void xopen(int *fd, const char *path, int flags, struct stat *st);
+
+/*
+ * Read GbE file and verify
+ * checksums.
+ *
+ * After this, we can run commands.
+ */
static void read_gbe_file(void);
-static void read_gbe_file_part(size_t part, uint8_t invert);
-static void cmd_setmac(void);
+static void read_checksums(void);
+static int good_checksum(size_t partnum);
+
+/*
+ * Execute user command on GbE data.
+ * These are stubs that call helpers.
+ */
+static void run_cmd(size_t c);
+static void check_command_num(size_t c);
+static u8 valid_command(size_t c);
+
+/*
+ * Helper functions for command: setmac
+ */
+static void cmd_helper_setmac(void);
static void parse_mac_string(void);
+static size_t xstrxlen(const char *scmp, size_t maxlen);
static void set_mac_byte(size_t mac_byte_pos);
static void set_mac_nib(size_t mac_str_pos,
size_t mac_byte_pos, size_t mac_nib_pos);
-static uint8_t hextonum(char ch_s);
-static uint8_t rhex(void);
-static void read_file_exact(int fd, void *buf, size_t len,
- off_t off, const char *path, const char *op);
-static int write_mac_part(size_t partnum);
-static void cmd_dump(void);
-static void print_mac_address(size_t partnum);
+static ushort hextonum(char ch_s);
+static ushort rhex(void);
+#if !defined(HAVE_ARC4RANDOM_BUF) || \
+ (HAVE_ARC4RANDOM_BUF) < 1
+static ulong entropy_jitter(void);
+#endif
+static void write_mac_part(size_t partnum);
+
+/*
+ * Helper functions for command: dump
+ */
+static void cmd_helper_dump(void);
+static void print_mac_from_nvm(size_t partnum);
static void hexdump(size_t partnum);
-static void cmd_setchecksum(void);
+
+/*
+ * Helper functions for commands:
+ * cat, cat16 and cat128
+ */
+static void cmd_helper_cat(void);
+static void gbe_cat_buf(u8 *b);
+
+/*
+ * After command processing, write
+ * the modified GbE file back.
+ *
+ * These are stub functions: check
+ * below for the actual functions.
+ */
+static void write_gbe_file(void);
+static void override_part_modified(void);
static void set_checksum(size_t part);
-static void cmd_brick(void);
-static void cmd_copy(void);
-static void cmd_swap(void);
-static int good_checksum(size_t partnum);
-static uint16_t word(size_t pos16, size_t part);
-static void set_word(size_t pos16, size_t part, uint16_t val16);
+static ushort calculated_checksum(size_t p);
+
+/*
+ * Helper functions for accessing
+ * the NVM area during operation.
+ */
+static ushort nvm_word(size_t pos16, size_t part);
+static void set_nvm_word(size_t pos16, size_t part, ushort val16);
+static void set_part_modified(size_t p);
static void check_nvm_bound(size_t pos16, size_t part);
-static void write_gbe_file(void);
-static void write_gbe_file_part(size_t part);
+static void check_bin(size_t a, const char *a_name);
+
+/*
+ * Helper functions for stub functions
+ * that handle GbE file reads/writes.
+ */
+static void rw_gbe_file_part(size_t p, int rw_type,
+ const char *rw_type_str);
+static void check_written_part(size_t p);
+static void report_io_err_rw(void);
+static u8 *gbe_mem_offset(size_t part, const char *f_op);
static off_t gbe_file_offset(size_t part, const char *f_op);
-static void *gbe_mem_offset(size_t part, const char *f_op);
static off_t gbe_x_offset(size_t part, const char *f_op,
const char *d_type, off_t nsize, off_t ncmp);
-static void set_part_modified(size_t p);
-static void check_part_num(size_t p);
-static void usage(void);
+static ssize_t rw_gbe_file_exact(int fd, u8 *mem, size_t nrw,
+ off_t off, int rw_type);
+static ssize_t rw_file_exact(int fd, u8 *mem, size_t len,
+ off_t off, int rw_type, int loop_eagain, int loop_eintr,
+ size_t max_retries, int off_reset);
+static ssize_t prw(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type, int loop_eagain, int loop_eintr,
+ int off_reset);
+static int io_args(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type);
+static int check_file(int fd, struct stat *st);
+static ssize_t rw_over_nrw(ssize_t r, size_t nrw);
+#if !defined(HAVE_REAL_PREAD_PWRITE) || \
+ HAVE_REAL_PREAD_PWRITE < 1
+static off_t lseek_loop(int fd, off_t off,
+ int whence, int loop_eagain, int loop_eintr);
+#endif
+static int try_err(int loop_err, int errval);
+
+/*
+ * Error handling and cleanup
+ */
static void err(int nvm_errval, const char *msg, ...);
+static int close_files(void);
static const char *getnvmprogname(void);
-static void set_err(int errval);
+static void usage(int usage_exit);
/*
* Sizes in bytes:
@@ -102,23 +464,11 @@ static void set_err(int errval);
#define NVM_CHECKSUM_WORD (NVM_WORDS - 1)
/*
- * When reading files, we loop on error EINTR
- * a maximum number of times as defined, thus:
- */
-#define MAX_RETRY_READ 30
-
-/*
- * Portably macro based on BSD nitems.
+ * Portable macro based on BSD nitems.
* Used to count the number of commands (see below).
*/
#define items(x) (sizeof((x)) / sizeof((x)[0]))
-static const char newrandom[] = "/dev/urandom";
-static const char oldrandom[] = "/dev/random"; /* fallback on OLD unix */
-#ifndef HAVE_ARC4RANDOM_BUF
-static const char *rname = NULL;
-#endif
-
/*
* GbE files can be 8KB, 16KB or 128KB,
* but we only need the two 4KB parts
@@ -128,269 +478,533 @@ static const char *rname = NULL;
*
* The code will handle this properly.
*/
-static uint8_t buf[GBE_FILE_SIZE];
+static u8 real_buf[GBE_FILE_SIZE];
+static u8 pad[GBE_FILE_SIZE]; /* the file that wouldn't die */
+static u8 *buf = real_buf;
-static uint16_t mac_buf[3];
+static ushort mac_buf[3];
static off_t gbe_file_size;
-static int gbe_flags;
-#ifndef HAVE_ARC4RANDOM_BUF
-static int urandom_fd = -1;
-#endif
static int gbe_fd = -1;
static size_t part;
-static uint8_t invert;
-static uint8_t part_modified[2];
+static u8 part_modified[2];
+static u8 part_valid[2];
-static const char *mac_str;
static const char rmac[] = "xx:xx:xx:xx:xx:xx";
+static const char *mac_str;
static const char *fname;
static const char *argv0;
+#ifndef SSIZE_MAX
+#define SSIZE_MAX ((ssize_t)(~((size_t)1 << (sizeof(ssize_t)*CHAR_BIT-1))))
+#endif
+
+/*
+ * Use these for .invert in command[]:
+ * If set to 1: read/write inverter (p0->p1, p1->p0)
+ */
+#define PART_INVERT 1
+#define NO_INVERT 0
+
+/*
+ * Use these for .argc in command[]:
+ */
+#define ARGC_3 3
+#define ARGC_4 4
+
+#define NO_LOOP_EAGAIN 0
+#define NO_LOOP_EINTR 0
+
+enum {
+ IO_READ,
+ IO_WRITE,
+ IO_PREAD,
+ IO_PWRITE
+};
+
+/*
+ * Used as indices for command[]
+ * MUST be in the same order as entries in command[]
+ */
+enum {
+ CMD_DUMP,
+ CMD_SETMAC,
+ CMD_SWAP,
+ CMD_COPY,
+ CMD_CAT,
+ CMD_CAT16,
+ CMD_CAT128
+};
+
+/*
+ * If set, a given part will always be written.
+ */
+enum {
+ SET_MOD_OFF, /* don't manually set part modified */
+ SET_MOD_0, /* set part 0 modified */
+ SET_MOD_1, /* set part 1 modified */
+ SET_MOD_N, /* set user-specified part modified */
+ /* affected by command[].invert */
+ SET_MOD_BOTH /* set both parts modified */
+};
+
+enum {
+ ARG_NOPART,
+ ARG_PART
+};
+
+enum {
+ SKIP_CHECKSUM_READ,
+ CHECKSUM_READ
+};
+
+enum {
+ SKIP_CHECKSUM_WRITE,
+ CHECKSUM_WRITE
+};
+
struct commands {
+ size_t chk;
const char *str;
- void (*cmd)(void);
- int args;
+ void (*run)(void);
+ int argc;
+ u8 invert;
+ u8 set_modified;
+ u8 arg_part;
+ u8 chksum_read;
+ u8 chksum_write;
+ size_t rw_size; /* within the 4KB GbE part */
+ int flags; /* e.g. O_RDWR or O_RDONLY */
};
+
+/*
+ * Command table, for nvmutil commands
+ */
static const struct commands command[] = {
- { "dump", cmd_dump, 3 },
- { "setmac", cmd_setmac, 3 },
- { "swap", cmd_swap, 3 },
- { "copy", cmd_copy, 4 },
- { "brick", cmd_brick, 4 },
- { "setchecksum", cmd_setchecksum, 4 },
+ { CMD_DUMP, "dump", cmd_helper_dump, ARGC_3,
+ NO_INVERT, SET_MOD_OFF,
+ ARG_NOPART,
+ SKIP_CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ NVM_SIZE, O_RDONLY },
+
+ { CMD_SETMAC, "setmac", cmd_helper_setmac, ARGC_3,
+ NO_INVERT, SET_MOD_OFF,
+ ARG_NOPART,
+ CHECKSUM_READ, CHECKSUM_WRITE,
+ NVM_SIZE, O_RDWR },
+
+ /*
+ * OPTIMISATION: Read inverted, so no copying is needed.
+ */
+ { CMD_SWAP, "swap", NULL, ARGC_3,
+ PART_INVERT, SET_MOD_BOTH,
+ ARG_NOPART,
+ CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ GBE_PART_SIZE, O_RDWR },
+
+ /*
+ * OPTIMISATION: Read inverted, so no copying is needed.
+ * The non-target part will not be read.
+ */
+ { CMD_COPY, "copy", NULL, ARGC_4,
+ PART_INVERT, SET_MOD_N,
+ ARG_PART,
+ CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ GBE_PART_SIZE, O_RDWR },
+
+ { CMD_CAT, "cat", cmd_helper_cat, ARGC_3,
+ NO_INVERT, SET_MOD_OFF,
+ ARG_NOPART,
+ CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ GBE_PART_SIZE, O_RDONLY },
+
+ { CMD_CAT16, "cat16", cmd_helper_cat, ARGC_3,
+ NO_INVERT, SET_MOD_OFF,
+ ARG_NOPART,
+ CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ GBE_PART_SIZE, O_RDONLY },
+
+ { CMD_CAT128, "cat128", cmd_helper_cat, ARGC_3,
+ NO_INVERT, SET_MOD_OFF,
+ ARG_NOPART,
+ CHECKSUM_READ, SKIP_CHECKSUM_WRITE,
+ GBE_PART_SIZE, O_RDONLY },
};
-static void (*cmd)(void) = NULL;
+#define MAX_CMD_LEN 50
+#define N_COMMANDS items(command)
+#define CMD_NULL N_COMMANDS
+
+/*
+ * Index in command[], will be set later
+ */
+static size_t cmd_index = CMD_NULL;
+
+/*
+ * asserts (variables/defines sanity check)
+ */
+typedef char assert_argc3[(ARGC_3==3)?1:-1];
+typedef char assert_argc4[(ARGC_4==4)?1:-1];
+typedef char assert_read[(IO_READ==0)?1:-1];
+typedef char assert_write[(IO_WRITE==1)?1:-1];
+typedef char assert_pread[(IO_PREAD==2)?1:-1];
+typedef char assert_pwrite[(IO_PWRITE==3)?1:-1];
+/* commands */
+typedef char assert_cmd_dump[(CMD_DUMP==0)?1:-1];
+typedef char assert_cmd_setmac[(CMD_SETMAC==1)?1:-1];
+typedef char assert_cmd_swap[(CMD_SWAP==2)?1:-1];
+typedef char assert_cmd_copy[(CMD_COPY==3)?1:-1];
+typedef char assert_cmd_cat[(CMD_CAT==4)?1:-1];
+typedef char assert_cmd_cat16[(CMD_CAT16==5)?1:-1];
+typedef char assert_cmd_cat128[(CMD_CAT128==6)?1:-1];
+/* mod_type */
+typedef char assert_mod_off[(SET_MOD_OFF==0)?1:-1];
+typedef char assert_mod_0[(SET_MOD_0==1)?1:-1];
+typedef char assert_mod_1[(SET_MOD_1==2)?1:-1];
+typedef char assert_mod_n[(SET_MOD_N==3)?1:-1];
+typedef char assert_mod_both[(SET_MOD_BOTH==4)?1:-1];
+/* bool */
+typedef char bool_arg_nopart[(ARG_NOPART==0)?1:-1];
+typedef char bool_arg_part[(ARG_PART==1)?1:-1];
+typedef char bool_skip_checksum_read[(SKIP_CHECKSUM_READ==0)?1:-1];
+typedef char bool_checksum_read[(CHECKSUM_READ==1)?1:-1];
+typedef char bool_skip_checksum_write[(SKIP_CHECKSUM_WRITE==0)?1:-1];
+typedef char bool_checksum_write[(CHECKSUM_WRITE==1)?1:-1];
+typedef char bool_no_invert[(NO_INVERT==0)?1:-1];
+typedef char bool_part_invert[(PART_INVERT==1)?1:-1];
+typedef char bool_loop_eintr[(LOOP_EINTR==1||LOOP_EINTR==0)?1:-1];
+typedef char bool_loop_eagain[(LOOP_EAGAIN==1||LOOP_EAGAIN==0)?1:-1];
+typedef char bool_no_loop_eintr[(NO_LOOP_EINTR==0)?1:-1];
+typedef char bool_no_loop_eagain[(NO_LOOP_EAGAIN==0)?1:-1];
+typedef char bool_off_err[(OFF_ERR==0)?1:-1];
+typedef char bool_off_reset[(OFF_RESET==0||OFF_RESET==1)?1:-1];
+
+static int io_err_gbe = 0;
+static int rw_check_err_read[] = {0, 0};
+static int rw_check_partial_read[] = {0, 0};
+static int rw_check_bad_part[] = {0, 0};
+
+static int post_rw_checksum[] = {0, 0};
+
+static dev_t gbe_dev;
+static ino_t gbe_ino;
+
+#if defined(HAVE_ARC4RANDOM_BUF) && \
+ (HAVE_ARC4RANDOM_BUF) > 0
+void arc4random_buf(void *buf, size_t n);
+#endif
int
main(int argc, char *argv[])
{
argv0 = argv[0];
- if (argc < 2)
- usage();
+ if (argc < 3)
+ usage(1);
fname = argv[1];
-#ifdef __OpenBSD__
- if (pledge("stdio rpath wpath unveil", NULL) == -1)
- err(ECANCELED, "pledge");
-
- /*
- * For restricted filesystem access on early error.
- *
- * Unveiling the random device early, regardless of
- * whether we will use it, prevents operations on any
- * GbE files until we permit it, while performing the
- * prerequisite error checks.
- *
- * We don't actually use the random device on platforms
- * that have arc4random, which includes OpenBSD.
- */
- if (unveil("/dev/urandom", "r") == -1)
- err(ECANCELED, "unveil '/dev/urandom'");
- if (unveil("/dev/random", "r") == -1)
- err(ECANCELED, "unveil '/dev/random'");
+#ifdef NVMUTIL_PLEDGE
+#ifdef NVMUTIL_UNVEIL
+ if (pledge("stdio flock rpath wpath unveil", NULL) == -1)
+ err(errno, "pledge");
+ if (unveil("/dev/null", "r") == -1)
+ err(errno, "unveil /dev/null");
+#else
+ if (pledge("stdio flock rpath wpath", NULL) == -1)
+ err(errno, "pledge");
#endif
+#endif
+
+ sanitize_command_list();
set_cmd(argc, argv);
- check_cmd_args(argc, argv);
- set_io_flags(argc, argv);
+ set_cmd_args(argc, argv);
-#ifdef __OpenBSD__
- if (gbe_flags == O_RDONLY) {
+#ifdef NVMUTIL_PLEDGE
+#ifdef NVMUTIL_UNVEIL
+ if (command[cmd_index].flags == O_RDONLY) {
if (unveil(fname, "r") == -1)
- err(ECANCELED, "unveil ro '%s'", fname);
+ err(errno, "%s: unveil ro", fname);
if (unveil(NULL, NULL) == -1)
- err(ECANCELED, "unveil block (ro)");
- if (pledge("stdio rpath", NULL) == -1)
- err(ECANCELED, "pledge ro (kill unveil)");
+ err(errno, "unveil block (ro)");
+ if (pledge("stdio flock rpath", NULL) == -1)
+ err(errno, "pledge ro (kill unveil)");
} else {
if (unveil(fname, "rw") == -1)
- err(ECANCELED, "unveil rw '%s'", fname);
+ err(errno, "%s: unveil rw", fname);
if (unveil(NULL, NULL) == -1)
- err(ECANCELED, "unveil block (rw)");
- if (pledge("stdio rpath wpath", NULL) == -1)
- err(ECANCELED, "pledge rw (kill unveil)");
+ err(errno, "unveil block (rw)");
+ if (pledge("stdio flock rpath wpath", NULL) == -1)
+ err(errno, "pledge rw (kill unveil)");
+ }
+#else
+ if (command[cmd_index].flags == O_RDONLY) {
+ if (pledge("stdio flock rpath", NULL) == -1)
+ err(errno, "pledge ro");
}
#endif
+#endif
-#ifndef HAVE_ARC4RANDOM_BUF
- open_dev_urandom();
+#if !defined(HAVE_ARC4RANDOM_BUF) || \
+ (HAVE_ARC4RANDOM_BUF) < 1
+ srand((uint)(time(NULL) ^ getpid()));
#endif
+
open_gbe_file();
+ lock_gbe_file();
-#ifdef __OpenBSD__
+#ifdef NVMUTIL_PLEDGE
if (pledge("stdio", NULL) == -1)
- err(ECANCELED, "pledge stdio (main)");
+ err(errno, "pledge stdio (main)");
#endif
+ /*
+ * Used by CMD_CAT, for padding
+ */
+ memset(pad, 0xff, sizeof(pad));
+
read_gbe_file();
- (*cmd)();
- write_gbe_file();
+ read_checksums();
- if (close(gbe_fd) == -1)
- err(ECANCELED, "close '%s'", fname);
-#ifndef HAVE_ARC4RANDOM_BUF
- if (close(urandom_fd) == -1)
- err(ECANCELED, "close '%s'", rname);
-#endif
+ run_cmd(cmd_index);
- /*
- * We still exit with non-zero status if
- * errno is set, but we don't need to print
- * the error on dump commands, because they
- * already print errors.
- *
- * If both parts have bad checksums, then
- * cmd_dump will cause non-zero exit. If at
- * least one part is valid, it resets errno.
- *
- * However, if we're not using cmd_dump, then
- * we have a bug somewhere in the code.
- */
- if (cmd != cmd_dump) {
- if (errno)
- err(ECANCELED, "Unhandled error on exit");
+ if (command[cmd_index].flags == O_RDWR) {
+
+ write_gbe_file();
+
+ /*
+ * We may otherwise read from
+ * cache, so we must sync.
+ */
+ if (fsync(gbe_fd) == -1)
+ err(errno, "%s: fsync (pre-verification)",
+ fname);
+
+ check_written_part(0);
+ check_written_part(1);
+
+ report_io_err_rw();
+
+ if (io_err_gbe)
+ err(EIO, "%s: bad write", fname);
}
- if (errno)
- return EXIT_FAILURE;
- else
- return EXIT_SUCCESS;
+ if (close_files() == -1)
+ err(EIO, "%s: close", fname);
+
+ return EXIT_SUCCESS;
}
+/*
+ * Guard against regressions by maintainers (command table)
+ */
static void
-set_cmd(int argc, char *argv[])
+sanitize_command_list(void)
{
- size_t i;
+ size_t c;
- /*
- * Example: ./nvmutil gbe.bin
- *
- * Here, we assume that the user
- * wants a randomised MAC address.
- */
- if (argc == 2) {
- cmd = cmd_setmac;
- return;
+ for (c = 0; c < N_COMMANDS; c++)
+ sanitize_command_index(c);
+}
+
+/*
+ * TODO: specific config checks per command
+ */
+static void
+sanitize_command_index(size_t c)
+{
+ u8 mod_type;
+ size_t gbe_rw_size;
+
+ check_command_num(c);
+
+ if (command[c].argc < 3)
+ err(EINVAL, "cmd index %lu: argc below 3, %d",
+ (ulong)c, command[c].argc);
+
+ if (command[c].str == NULL)
+ err(EINVAL, "cmd index %lu: NULL str",
+ (ulong)c);
+ if (*command[c].str == '\0')
+ err(EINVAL, "cmd index %lu: empty str",
+ (ulong)c);
+
+ if (xstrxlen(command[c].str, MAX_CMD_LEN + 1) >
+ MAX_CMD_LEN) {
+ err(EINVAL, "cmd index %lu: str too long: %s",
+ (ulong)c, command[c].str);
}
- for (i = 0; i < items(command); i++) {
- if (strcmp(argv[2], command[i].str) != 0)
- continue;
- if (argc >= command[i].args) {
- cmd = command[i].cmd;
- break;
- }
+ mod_type = command[c].set_modified;
+ switch (mod_type) {
+ case SET_MOD_0:
+ case SET_MOD_1:
+ case SET_MOD_N:
+ case SET_MOD_BOTH:
+ case SET_MOD_OFF:
+ break;
+ default:
+ err(EINVAL, "Unsupported set_mod type: %u", mod_type);
+ }
+
+ check_bin(command[c].invert, "cmd.invert");
+ check_bin(command[c].arg_part, "cmd.arg_part");
+ check_bin(command[c].chksum_read, "cmd.chksum_read");
+ check_bin(command[c].chksum_write, "cmd.chksum_write");
+
+ gbe_rw_size = command[c].rw_size;
- err(EINVAL, "Too few args: command '%s'", command[i].str);
+ switch (gbe_rw_size) {
+ case GBE_PART_SIZE:
+ case NVM_SIZE:
+ break;
+ default:
+ err(EINVAL, "Unsupported rw_size: %lu",
+ (ulong)gbe_rw_size);
}
+
+ if (gbe_rw_size > GBE_PART_SIZE)
+ err(EINVAL, "rw_size larger than GbE part: %lu",
+ (ulong)gbe_rw_size);
+
+ if (command[c].flags != O_RDONLY &&
+ command[c].flags != O_RDWR)
+ err(EINVAL, "invalid cmd.flags setting");
}
static void
-check_cmd_args(int argc, char *argv[])
+set_cmd(int argc, char *argv[])
{
- if (cmd == NULL && argc > 2) {
- /*
- * Here, no valid command was found, but a
- * 3rd argument is available, which tells
- * us that the 3rd argument is a MAC address
- * supplied by the user, which could also
- * contain one or more random characters.
- *
- * This is intentional, because a lot of
- * users might run something like:
- *
- * ./nvmutil gbe.bin xx:1f:16:??:??:??
- *
- * Instead of (more properly):
- *
- * ./nvmutil gbe.bin setmac xx:1f:16:??:??:??
- *
- * This quirk makes the tool easier to use.
- */
- mac_str = argv[2];
- cmd = cmd_setmac;
- } else if (cmd == cmd_setmac) {
- /*
- * ./nvmutil gbe.bin setmac [MAC]
- */
- mac_str = rmac; /* random MAC */
- if (argc > 3)
- mac_str = argv[3];
- } else if (cmd != NULL && argc > 3) { /* user-supplied partnum */
- /*
- * Example: ./nvmutil gbe.bin copy 0
- */
- part = conv_argv_part_num(argv[3]);
+ const char *cmd_str;
+
+ for (cmd_index = 0; valid_command(cmd_index); cmd_index++) {
+ cmd_str = command[cmd_index].str;
+
+ if (xstrxcmp(argv[2], cmd_str, MAX_CMD_LEN) != 0)
+ continue;
+ else if (argc >= command[cmd_index].argc)
+ return;
+
+ err(EINVAL, "Too few args on command '%s'", cmd_str);
}
- if (cmd == NULL)
- err(EINVAL, "Bad command");
+ cmd_index = CMD_NULL;
}
-static size_t
-conv_argv_part_num(const char *part_str)
+static void
+set_cmd_args(int argc, char *argv[])
{
- unsigned char ch;
+ u8 arg_part;
- /*
- * Because char signedness is implementation-defined,
- * we cast to unsigned char before arithmetic.
- */
+ if (!valid_command(cmd_index) || argc < 3)
+ usage(1);
- if (part_str[0] == '\0' || part_str[1] != '\0')
- err(EINVAL, "Partnum string '%s' wrong length", part_str);
+ arg_part = command[cmd_index].arg_part;
- ch = (unsigned char)part_str[0] - '0';
+ /* Maintainer bugs */
+ if (arg_part && argc < 4)
+ err(EINVAL,
+ "arg_part set for command that needs argc4");
+ if (arg_part && cmd_index == CMD_SETMAC)
+ err(EINVAL,
+ "arg_part set on CMD_SETMAC");
- check_part_num((size_t)ch);
- return (size_t)ch;
+ if (cmd_index == CMD_SETMAC)
+ mac_str = argc >= 4 ? argv[3] : rmac;
+ else if (arg_part)
+ part = conv_argv_part_num(argv[3]);
}
-static void
-set_io_flags(int argc, char *argv[])
+static size_t
+conv_argv_part_num(const char *part_str)
{
- gbe_flags = O_RDWR;
+ u8 ch;
- if (argc < 3)
- return;
+ if (part_str[0] == '\0' || part_str[1] != '\0')
+ err(EINVAL, "Partnum string '%s' wrong length", part_str);
+
+ /* char signedness is implementation-defined */
+ ch = (u8)part_str[0];
+ if (ch < '0' || ch > '1')
+ err(EINVAL, "Bad part number (%c)", ch);
- if (strcmp(argv[2], "dump") == 0)
- gbe_flags = O_RDONLY;
+ return (size_t)(ch - '0');
}
-#ifndef HAVE_ARC4RANDOM_BUF
-static void
-open_dev_urandom(void)
+/*
+ * Portable strcmp() but blocks NULL/empty/unterminated
+ * strings. Even stricter than strncmp().
+ */
+static int
+xstrxcmp(const char *a, const char *b, size_t maxlen)
{
- struct stat st_urandom_fd;
+ size_t i;
- rname = newrandom;
+ if (a == NULL || b == NULL)
+ err(EINVAL, "NULL input to xstrxcmp");
- if ((urandom_fd = open(rname, O_RDONLY)) == -1) {
- /*
- * Fall back to /dev/random on old platforms
- * where /dev/urandom does not exist.
- *
- * We must reset the error condition first,
- * to prevent stale error status later.
- */
- errno = 0;
+ if (*a == '\0' || *b == '\0')
+ err(EINVAL, "Empty string in xstrxcmp");
+
+ for (i = 0; i < maxlen; i++) {
+ u8 ac = (u8)a[i];
+ u8 bc = (u8)b[i];
+
+ if (ac == '\0' || bc == '\0') {
+ if (ac == bc)
+ return 0;
+ return ac - bc;
+ }
- rname = oldrandom;
- xopen(&urandom_fd, rname, O_RDONLY, &st_urandom_fd);
+ if (ac != bc)
+ return ac - bc;
}
+
+ /*
+ * We reached maxlen, so assume unterminated string.
+ */
+ err(EINVAL, "Unterminated string in xstrxcmp");
+
+ /*
+ * Should never reach here. This keeps compilers happy.
+ */
+ errno = EINVAL;
+ return -1;
}
-#endif
static void
open_gbe_file(void)
{
struct stat gbe_st;
+ int flags;
- xopen(&gbe_fd, fname, gbe_flags, &gbe_st);
+ xopen(&gbe_fd, fname,
+ command[cmd_index].flags | O_BINARY | O_NOFOLLOW, &gbe_st);
+
+ /* inode will be checked later on write */
+ gbe_dev = gbe_st.st_dev;
+ gbe_ino = gbe_st.st_ino;
+
+ if (gbe_st.st_nlink > 1)
+ fprintf(stderr,
+ "%s: warning: file has %lu hard links\n",
+ fname, (ulong)gbe_st.st_nlink);
+
+ if (gbe_st.st_nlink == 0)
+ err(EIO, "%s: file unlinked while open", fname);
+
+ flags = fcntl(gbe_fd, F_GETFL);
+ if (flags == -1)
+ err(errno, "%s: fcntl(F_GETFL)", fname);
+
+ /*
+ * O_APPEND must not be used, because this
+ * allows POSIX write() to ignore the
+ * current write offset and write at EOF,
+ * which would therefore break pread/pwrite
+ */
+ if (flags & O_APPEND)
+ err(EIO, "%s: O_APPEND flag");
gbe_file_size = gbe_st.st_size;
@@ -400,78 +1014,165 @@ open_gbe_file(void)
case SIZE_128KB:
break;
default:
- err(ECANCELED, "File size must be 8KB, 16KB or 128KB");
+ err(EINVAL, "File size must be 8KB, 16KB or 128KB");
}
}
static void
+lock_gbe_file(void)
+{
+ struct flock fl;
+
+ memset(&fl, 0, sizeof(fl));
+
+ if (command[cmd_index].flags == O_RDONLY)
+ fl.l_type = F_RDLCK;
+ else
+ fl.l_type = F_WRLCK;
+
+ fl.l_whence = SEEK_SET;
+
+ if (fcntl(gbe_fd, F_SETLK, &fl) == -1)
+ err(errno, "file is locked by another process");
+}
+
+static void
xopen(int *fd_ptr, const char *path, int flags, struct stat *st)
{
if ((*fd_ptr = open(path, flags)) == -1)
- err(ECANCELED, "%s", path);
+ err(errno, "%s", path);
+
if (fstat(*fd_ptr, st) == -1)
- err(ECANCELED, "%s", path);
+ err(errno, "%s", path);
+
+ if (!S_ISREG(st->st_mode))
+ err(errno, "%s: not a regular file", path);
+
+ if (lseek(*fd_ptr, 0, SEEK_CUR) == (off_t)-1)
+ err(errno, "%s: file not seekable", path);
}
static void
read_gbe_file(void)
{
size_t p;
- uint8_t do_read[2] = {1, 1};
+ u8 do_read[2] = {1, 1};
/*
- * The copy, brick and setchecksum commands need
- * only read data from the user-specified part.
- *
- * We can skip reading the other part, thus:
+ * Commands specifying a partnum only
+ * need the given GbE part to be read.
*/
- if (cmd == cmd_copy ||
- cmd == cmd_brick ||
- cmd == cmd_setchecksum)
+ if (command[cmd_index].arg_part)
do_read[part ^ 1] = 0;
+ for (p = 0; p < 2; p++) {
+ if (do_read[p])
+ rw_gbe_file_part(p, IO_PREAD, "pread");
+ }
+}
+
+static void
+read_checksums(void)
+{
+ size_t p;
+ size_t skip_part;
+ u8 invert;
+ u8 arg_part;
+ u8 num_invalid;
+ u8 max_invalid;
+
+ part_valid[0] = 0;
+ part_valid[1] = 0;
+
+ if (!command[cmd_index].chksum_read)
+ return;
+
+ num_invalid = 0;
+ max_invalid = 2;
+
+ invert = command[cmd_index].invert;
+ arg_part = command[cmd_index].arg_part;
+ if (arg_part)
+ max_invalid = 1;
+
/*
- * SPEED HACK:
- *
- * On copy/swap commands, flip where data gets written to memory,
- * so that cmd_copy and cmd_swap don't have to work on every word
- *
- * NOTE:
- *
- * write_gbe_file() will not use this, but copy/setchecksum commands
- * will directly manipulate part_modified[], telling write_gbe_file()
- * to also write in reverse, as in read_gbe_file().
+ * Skip verification on this part,
+ * but only when arg_part is set.
*/
- if (cmd == cmd_copy || cmd == cmd_swap)
- invert = 1;
+ skip_part = part ^ 1 ^ invert;
for (p = 0; p < 2; p++) {
- if (do_read[p])
- read_gbe_file_part(p, invert);
+ /*
+ * Only verify a part if it was *read*
+ */
+ if (arg_part && (p == skip_part))
+ continue;
+
+ part_valid[p] = good_checksum(p);
+ if (!part_valid[p])
+ ++num_invalid;
+ }
+
+ if (num_invalid >= max_invalid) {
+ if (max_invalid == 1)
+ err(ECANCELED, "%s: part %lu has a bad checksum",
+ fname, (ulong)part);
+ err(ECANCELED, "%s: No valid checksum found in file",
+ fname);
}
}
+static int
+good_checksum(size_t partnum)
+{
+ ushort expected_checksum = calculated_checksum(partnum);
+ ushort current_checksum = nvm_word(NVM_CHECKSUM_WORD, partnum);
+
+ if (current_checksum == expected_checksum)
+ return 1;
+
+ return 0;
+}
+
static void
-read_gbe_file_part(size_t p, uint8_t invert)
+run_cmd(size_t c)
{
- read_file_exact(gbe_fd, gbe_mem_offset(p ^ invert, "pread"),
- GBE_PART_SIZE, gbe_file_offset(p, "pread"), fname, "pread");
+ check_command_num(c);
+ if (command[c].run != NULL)
+ command[c].run();
}
static void
-cmd_setmac(void)
+check_command_num(size_t c)
+{
+ if (!valid_command(c))
+ err(EINVAL, "Invalid run_cmd arg: %lu",
+ (ulong)c);
+}
+
+static u8
+valid_command(size_t c)
+{
+ if (c >= N_COMMANDS)
+ return 0;
+
+ if (c != command[c].chk)
+ err(EINVAL, "Invalid cmd chk value (%lu) vs arg: %lu",
+ (ulong)command[c].chk, (ulong)c);
+
+ return 1;
+}
+
+static void
+cmd_helper_setmac(void)
{
size_t partnum;
- uint8_t mac_updated = 0;
- parse_mac_string();
printf("MAC address to be written: %s\n", mac_str);
+ parse_mac_string();
for (partnum = 0; partnum < 2; partnum++)
- mac_updated |= write_mac_part(partnum);
-
- if (mac_updated)
- errno = 0;
+ write_mac_part(partnum);
}
static void
@@ -479,7 +1180,7 @@ parse_mac_string(void)
{
size_t mac_byte;
- if (strlen(mac_str) != 17)
+ if (xstrxlen(mac_str, 18) != 17)
err(EINVAL, "MAC address is the wrong length");
memset(mac_buf, 0, sizeof(mac_buf));
@@ -494,6 +1195,33 @@ parse_mac_string(void)
err(EINVAL, "Must not specify multicast MAC address");
}
+/*
+ * strnlen() but aborts on NULL input, and empty strings.
+ * Our version also prohibits unterminated strings.
+ * strnlen() was standardized in POSIX.1-2008 and is not
+ * available on some older systems, so we provide our own.
+ */
+static size_t
+xstrxlen(const char *scmp, size_t maxlen)
+{
+ size_t xstr_index;
+
+ if (scmp == NULL)
+ err(EINVAL, "NULL input to xstrxlen");
+
+ if (*scmp == '\0')
+ err(EINVAL, "Empty string in xstrxlen");
+
+ for (xstr_index = 0;
+ xstr_index < maxlen && scmp[xstr_index] != '\0';
+ xstr_index++);
+
+ if (xstr_index == maxlen)
+ err(EINVAL, "Unterminated string in xstrxlen");
+
+ return xstr_index;
+}
+
static void
set_mac_byte(size_t mac_byte_pos)
{
@@ -513,159 +1241,176 @@ set_mac_byte(size_t mac_byte_pos)
static void
set_mac_nib(size_t mac_str_pos,
- size_t mac_byte_pos, size_t mac_nib_pos)
+ size_t mac_byte_pos, size_t mac_nib_pos)
{
char mac_ch;
- uint16_t hex_num;
+ ushort hex_num;
mac_ch = mac_str[mac_str_pos + mac_nib_pos];
- hex_num = hextonum(mac_ch);
- if (hex_num > 15)
+ if ((hex_num = hextonum(mac_ch)) > 15)
err(EINVAL, "Invalid character '%c'",
mac_str[mac_str_pos + mac_nib_pos]);
- /* If random, ensure that local/unicast bits are set */
+ /*
+ * If random, ensure that local/unicast bits are set.
+ */
if ((mac_byte_pos == 0) && (mac_nib_pos == 1) &&
((mac_ch | 0x20) == 'x' ||
(mac_ch == '?')))
hex_num = (hex_num & 0xE) | 2; /* local, unicast */
/*
- * Words other than the MAC address are stored little
- * endian in the file, and we handle that when reading.
- * However, MAC address words are stored big-endian
- * in that file, so we write each 2-byte word logically
- * in little-endian order, which on little-endian would
- * be stored big-endian in memory, and vice versa.
- *
- * Later code using the MAC string will handle this.
+ * MAC words stored big endian in-file, little-endian
+ * logically, so we reverse the order.
*/
-
mac_buf[mac_byte_pos >> 1] |= hex_num <<
(((mac_byte_pos & 1) << 3) /* left or right byte? */
| ((mac_nib_pos ^ 1) << 2)); /* left or right nib? */
}
-static uint8_t
+static ushort
hextonum(char ch_s)
{
- /*
- * We assume char is signed, hence ch_s.
- * We explicitly cast to unsigned:
- */
- unsigned char ch = (unsigned char)ch_s;
+ u8 ch = (u8)ch_s;
- if ((unsigned)(ch - '0') <= 9)
+ if ((uint)(ch - '0') <= 9)
return ch - '0';
ch |= 0x20;
- if ((unsigned)(ch - 'a') <= 5)
+ if ((uint)(ch - 'a') <= 5)
return ch - 'a' + 10;
- else if (ch == '?' || ch == 'x')
+
+ if (ch == '?' || ch == 'x')
return rhex(); /* random character */
- else
- return 16; /* invalid character */
+
+ return 16; /* invalid character */
}
-static uint8_t
+#if defined(HAVE_ARC4RANDOM_BUF) && \
+ (HAVE_ARC4RANDOM_BUF) > 0
+static ushort
rhex(void)
{
+ static u8 num[12];
static size_t n = 0;
- static uint8_t rnum[12];
if (!n) {
- n = sizeof(rnum);
-#ifdef HAVE_ARC4RANDOM_BUF
- arc4random_buf(rnum, n);
-#else
- read_file_exact(urandom_fd, rnum, n, 0, rname, NULL);
-#endif
+ n = 12;
+ arc4random_buf(num, 12);
}
- return rnum[--n] & 0xf;
+ return num[--n] & 0xf;
}
-
-static void
-read_file_exact(int fd, void *buf, size_t len,
- off_t off, const char *path, const char *op)
+#else
+static ushort
+rhex(void)
{
- int retry;
- ssize_t rval;
+ struct timeval tv;
+ ulong mix;
+ static ulong counter = 0;
- for (retry = 0; retry < MAX_RETRY_READ; retry++) {
- if (op)
- rval = pread(fd, buf, len, off);
- else
- rval = read(fd, buf, len);
+ gettimeofday(&tv, NULL);
- if (rval == (ssize_t)len) {
- errno = 0;
- return;
- }
+ mix = (ulong)tv.tv_sec
+ ^ (ulong)tv.tv_usec
+ ^ (ulong)getpid()
+ ^ (ulong)&mix
+ ^ counter++
+ ^ entropy_jitter();
+
+ /*
+ * Stack addresses can vary between
+ * calls, thus increasing entropy.
+ */
+ mix ^= (ulong)&mix;
+ mix ^= (ulong)&tv;
+ mix ^= (ulong)&counter;
+
+ return (ushort)(mix & 0xf);
+}
+
+static ulong
+entropy_jitter(void)
+{
+ struct timeval a, b;
+ ulong mix = 0;
+ long mix_diff;
+ int i;
+
+ for (i = 0; i < 8; i++) {
+ gettimeofday(&a, NULL);
+ getpid();
+ gettimeofday(&b, NULL);
- if (rval != -1)
- err(ECANCELED,
- "Short %s, %zd bytes, on file: %s",
- op ? op : "read", rval, path);
+ /*
+ * prevent negative numbers to prevent overflow,
+ * which would bias rand to large numbers
+ */
+ mix_diff = (long)(b.tv_usec - a.tv_usec);
+ if (mix_diff < 0)
+ mix_diff = -mix_diff;
- if (errno != EINTR)
- err(ECANCELED,
- "Could not %s file: '%s'",
- op ? op : "read", path);
+ mix ^= (ulong)(mix_diff);
+ mix ^= (ulong)&mix;
}
- err(EINTR, "%s: max retries exceeded on file: %s",
- op ? op : "read", path);
+ return mix;
}
+#endif
-static int
+static void
write_mac_part(size_t partnum)
{
size_t w;
- if (!good_checksum(partnum))
- return 0;
+ check_bin(partnum, "part number");
+ if (!part_valid[partnum])
+ return;
for (w = 0; w < 3; w++)
- set_word(w, partnum, mac_buf[w]);
-
- printf("Wrote MAC address to part %zu: ", partnum);
- print_mac_address(partnum);
-
- set_checksum(partnum);
+ set_nvm_word(w, partnum, mac_buf[w]);
- return 1;
+ printf("Wrote MAC address to part %lu: ",
+ (ulong)partnum);
+ print_mac_from_nvm(partnum);
}
static void
-cmd_dump(void)
+cmd_helper_dump(void)
{
size_t partnum;
- int num_invalid = 0;
- for (partnum = 0; partnum < 2; partnum++) {
- if (!good_checksum(partnum))
- ++num_invalid;
+ part_valid[0] = good_checksum(0);
+ part_valid[1] = good_checksum(1);
- printf("MAC (part %zu): ", partnum);
- print_mac_address(partnum);
+ for (partnum = 0; partnum < 2; partnum++) {
+ if (!part_valid[partnum])
+ fprintf(stderr,
+ "BAD checksum %04x in part %lu (expected %04x)\n",
+ nvm_word(NVM_CHECKSUM_WORD, partnum),
+ (ulong)partnum,
+ calculated_checksum(partnum));
+
+ printf("MAC (part %lu): ",
+ (ulong)partnum);
+ print_mac_from_nvm(partnum);
hexdump(partnum);
}
-
- if (num_invalid < 2)
- errno = 0;
}
static void
-print_mac_address(size_t partnum)
+print_mac_from_nvm(size_t partnum)
{
size_t c;
+ ushort val16;
for (c = 0; c < 3; c++) {
- uint16_t val16 = word(c, partnum);
- printf("%02x:%02x", val16 & 0xff, val16 >> 8);
+ val16 = nvm_word(c, partnum);
+ printf("%02x:%02x",
+ (uint)(val16 & 0xff),
+ (uint)(val16 >> 8));
if (c == 2)
printf("\n");
else
@@ -678,212 +1423,347 @@ hexdump(size_t partnum)
{
size_t c;
size_t row;
- uint16_t val16;
+ ushort val16;
for (row = 0; row < 8; row++) {
- printf("%08zx ", row << 4);
+ printf("%08lx ", (ulong)((size_t)row << 4));
for (c = 0; c < 8; c++) {
- val16 = word((row << 3) + c, partnum);
+ val16 = nvm_word((row << 3) + c, partnum);
if (c == 4)
printf(" ");
- printf(" %02x %02x", val16 & 0xff, val16 >> 8);
+ printf(" %02x %02x",
+ (uint)(val16 & 0xff),
+ (uint)(val16 >> 8));
}
printf("\n");
}
}
static void
-cmd_setchecksum(void)
+cmd_helper_cat(void)
{
- set_checksum(part);
-}
+ size_t p;
+ size_t ff;
+ size_t n = 0;
-static void
-set_checksum(size_t p)
-{
- size_t c;
- uint16_t val16 = 0;
+ if (cmd_index == CMD_CAT16)
+ n = 1;
+ else if (cmd_index == CMD_CAT128)
+ n = 15;
+ else if (cmd_index != CMD_CAT)
+ err(EINVAL, "cmd_helper_cat called erroneously");
- check_part_num(p);
+ fflush(NULL);
- for (c = 0; c < NVM_CHECKSUM_WORD; c++)
- val16 += word(c, p);
+ for (p = 0; p < 2; p++) {
+ gbe_cat_buf(buf + (size_t)(p * GBE_PART_SIZE));
- set_word(NVM_CHECKSUM_WORD, p, NVM_CHECKSUM - val16);
+ for (ff = 0; ff < n; ff++)
+ gbe_cat_buf(pad);
+ }
}
static void
-cmd_brick(void)
+gbe_cat_buf(u8 *b)
{
- uint16_t checksum_word;
-
- if (!good_checksum(part)) {
- err(ECANCELED,
- "Part %zu checksum already invalid in file '%s'",
- part, fname);
- }
-
- /*
- * We know checksum_word is valid, so we need only
- * flip one bit to invalidate it.
- */
- checksum_word = word(NVM_CHECKSUM_WORD, part);
- set_word(NVM_CHECKSUM_WORD, part, checksum_word ^ 1);
+ if (rw_file_exact(STDOUT_FILENO, b,
+ GBE_PART_SIZE, 0, IO_WRITE, LOOP_EAGAIN, LOOP_EINTR,
+ MAX_ZERO_RW_RETRY, OFF_ERR) < 0)
+ err(errno, "stdout: cat");
}
static void
-cmd_copy(void)
+write_gbe_file(void)
{
- if (!good_checksum(part ^ 1))
- err(ECANCELED, "copy p%zu, file '%s'", part ^ 1, fname);
+ struct stat gbe_st;
- /*
- * SPEED HACK:
- *
- * read_gbe_file() already performed the copy,
- * by virtue of inverted read. We need
- * only set the other part as changed.
- */
- set_part_modified(part ^ 1);
+ size_t p;
+ size_t partnum;
+ u8 update_checksum;
+
+ if (command[cmd_index].flags == O_RDONLY)
+ return;
+
+ update_checksum = command[cmd_index].chksum_write;
+
+ override_part_modified();
+
+ if (fstat(gbe_fd, &gbe_st) == -1)
+ err(errno, "%s: re-check", fname);
+
+ if (gbe_st.st_dev != gbe_dev || gbe_st.st_ino != gbe_ino)
+ err(EIO, "%s: file replaced while open", fname);
+
+ if (gbe_st.st_size != gbe_file_size)
+ err(errno, "%s: file size changed before write", fname);
+
+ if (!S_ISREG(gbe_st.st_mode))
+ err(errno, "%s: file type changed before write", fname);
+
+ for (p = 0; p < 2; p++) {
+ partnum = p ^ command[cmd_index].invert;
+
+ if (!part_modified[partnum])
+ continue;
+
+ if (update_checksum)
+ set_checksum(partnum);
+
+ rw_gbe_file_part(partnum, IO_PWRITE, "pwrite");
+ }
}
static void
-cmd_swap(void)
+override_part_modified(void)
{
- if (!(good_checksum(0) || good_checksum(1)))
- err(ECANCELED, "swap parts, file '%s'", fname);
+ u8 mod_type = command[cmd_index].set_modified;
- /*
- * good_checksum() can set errno, if one
- * of the parts is bad. We will reset it.
- */
- errno = 0;
-
- /*
- * SPEED HACK:
- *
- * read_gbe_file() already performed the swap,
- * by virtue of inverted read. We need
- * only set both parts as changed.
- */
- set_part_modified(0);
- set_part_modified(1);
+ switch (mod_type) {
+ case SET_MOD_0:
+ set_part_modified(0);
+ break;
+ case SET_MOD_1:
+ set_part_modified(1);
+ break;
+ case SET_MOD_N:
+ set_part_modified(part ^ command[cmd_index].invert);
+ break;
+ case SET_MOD_BOTH:
+ set_part_modified(0);
+ set_part_modified(1);
+ break;
+ case SET_MOD_OFF:
+ break;
+ default:
+ err(EINVAL, "Unsupported set_mod type: %u",
+ mod_type);
+ }
}
-static int
-good_checksum(size_t partnum)
+static void
+set_checksum(size_t p)
{
- size_t w;
- uint16_t total = 0;
-
- for (w = 0; w <= NVM_CHECKSUM_WORD; w++)
- total += word(w, partnum);
+ check_bin(p, "part number");
+ set_nvm_word(NVM_CHECKSUM_WORD, p, calculated_checksum(p));
+}
- if (total == NVM_CHECKSUM)
- return 1;
+static ushort
+calculated_checksum(size_t p)
+{
+ size_t c;
+ uint val16 = 0;
- fprintf(stderr, "WARNING: BAD checksum in part %zu\n",
- partnum ^ invert);
+ for (c = 0; c < NVM_CHECKSUM_WORD; c++)
+ val16 += (uint)nvm_word(c, p);
- set_err(ECANCELED);
- return 0;
+ return (ushort)((NVM_CHECKSUM - val16) & 0xffff);
}
/*
* GbE NVM files store 16-bit (2-byte) little-endian words.
* We must therefore swap the order when reading or writing.
+ *
+ * NOTE: The MAC address words are stored big-endian in the
+ * file, but we assume otherwise and adapt accordingly.
*/
-static uint16_t
-word(size_t pos16, size_t p)
+static ushort
+nvm_word(size_t pos16, size_t p)
{
size_t pos;
check_nvm_bound(pos16, p);
pos = (pos16 << 1) + (p * GBE_PART_SIZE);
- return buf[pos] | (buf[pos + 1] << 8);
+ return (ushort)buf[pos] |
+ ((ushort)buf[pos + 1] << 8);
}
static void
-set_word(size_t pos16, size_t p, uint16_t val16)
+set_nvm_word(size_t pos16, size_t p, ushort val16)
{
size_t pos;
check_nvm_bound(pos16, p);
pos = (pos16 << 1) + (p * GBE_PART_SIZE);
- buf[pos] = (uint8_t)(val16 & 0xff);
- buf[pos + 1] = (uint8_t)(val16 >> 8);
+ buf[pos] = (u8)(val16 & 0xff);
+ buf[pos + 1] = (u8)(val16 >> 8);
set_part_modified(p);
}
static void
+set_part_modified(size_t p)
+{
+ check_bin(p, "part number");
+ part_modified[p] = 1;
+}
+
+static void
check_nvm_bound(size_t c, size_t p)
{
/*
- * NVM_SIZE assumed as the limit, because the
+ * NVM_SIZE assumed as the limit, because this
* current design assumes that we will only
* ever modified the NVM area.
- *
- * The only exception is copy/swap, but these
- * do not use word/set_word and therefore do
- * not cause check_nvm_bound() to be called.
- *
- * TODO:
- * This should be adjusted in the future, if
- * we ever wish to work on the extented area.
*/
- check_part_num(p);
+ check_bin(p, "part number");
if (c >= NVM_WORDS)
- err(EINVAL, "check_nvm_bound: out of bounds %zu", c);
+ err(ECANCELED, "check_nvm_bound: out of bounds %lu",
+ (ulong)c);
}
static void
-write_gbe_file(void)
+check_bin(size_t a, const char *a_name)
{
- size_t p;
+ if (a > 1)
+ err(EINVAL, "%s must be 0 or 1, but is %lu",
+ a_name, (ulong)a);
+}
- if (gbe_flags == O_RDONLY)
- return;
+static void
+rw_gbe_file_part(size_t p, int rw_type,
+ const char *rw_type_str)
+{
+ ssize_t r;
+ size_t gbe_rw_size = command[cmd_index].rw_size;
+ u8 invert = command[cmd_index].invert;
- for (p = 0; p < 2; p++) {
- if (part_modified[p])
- write_gbe_file_part(p);
- }
+ u8 *mem_offset;
+ off_t file_offset;
+
+ if (rw_type < IO_PREAD || rw_type > IO_PWRITE)
+ err(errno, "%s: %s: part %lu: invalid rw_type, %d",
+ fname, rw_type_str, (ulong)p, rw_type);
+
+ if (rw_type == IO_PWRITE)
+ invert = 0;
+
+ /*
+ * Inverted reads are used by copy/swap.
+ * E.g. read from p0 (file) to p1 (mem).
+ */
+ mem_offset = gbe_mem_offset(p ^ invert, rw_type_str);
+ file_offset = (off_t)gbe_file_offset(p, rw_type_str);
+
+ r = rw_gbe_file_exact(gbe_fd, mem_offset,
+ gbe_rw_size, file_offset, rw_type);
+
+ if (r == -1)
+ err(errno, "%s: %s: part %lu",
+ fname, rw_type_str, (ulong)p);
+
+ if ((size_t)r != gbe_rw_size)
+ err(EIO, "%s: partial %s: part %lu",
+ fname, rw_type_str, (ulong)p);
}
static void
-write_gbe_file_part(size_t p)
+check_written_part(size_t p)
{
- ssize_t rval = pwrite(gbe_fd, gbe_mem_offset(p, "pwrite"),
- GBE_PART_SIZE, gbe_file_offset(p, "pwrite"));
+ ssize_t r;
+ size_t gbe_rw_size;
+ u8 *mem_offset;
+ off_t file_offset;
+ u8 *buf_restore;
+ struct stat st;
+
+ if (!part_modified[p])
+ return;
+
+ gbe_rw_size = command[cmd_index].rw_size;
- if (rval == -1)
- err(ECANCELED, "Can't write %zu b to '%s' p%zu",
- GBE_PART_SIZE, fname, p);
+ /* invert not needed for pwrite */
+ mem_offset = gbe_mem_offset(p, "pwrite");
+ file_offset = (off_t)gbe_file_offset(p, "pwrite");
- if (rval != GBE_PART_SIZE)
- err(ECANCELED, "CORRUPTED WRITE (%zd b) to file '%s' p%zu",
- rval, fname, p);
+ memset(pad, 0xff, sizeof(pad));
+
+ if (fstat(gbe_fd, &st) == -1)
+ err(errno, "%s: fstat (post-write)", fname);
+
+ if (st.st_dev != gbe_dev || st.st_ino != gbe_ino)
+ err(EIO, "%s: file changed during write", fname);
+
+ r = rw_gbe_file_exact(gbe_fd, pad,
+ gbe_rw_size, file_offset, IO_PREAD);
+
+ if (r == -1)
+ rw_check_err_read[p] = io_err_gbe = 1;
+ else if ((size_t)r != gbe_rw_size)
+ rw_check_partial_read[p] = io_err_gbe = 1;
+ else if (memcmp(mem_offset, pad, gbe_rw_size) != 0)
+ rw_check_bad_part[p] = io_err_gbe = 1;
+
+ if (rw_check_err_read[p] ||
+ rw_check_partial_read[p])
+ return;
+
+ /*
+ * We only load one part on-file, into memory but
+ * always at offset zero, for post-write checks.
+ * That's why we hardcode good_checksum(0).
+ */
+ buf_restore = buf;
+ buf = pad;
+ post_rw_checksum[p] = good_checksum(0);
+ buf = buf_restore;
}
-/*
- * Reads to GbE from write_gbe_file_part and read_gbe_file_part
- * are filtered through here. These operations must
- * only write from the 0th position or the half position
- * within the GbE file, and write 4KB of data.
- *
- * This check is called, to ensure just that.
- */
-static off_t
-gbe_file_offset(size_t p, const char *f_op)
+static void
+report_io_err_rw(void)
{
- return gbe_x_offset(p, f_op, "file",
- gbe_file_size >> 1, gbe_file_size);
+ size_t p;
+
+ if (!io_err_gbe)
+ return;
+
+ for (p = 0; p < 2; p++) {
+ if (!part_modified[p])
+ continue;
+
+ if (rw_check_err_read[p])
+ fprintf(stderr,
+ "%s: pread: p%lu (post-verification)\n",
+ fname, (ulong)p);
+ if (rw_check_partial_read[p])
+ fprintf(stderr,
+ "%s: partial pread: p%lu (post-verification)\n",
+ fname, (ulong)p);
+ if (rw_check_bad_part[p])
+ fprintf(stderr,
+ "%s: pwrite: corrupt write on p%lu\n",
+ fname, (ulong)p);
+
+ if (rw_check_err_read[p] ||
+ rw_check_partial_read[p]) {
+ fprintf(stderr,
+ "%s: p%lu: skipped checksum verification "
+ "(because read failed)\n",
+ fname, (ulong)p);
+
+ continue;
+ }
+
+ fprintf(stderr, "%s: ", fname);
+
+ if (post_rw_checksum[p])
+ fprintf(stderr, "GOOD");
+ else
+ fprintf(stderr, "BAD");
+
+ fprintf(stderr, " checksum in p%lu on-disk.\n",
+ (ulong)p);
+
+ if (post_rw_checksum[p]) {
+ fprintf(stderr,
+ " This does NOT mean it's safe. it may be\n"
+ " salvageable if you use the cat feature.\n");
+ }
+ }
}
/*
@@ -891,13 +1771,29 @@ gbe_file_offset(size_t p, const char *f_op)
* but used to check Gbe bounds in memory,
* and it is *also* used during file I/O.
*/
-static void *
+static u8 *
gbe_mem_offset(size_t p, const char *f_op)
{
off_t gbe_off = gbe_x_offset(p, f_op, "mem",
GBE_PART_SIZE, GBE_FILE_SIZE);
- return (void *)(buf + gbe_off);
+ return (u8 *)(buf + (size_t)gbe_off);
+}
+
+/*
+ * I/O operations filtered here. These operations must
+ * only write from the 0th position or the half position
+ * within the GbE file, and write 4KB of data.
+ *
+ * This check is called, to ensure just that.
+ */
+static off_t
+gbe_file_offset(size_t p, const char *f_op)
+{
+ off_t gbe_file_half_size = gbe_file_size >> 1;
+
+ return gbe_x_offset(p, f_op, "file",
+ gbe_file_half_size, gbe_file_size);
}
static off_t
@@ -906,57 +1802,493 @@ gbe_x_offset(size_t p, const char *f_op, const char *d_type,
{
off_t off;
- check_part_num(p);
+ check_bin(p, "part number");
- off = (off_t)p * nsize;
+ off = ((off_t)p) * (off_t)nsize;
- if (off + GBE_PART_SIZE > ncmp)
- err(ECANCELED, "GbE %s %s out of bounds: %s",
- d_type, f_op, fname);
+ if (off > ncmp - GBE_PART_SIZE)
+ err(ECANCELED, "%s: GbE %s %s out of bounds",
+ fname, d_type, f_op);
if (off != 0 && off != ncmp >> 1)
- err(ECANCELED, "GbE %s %s at bad offset: %s",
- d_type, f_op, fname);
+ err(ECANCELED, "%s: GbE %s %s at bad offset",
+ fname, d_type, f_op);
return off;
}
-static void
-set_part_modified(size_t p)
+static ssize_t
+rw_gbe_file_exact(int fd, u8 *mem, size_t nrw,
+ off_t off, int rw_type)
{
- check_part_num(p);
- part_modified[p] = 1;
+ size_t mem_addr;
+ size_t buf_addr;
+ ssize_t r;
+
+ if (io_args(fd, mem, nrw, off, rw_type) == -1)
+ return -1;
+
+ mem_addr = (size_t)(void *)mem;
+ buf_addr = (size_t)(void *)buf;
+
+ if (mem != (void *)pad) {
+ if (mem_addr < buf_addr)
+ goto err_rw_gbe_file_exact;
+
+ if ((mem_addr - buf_addr) >= (size_t)GBE_FILE_SIZE)
+ goto err_rw_gbe_file_exact;
+ }
+
+ if (off < 0 || off >= gbe_file_size)
+ goto err_rw_gbe_file_exact;
+
+ if (nrw > (size_t)(gbe_file_size - off))
+ goto err_rw_gbe_file_exact;
+
+ if (nrw > (size_t)GBE_PART_SIZE)
+ goto err_rw_gbe_file_exact;
+
+ r = rw_file_exact(fd, mem, nrw, off, rw_type,
+ NO_LOOP_EAGAIN, LOOP_EINTR, MAX_ZERO_RW_RETRY,
+ OFF_ERR);
+
+ return rw_over_nrw(r, nrw);
+
+err_rw_gbe_file_exact:
+ errno = EIO;
+ return -1;
}
-static void
-check_part_num(size_t p)
+/*
+ * Safe I/O functions wrapping around
+ * read(), write() and providing a portable
+ * analog of both pread() and pwrite().
+ * These functions are designed for maximum
+ * robustness, checking NULL inputs, overflowed
+ * outputs, and all kinds of errors that the
+ * standard libc functions don't.
+ *
+ * Looping on EINTR and EAGAIN is supported.
+ * EINTR/EAGAIN looping is done indefinitely.
+ */
+
+/*
+ * rw_file_exact() - Read perfectly or die
+ *
+ * Read/write, and absolutely insist on an
+ * absolute read; e.g. if 100 bytes are
+ * requested, this MUST return 100.
+ *
+ * This function will never return zero.
+ * It will only return below (error),
+ * or above (success). On error, -1 is
+ * returned and errno is set accordingly.
+ *
+ * Zero-byte returns are not allowed.
+ * It will re-spin a finite number of
+ * times upon zero-return, to recover,
+ * otherwise it will return an error.
+ */
+static ssize_t
+rw_file_exact(int fd, u8 *mem, size_t nrw,
+ off_t off, int rw_type, int loop_eagain,
+ int loop_eintr, size_t max_retries,
+ int off_reset)
{
- if (p > 1)
- err(EINVAL, "Bad part number (%zu)", p);
+ ssize_t rv = 0;
+ ssize_t rc = 0;
+ size_t retries_on_zero = 0;
+ off_t off_cur;
+ size_t nrw_cur;
+ void *mem_cur;
+
+ if (io_args(fd, mem, nrw, off, rw_type) == -1)
+ return -1;
+
+ while (1) {
+
+ /* Prevent theoretical overflow */
+ if (rv >= 0 && (size_t)rv > (nrw - rc))
+ goto err_rw_file_exact;
+
+ rc += rv;
+ if ((size_t)rc >= nrw)
+ break;
+
+ mem_cur = (void *)(mem + (size_t)rc);
+ nrw_cur = (size_t)(nrw - (size_t)rc);
+ if (off < 0)
+ goto err_rw_file_exact;
+ off_cur = (off_t)((size_t)off + (size_t)rc);
+
+ rv = prw(fd, mem_cur, nrw_cur, off_cur,
+ rw_type, loop_eagain, loop_eintr,
+ off_reset);
+
+ if (rv < 0)
+ return -1;
+
+ if (rv == 0) {
+ if (retries_on_zero++ < max_retries)
+ continue;
+ goto err_rw_file_exact;
+ }
+
+ retries_on_zero = 0;
+ }
+
+ if ((size_t)rc != nrw)
+ goto err_rw_file_exact;
+
+ return rw_over_nrw(rc, nrw);
+
+err_rw_file_exact:
+ errno = EIO;
+ return -1;
}
-static void
-usage(void)
+/*
+ * prw() - portable read-write
+ *
+ * This implements a portable analog of pwrite()
+ * and pread() - note that this version is not
+ * thread-safe (race conditions are possible on
+ * shared file descriptors).
+ *
+ * This limitation is acceptable, since nvmutil is
+ * single-threaded. Portability is the main goal.
+ *
+ * If you need real pwrite/pread, just compile
+ * with flag: HAVE_REAL_PREAD_PWRITE=1
+ *
+ * A fallback is provided for regular read/write.
+ * rw_type can be IO_READ, IO_WRITE, IO_PREAD
+ * or IO_PWRITE
+ *
+ * loop_eagain does a retry loop on EAGAIN if set
+ * loop_eintr does a retry loop on EINTR if set
+ *
+ * Unlike the bare syscalls, prw() does security
+ * checks e.g. checks NULL strings, checks bounds,
+ * also mitigates a few theoretical libc bugs.
+ * It is designed for extremely safe single-threaded
+ * I/O on applications that need it.
+ *
+ * NOTE: If you use loop_eagain (1), you enable wait
+ * loop on EAGAIN. Beware if using this on a non-blocking
+ * pipe (it could spin indefinitely).
+ *
+ * off_reset: if zero, and using fallback pwrite/pread
+ * analogs, we check if a file offset changed,
+ * which would indicate another thread changed
+ * it, and return error, without resetting the
+ * file - this would allow that thread to keep
+ * running, but we could then cause a whole
+ * program exit if we wanted to.
+ * if not zero:
+ * we reset and continue, and pray for the worst.
+ */
+
+static ssize_t
+prw(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type,
+ int loop_eagain, int loop_eintr,
+ int off_reset)
{
- const char *util = getnvmprogname();
+ ssize_t r;
+ int positional_rw;
+ struct stat st;
+#if !defined(HAVE_REAL_PREAD_PWRITE) || \
+ HAVE_REAL_PREAD_PWRITE < 1
+ int saved_errno;
+ off_t verified;
+ off_t off_orig;
+ off_t off_last;
+#endif
-#ifdef __OpenBSD__
- if (pledge("stdio", NULL) == -1)
- err(ECANCELED, "pledge");
+ if (io_args(fd, mem, nrw, off, rw_type) == -1)
+ return -1;
+
+ r = -1;
+
+ /* Programs like cat can use this,
+ so we only check if it's a normal
+ file if not looping EAGAIN */
+ if (!loop_eagain) {
+ /*
+ * Checking on every run of prw()
+ * is expensive if called many
+ * times, but is defensive in
+ * case the status changes.
+ */
+ if (check_file(fd, &st) == -1)
+ return -1;
+ }
+
+ if (rw_type >= IO_PREAD)
+ positional_rw = 1; /* pread/pwrite */
+ else
+ positional_rw = 0; /* read/write */
+
+try_rw_again:
+
+ if (!positional_rw) {
+#if defined(HAVE_REAL_PREAD_PWRITE) && \
+ HAVE_REAL_PREAD_PWRITE > 0
+real_pread_pwrite:
#endif
- fprintf(stderr,
- "Modify Intel GbE NVM images e.g. set MAC\n"
- "USAGE:\n"
- "\t%s FILE dump\n"
- "\t%s FILE # same as setmac without [MAC]\n"
- "\t%s FILE setmac [MAC]\n"
- "\t%s FILE swap\n"
- "\t%s FILE copy 0|1\n"
- "\t%s FILE brick 0|1\n"
- "\t%s FILE setchecksum 0|1\n",
- util, util, util, util, util, util, util);
+ if (rw_type == IO_WRITE)
+ r = write(fd, mem, nrw);
+ else if (rw_type == IO_READ)
+ r = read(fd, mem, nrw);
+#if defined(HAVE_REAL_PREAD_PWRITE) && \
+ HAVE_REAL_PREAD_PWRITE > 0
+ else if (rw_type == IO_PWRITE)
+ r = pwrite(fd, mem, nrw, off);
+ else if (rw_type == IO_PREAD)
+ r = pread(fd, mem, nrw, off);
+#endif
+
+ if (r == -1 && (errno == try_err(loop_eintr, EINTR)
+ || errno == try_err(loop_eagain, EAGAIN)))
+ goto try_rw_again;
+
+ return rw_over_nrw(r, nrw);
+ }
- err(ECANCELED, "Too few arguments");
+#if defined(HAVE_REAL_PREAD_PWRITE) && \
+ HAVE_REAL_PREAD_PWRITE > 0
+ goto real_pread_pwrite;
+#else
+ if ((off_orig = lseek_loop(fd, (off_t)0, SEEK_CUR,
+ loop_eagain, loop_eintr)) == (off_t)-1) {
+ r = -1;
+ } else if (lseek_loop(fd, off, SEEK_SET,
+ loop_eagain, loop_eintr) == (off_t)-1) {
+ r = -1;
+ } else {
+ verified = lseek_loop(fd, (off_t)0, SEEK_CUR,
+ loop_eagain, loop_eintr);
+
+ /*
+ * Partial thread-safety: detect
+ * if the offset changed to what
+ * we previously got. If it did,
+ * then another thread may have
+ * changed it. Enabled if
+ * off_reset is OFF_RESET.
+ *
+ * We do this *once*, on the theory
+ * that nothing is touching it now.
+ */
+ if (off_reset && off != verified)
+ lseek_loop(fd, off, SEEK_SET,
+ loop_eagain, loop_eintr);
+
+ do {
+ /*
+ * Verify again before I/O
+ * (even with OFF_ERR)
+ *
+ * This implements the first check
+ * even with OFF_ERR, but without
+ * the recovery. On ERR_RESET, if
+ * the check fails again, then we
+ * know something else is touching
+ * the file, so it's best that we
+ * probably leave it alone and err.
+ *
+ * In other words, ERR_RESET only
+ * tolerates one change. Any more
+ * will cause an exit, including
+ * per EINTR/EAGAIN re-spin.
+ */
+ verified = lseek_loop(fd, (off_t)0, SEEK_CUR,
+ loop_eagain, loop_eintr);
+
+ if (off != verified)
+ goto err_prw;
+
+ if (rw_type == IO_PREAD)
+ r = read(fd, mem, nrw);
+ else if (rw_type == IO_PWRITE)
+ r = write(fd, mem, nrw);
+
+ if (rw_over_nrw(r, nrw) == -1) {
+ errno = EIO;
+ break;
+ }
+
+ } while (r == -1 &&
+ (errno == try_err(loop_eintr, EINTR)
+ || errno == try_err(loop_eagain, EAGAIN)));
+ }
+
+ saved_errno = errno;
+
+ off_last = lseek_loop(fd, off_orig, SEEK_SET,
+ loop_eagain, loop_eintr);
+
+ if (off_last != off_orig) {
+ errno = saved_errno;
+ goto err_prw;
+ }
+
+ errno = saved_errno;
+
+ return rw_over_nrw(r, nrw);
+#endif
+
+err_prw:
+ errno = EIO;
+ return -1;
+}
+
+static int
+io_args(int fd, void *mem, size_t nrw,
+ off_t off, int rw_type)
+{
+ /* obviously */
+ if (mem == NULL)
+ goto err_io_args;
+
+ /* uninitialised fd */
+ if (fd < 0)
+ goto err_io_args;
+
+ /* negative offset */
+ if (off < 0)
+ goto err_io_args;
+
+ /* prevent zero-byte rw */
+ if (!nrw)
+ goto err_io_args;
+
+ /* prevent overflow */
+ if (nrw > (size_t)SSIZE_MAX)
+ goto err_io_args;
+
+ /* prevent overflow */
+ if (((size_t)off + nrw) < (size_t)off)
+ goto err_io_args;
+
+ if (rw_type > IO_PWRITE)
+ goto err_io_args;
+
+ return 0;
+
+err_io_args:
+ errno = EIO;
+ return -1;
+}
+
+static int
+check_file(int fd, struct stat *st)
+{
+ if (fstat(fd, st) == -1)
+ goto err_is_file;
+
+ if (!S_ISREG(st->st_mode))
+ goto err_is_file;
+
+ return 0;
+
+err_is_file:
+ errno = EIO;
+ return -1;
+}
+
+/*
+ * Check overflows caused by buggy libc.
+ *
+ * POSIX can say whatever it wants.
+ * specification != implementation
+ */
+static ssize_t
+rw_over_nrw(ssize_t r, size_t nrw)
+{
+ /*
+ * If a byte length of zero
+ * was requested, that is
+ * clearly a bug. No way.
+ */
+ if (!nrw)
+ goto err_rw_over_nrw;
+
+ if (r == -1)
+ return r;
+
+ if ((size_t)r > SSIZE_MAX) {
+ /*
+ * Theoretical buggy libc
+ * check. Extremely academic.
+ *
+ * Specifications never
+ * allow this return value
+ * to exceed SSIZE_MAX, but
+ * spec != implementation
+ *
+ * Check this after using
+ * [p]read() or [p]write()
+ */
+ goto err_rw_over_nrw;
+ }
+
+ /*
+ * Theoretical buggy libc:
+ * Should never return a number of
+ * bytes above the requested length.
+ */
+ if ((size_t)r > nrw)
+ goto err_rw_over_nrw;
+
+ return r;
+
+err_rw_over_nrw:
+
+ errno = EIO;
+ return -1;
+}
+
+#if !defined(HAVE_REAL_PREAD_PWRITE) || \
+ HAVE_REAL_PREAD_PWRITE < 1
+/*
+ * lseek_loop() does lseek() but optionally
+ * on an EINTR/EAGAIN wait loop. Used by prw()
+ * for setting offsets for positional I/O.
+ */
+static off_t
+lseek_loop(int fd, off_t off, int whence,
+ int loop_eagain, int loop_eintr)
+{
+ off_t old = -1;
+
+ do {
+ old = lseek(fd, off, whence);
+ } while (old == (off_t)-1 && (
+ errno == try_err(loop_eintr, EINTR) ||
+ errno == try_err(loop_eagain, EAGAIN)));
+
+ return old;
+}
+#endif
+
+/*
+ * If a given error loop is enabled,
+ * e.g. EINTR or EAGAIN, an I/O operation
+ * will loop until errno isn't -1 and one
+ * of these, e.g. -1 and EINTR
+ */
+static int
+try_err(int loop_err, int errval)
+{
+ if (loop_err)
+ return errval;
+
+ /* errno is never negative,
+ so functions checking it
+ can use it accordingly */
+ return -1;
}
static void
@@ -964,19 +2296,44 @@ err(int nvm_errval, const char *msg, ...)
{
va_list args;
+ if (errno == 0)
+ errno = nvm_errval;
+
+ (void)close_files();
+
fprintf(stderr, "%s: ", getnvmprogname());
va_start(args, msg);
vfprintf(stderr, msg, args);
va_end(args);
- set_err(nvm_errval);
fprintf(stderr, ": %s", strerror(errno));
fprintf(stderr, "\n");
exit(EXIT_FAILURE);
}
+static int
+close_files(void)
+{
+ int close_err_gbe = 0;
+ int saved_errno = errno;
+
+ if (gbe_fd > -1) {
+ if (close(gbe_fd) == -1)
+ close_err_gbe = errno;
+ gbe_fd = -1;
+ }
+
+ if (saved_errno)
+ errno = saved_errno;
+
+ if (close_err_gbe)
+ return -1;
+
+ return 0;
+}
+
static const char *
getnvmprogname(void)
{
@@ -994,12 +2351,27 @@ getnvmprogname(void)
}
static void
-set_err(int x)
+usage(int usage_exit)
{
- if (errno)
- return;
- if (x)
- errno = x;
- else
- errno = ECANCELED;
+ const char *util = getnvmprogname();
+
+#ifdef NVMUTIL_PLEDGE
+ if (pledge("stdio", NULL) == -1)
+ err(errno, "pledge");
+#endif
+ fprintf(stderr,
+ "Modify Intel GbE NVM images e.g. set MAC\n"
+ "USAGE:\n"
+ "\t%s FILE dump\n"
+ "\t%s FILE setmac [MAC]\n"
+ "\t%s FILE swap\n"
+ "\t%s FILE copy 0|1\n"
+ "\t%s FILE cat\n"
+ "\t%s FILE cat16\n"
+ "\t%s FILE cat128\n",
+ util, util, util, util,
+ util, util, util);
+
+ if (usage_exit)
+ err(EINVAL, "Too few arguments");
}
diff --git a/util/spkmodem_decode/.gitignore b/util/spkmodem_decode/.gitignore
new file mode 100644
index 00000000..42814fe4
--- /dev/null
+++ b/util/spkmodem_decode/.gitignore
@@ -0,0 +1,2 @@
+spkmodem-recv
+spkmodem-decode
diff --git a/util/spkmodem_decode/Makefile b/util/spkmodem_decode/Makefile
new file mode 100644
index 00000000..b00c4f43
--- /dev/null
+++ b/util/spkmodem_decode/Makefile
@@ -0,0 +1,30 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: 2022,2026 Leah Rowe <leah@libreboot.org>
+# SPDX-FileCopyrightText: 2023 Riku Viitanen <riku.viitanen@protonmail.com>
+
+CC?=cc
+CFLAGS?=-Os -Wall -Wextra -Werror -pedantic -std=c90
+DESTDIR?=
+PREFIX?=/usr/local
+INSTALL?=install
+
+PROG=spkmodem-decode
+
+all: $(PROG)
+
+$(PROG): spkmodem-decode.c
+ $(CC) $(CFLAGS) spkmodem-decode.c -o $(PROG)
+
+install: $(PROG)
+ mkdir -p $(DESTDIR)$(PREFIX)/bin/
+ install -c $(PROG) $(DESTDIR)$(PREFIX)/bin/
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/$(PROG)
+
+clean:
+ rm -f $(PROG)
+
+distclean: clean
+
+.PHONY: all install uninstall clean distclean
diff --git a/util/spkmodem_decode/spkmodem-decode.c b/util/spkmodem_decode/spkmodem-decode.c
new file mode 100644
index 00000000..3b3b33f8
--- /dev/null
+++ b/util/spkmodem_decode/spkmodem-decode.c
@@ -0,0 +1,725 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * Copyright (c) 2013 Free Software Foundation, Inc.
+ * Copyright (c) 2023, 2026 Leah Rowe <leah@libreboot.org>
+ *
+ * This program receives text encoded as pulses on the PC speaker,
+ * and decodes them via simple FSK (Frequency Shift Keying)
+ * demodulation and FIR (Finite Impulse Response) frequency
+ * discriminator.
+ *
+ * It waits for specific tones at specific intervals.
+ * It detects tones within the audio stream and reconstructs
+ * characters bit-by-bit as the encoded modem signal is received.
+ * This is performance-efficient on most CPUs, and has relatively
+ * high tolerance for noisy signals (similar to techniques used
+ * for data stored on audio cassette tapes).
+ *
+ * This is a special interface provided by coreboot and GNU GRUB,
+ * for computers that lack serial ports (useful for debugging).
+ * Note that GRUB and coreboot can both send these signals; this
+ * tool merely decodes them. This tool does not *encode*, only
+ * decode.
+ *
+ * Usage example (NOTE: little endian!):
+ * parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-decode
+ *
+ * Originally provided by GNU GRUB, this version is a heavily
+ * modified fork that complies with the OpenBSD Kernel Source
+ * File Style Guide (KNF) instead of GNU coding standards; it
+ * emphasises strict error handling, portability and code
+ * quality, as characterised by OpenBSD projects. Several magic
+ * numbers have been tidied up, calculated (not hardcoded) and
+ * thoroughly explained, unlike in the original version.
+ *
+ * The original version was essentially a blob, masquerading as
+ * source code. This forked source code is therefore the result
+ * of extensive reverse engineering (of the GNU source code)!
+ * This cleaned up code and extensive commenting will thoroughly
+ * explain how the decoding works. This was done as an academic
+ * exercise in 2023, continuing in 2026.
+ *
+ * This fork of spkmodem-recv, called spkmodem-decode, is provided
+ * with Libreboot releases:
+ * https://libreboot.org/
+ *
+ * The original GNU version is here, if you're morbidly curious:
+ * https://cgit.git.savannah.gnu.org/cgit/grub.git/plain/util/spkmodem-recv.c?id=3dce38eb196f47bdf86ab028de74be40e13f19fd
+ *
+ * Libreboot's version was renamed to spkmodem-decode on 12 March 2026,
+ * since Libreboot's version no longer closely resembles the GNU
+ * version at all; ergo, a full rename was in order. GNU's version
+ * was called spkmodem-recv.
+ */
+
+#define _POSIX_SOURCE
+
+/*
+ * For OpenBSD define, to detect version
+ * for deciding whether to use pledge(2)
+ */
+#ifdef __OpenBSD__
+#include <sys/param.h>
+#endif
+
+#include <errno.h>
+#include <limits.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+/*
+ * spkmodem is essentially using FSK (Frequency Shift Keying)
+ * with two primary tones representing encoded bits,
+ * separated by a framing tone.
+ * Very cheap on CPU cycles and avoids needing something more
+ * expensive like FFT or Goertzel filters, and tolerates
+ * weak/noisy signals.
+ */
+
+/*
+ * Frequency of audio in Hz
+ * WARNING: if changing, make sure to adjust
+ * SAMPLES_PER_FRAME accordingly (see maths below)
+ */
+#define SAMPLE_RATE 48000
+
+/*
+ * One analysis frame spans 5 ms.
+ *
+ * frame_time = SAMPLES_PER_FRAME / SAMPLE_RATE
+ *
+ * With the default sample rate (48 kHz):
+ *
+ * frame_time = N / 48000
+ * 0.005 s = N / 48000
+ * N = 0.005 × 48000 = 240 samples
+ */
+#define SAMPLES_PER_FRAME 240
+
+/*
+ * Number of analysis frames per second.
+ *
+ * Each increment in the frequency counters corresponds
+ * roughly to this many Hertz of tone frequency.
+ *
+ * With the default values:
+ * FRAME_RATE = 48000 / 240 = 200 Hz
+ */
+#define FRAME_RATE ((SAMPLE_RATE) / (SAMPLES_PER_FRAME))
+
+/*
+ * Two FIR windows are maintained; one for data tone,
+ * and one for the separator tone. They are positioned
+ * one frame apart in the ring buffer.
+ */
+#define MAX_SAMPLES (2 * (SAMPLES_PER_FRAME))
+
+/*
+ * Approx byte offset for ring buffer span, just for
+ * easier debug output correlating to the audio stream.
+ */
+#define SAMPLE_OFFSET ((MAX_SAMPLES) * (sizeof(short)))
+
+/*
+ * Expected tone ranges (approximate, derived from spkmodem).
+ * These values are intentionally wide because real-world setups
+ * often involve microphones, room acoustics, and cheap ADCs.
+ */
+#define SEP_TONE_MIN_HZ 1000
+#define SEP_TONE_MAX_HZ 3000
+
+#define SEP_TOLERANCE_PULSES \
+ (((SEP_TONE_MAX_HZ) - (SEP_TONE_MIN_HZ)) / (2 * (FRAME_RATE)))
+
+#define DATA_TONE_MIN_HZ 3000
+#define DATA_TONE_MAX_HZ 12000
+
+/* Mid point used to distinguish the two data tones. */
+#define DATA_TONE_THRESHOLD_HZ 5000
+
+/*
+ * Convert tone frequency ranges into pulse counts within the
+ * sliding analysis window.
+ *
+ * pulse_count = tone_frequency / FRAME_RATE
+ * where FRAME_RATE = SAMPLE_RATE / SAMPLES_PER_FRAME.
+ */
+#define FREQ_SEP_MIN ((SEP_TONE_MIN_HZ) / (FRAME_RATE))
+#define FREQ_SEP_MAX ((SEP_TONE_MAX_HZ) / (FRAME_RATE))
+
+#define FREQ_DATA_MIN ((DATA_TONE_MIN_HZ) / (FRAME_RATE))
+#define FREQ_DATA_MAX ((DATA_TONE_MAX_HZ) / (FRAME_RATE))
+
+#define FREQ_DATA_THRESHOLD ((DATA_TONE_THRESHOLD_HZ) / (FRAME_RATE))
+
+/*
+ * These determine how long the program will wait during
+ * tone auto-detection, before shifting to defaults.
+ * It is done every LEARN_FRAMES number of frames.
+ */
+#define LEARN_SECONDS 1
+#define LEARN_FRAMES ((LEARN_SECONDS) * (FRAME_RATE))
+
+/*
+ * Sample amplitude threshold used to convert the waveform
+ * into a pulse stream. Values near zero are regarded as noise.
+ */
+#define THRESHOLD 500
+
+#define READ_BUF 4096
+
+struct decoder_state {
+ unsigned char pulse[MAX_SAMPLES];
+
+ signed short inbuf[READ_BUF];
+ size_t inpos;
+ size_t inlen;
+
+ int ringpos;
+ int sep_pos;
+
+ /*
+ * Sliding window pulse counters
+ * used to detect modem tones
+ */
+ int freq_data;
+ int freq_separator;
+ int sample_count;
+
+ int ascii_bit;
+ unsigned char ascii;
+
+ int debug;
+ int swap_bytes;
+
+ /* dynamic separator calibration */
+ int sep_sum;
+ int sep_samples;
+ int sep_min;
+ int sep_max;
+
+ /* for automatic tone detection */
+ int freq_min;
+ int freq_max;
+ int freq_threshold;
+ int learn_frames;
+
+ /* previous sample used for edge detection */
+ signed short prev_sample;
+};
+
+static const char *argv0;
+
+/*
+ * 16-bit little endian words are read
+ * continuously. we will swap them, if
+ * the host cpu is big endian.
+ */
+static int host_is_big_endian(void);
+
+/* main loop */
+static void handle_audio(struct decoder_state *st);
+
+/* separate tone tolerances */
+static void select_separator_tone(struct decoder_state *st);
+static int is_valid_signal(struct decoder_state *st);
+
+/* output to terminal */
+static int set_ascii_bit(struct decoder_state *st);
+static void print_char(struct decoder_state *st);
+static void reset_char(struct decoder_state *st);
+
+/* process samples/frames */
+static void decode_pulse(struct decoder_state *st);
+static signed short read_sample(struct decoder_state *st);
+static void read_words(struct decoder_state *st);
+
+/* continually adjust tone */
+static void detect_tone(struct decoder_state *st);
+static int silent_signal(struct decoder_state *st);
+static void select_low_tone(struct decoder_state *st);
+
+/* debug */
+static void print_stats(struct decoder_state *st);
+
+/* error handling / usage */
+static void err(int errval, const char *msg, ...);
+static void usage(void);
+static const char *progname(void);
+
+/* portability (old systems) */
+int getopt(int, char * const *, const char *);
+extern char *optarg;
+extern int optind;
+extern int opterr;
+extern int optopt;
+
+#ifndef CHAR_BIT
+#define CHAR_BIT 8
+#endif
+
+typedef char static_assert_char_is_8_bits[(CHAR_BIT == 8) ? 1 : -1];
+typedef char static_assert_char_is_1[(sizeof(char) == 1) ? 1 : -1];
+typedef char static_assert_short[(sizeof(short) == 2) ? 1 : -1];
+typedef char static_assert_int_is_4[(sizeof(int) >= 4) ? 1 : -1];
+typedef char static_assert_twos_complement[
+ ((-1 & 3) == 3) ? 1 : -1
+];
+
+int
+main(int argc, char **argv)
+{
+ struct decoder_state st;
+ int c;
+
+ argv0 = argv[0];
+
+#if defined (__OpenBSD__) && defined(OpenBSD)
+#if OpenBSD >= 509
+ if (pledge("stdio", NULL) == -1)
+ err(errno, "pledge");
+#endif
+#endif
+
+ memset(&st, 0, sizeof(st));
+
+ while ((c = getopt(argc, argv, "d")) != -1) {
+ if (c != 'd')
+ usage();
+ st.debug = 1;
+ break;
+ }
+
+ /* fallback in case tone detection fails */
+ st.freq_min = 100000;
+ st.freq_max = 0;
+ st.freq_threshold = FREQ_DATA_THRESHOLD;
+
+ /*
+ * Used for separator calibration
+ */
+ st.sep_min = FREQ_SEP_MIN;
+ st.sep_max = FREQ_SEP_MAX;
+
+ st.ascii_bit = 7;
+
+ st.ringpos = 0;
+ st.sep_pos = SAMPLES_PER_FRAME;
+
+ if (host_is_big_endian())
+ st.swap_bytes = 1;
+
+ setvbuf(stdout, NULL, _IONBF, 0);
+
+ for (;;)
+ handle_audio(&st);
+
+ return EXIT_SUCCESS;
+}
+
+static int
+host_is_big_endian(void)
+{
+ unsigned int x = 1;
+ return (*(unsigned char *)&x == 0);
+}
+
+static void
+handle_audio(struct decoder_state *st)
+{
+ int sample;
+
+ /*
+ * If the modem signal disappears for several (read: 3)
+ * frames, discard the partially assembled character.
+ */
+ if (st->sample_count >= (3 * SAMPLES_PER_FRAME) ||
+ st->freq_separator <= 0)
+ reset_char(st);
+
+ st->sample_count = 0;
+
+ /* process exactly one frame */
+ for (sample = 0; sample < SAMPLES_PER_FRAME; sample++)
+ decode_pulse(st);
+
+ select_separator_tone(st);
+
+ if (set_ascii_bit(st) < 0)
+ print_char(st);
+
+ /* Detect tone per each frame */
+ detect_tone(st);
+}
+
+/*
+ * collect separator tone statistics
+ * (and auto-adjust tolerances)
+ */
+static void
+select_separator_tone(struct decoder_state *st)
+{
+ int avg;
+
+ if (!is_valid_signal(st))
+ return;
+
+ st->sep_sum += st->freq_separator;
+ st->sep_samples++;
+
+ if (st->sep_samples != 50)
+ return;
+
+ avg = st->sep_sum / st->sep_samples;
+
+ st->sep_min = avg - SEP_TOLERANCE_PULSES;
+ st->sep_max = avg + SEP_TOLERANCE_PULSES;
+
+ /* reset calibration accumulators */
+ st->sep_sum = 0;
+ st->sep_samples = 0;
+
+ if (st->debug)
+ printf("separator calibrated: %dHz\n",
+ avg * FRAME_RATE);
+}
+
+/*
+ * Verify that the observed pulse densities fall within the
+ * expected ranges for spkmodem tones. This prevents random noise
+ * from being misinterpreted as data.
+ */
+static int
+is_valid_signal(struct decoder_state *st)
+{
+ if (st->freq_data <= 0)
+ return 0;
+
+ if (st->freq_separator < st->sep_min ||
+ st->freq_separator > st->sep_max)
+ return 0;
+
+ return 1;
+}
+
+/*
+ * Each validated frame contributes one bit of modem data.
+ * Bits are accumulated MSB-first into the ASCII byte.
+ */
+static int
+set_ascii_bit(struct decoder_state *st)
+{
+ if (st->debug)
+ print_stats(st);
+
+ if (!is_valid_signal(st))
+ return st->ascii_bit;
+
+ if (st->freq_data < st->freq_threshold)
+ st->ascii |= (1 << st->ascii_bit);
+
+ st->ascii_bit--;
+
+ return st->ascii_bit;
+}
+
+static void
+print_char(struct decoder_state *st)
+{
+ if (st->debug)
+ printf("<%c,%x>", st->ascii, st->ascii);
+ else
+ putchar(st->ascii);
+
+ reset_char(st);
+}
+
+static void
+reset_char(struct decoder_state *st)
+{
+ st->ascii = 0;
+ st->ascii_bit = 7;
+}
+
+/*
+ * Main demodulation step (moving-sum FIR filter).
+ */
+static void
+decode_pulse(struct decoder_state *st)
+{
+ unsigned char old_ring, old_sep;
+ unsigned char new_pulse;
+ signed short sample;
+ int ringpos;
+ int sep_pos;
+ int diff_edge;
+ int diff_amp;
+
+ ringpos = st->ringpos;
+ sep_pos = st->sep_pos;
+
+ /*
+ * Sliding rectangular FIR (Finite Impulse Response) filter.
+ *
+ * After thresholding, the signal becomes a stream of 0/1 pulses.
+ * The decoder measures pulse density over two windows:
+ *
+ * freq_data: pulses in the "data" window
+ * freq_separator: pulses in the "separator" window
+ *
+ * Instead of calculating each window every time (O(N) per frame), we
+ * update the window sums incrementally:
+ *
+ * sum_new = sum_old - pulse_leaving + pulse_entering
+ *
+ * This keeps the filter O(1) per sample instead of O(N).
+ * The technique is equivalent to a rectangular FIR filter
+ * implemented as a sliding moving sum.
+ *
+ * The two windows are exactly SAMPLES_PER_FRAME apart in the ring
+ * buffer, so the pulse leaving the data window is simultaneously
+ * entering the separator window.
+ */
+ old_ring = st->pulse[ringpos];
+ old_sep = st->pulse[sep_pos];
+ st->freq_data -= old_ring;
+ st->freq_data += old_sep;
+ st->freq_separator -= old_sep;
+
+ sample = read_sample(st);
+
+ /*
+ * Avoid startup edge. Since
+ * it's zero at startup, this
+ * may wrongly produce a pulse
+ */
+ if (st->sample_count == 0)
+ st->prev_sample = sample;
+
+ /*
+ * Detect edges instead of amplitude.
+ * This is more tolerant of weak microphones
+ * and speaker distortion..
+ *
+ * However, we check both slope edges and
+ * amplitude, to mitagate noise.
+ */
+ diff_amp = sample;
+ diff_edge = sample - st->prev_sample;
+ if (diff_edge < 0)
+ diff_edge = -diff_edge;
+ if (diff_amp < 0)
+ diff_amp = -diff_amp;
+ if (diff_edge > THRESHOLD &&
+ diff_amp > THRESHOLD)
+ new_pulse = 1;
+ else
+ new_pulse = 0;
+ st->prev_sample = sample;
+
+ st->pulse[ringpos] = new_pulse;
+ st->freq_separator += new_pulse;
+
+ /*
+ * Advance both FIR windows through the ring buffer.
+ * The separator window always stays one frame ahead
+ * of the data window.
+ */
+ if (++ringpos >= MAX_SAMPLES)
+ ringpos = 0;
+ if (++sep_pos >= MAX_SAMPLES)
+ sep_pos = 0;
+
+ st->ringpos = ringpos;
+ st->sep_pos = sep_pos;
+
+ st->sample_count++;
+}
+
+static signed short
+read_sample(struct decoder_state *st)
+{
+ signed short sample;
+ unsigned short u;
+
+ while (st->inpos >= st->inlen)
+ read_words(st);
+
+ sample = st->inbuf[st->inpos++];
+
+ if (st->swap_bytes) {
+ u = (unsigned short)sample;
+ u = (u >> 8) | (u << 8);
+
+ sample = (signed short)u;
+ }
+
+ return sample;
+}
+
+static void
+read_words(struct decoder_state *st)
+{
+ size_t n;
+
+ n = fread(st->inbuf, sizeof(st->inbuf[0]),
+ READ_BUF, stdin);
+
+ if (n != 0) {
+ st->inpos = 0;
+ st->inlen = n;
+
+ return;
+ }
+
+ if (ferror(stdin))
+ err(errno, "stdin read");
+ if (feof(stdin))
+ exit(EXIT_SUCCESS);
+}
+
+/*
+ * Automatically detect spkmodem tone
+ */
+static void
+detect_tone(struct decoder_state *st)
+{
+ if (st->learn_frames >= LEARN_FRAMES)
+ return;
+
+ st->learn_frames++;
+
+ if (silent_signal(st))
+ return;
+
+ select_low_tone(st);
+
+ if (st->learn_frames != LEARN_FRAMES)
+ return;
+
+ /*
+ * If the observed frequencies are too close,
+ * learning likely failed (only one tone seen).
+ * Keep the default threshold.
+ */
+ if (st->freq_max - st->freq_min < 2)
+ return;
+
+ st->freq_threshold =
+ (st->freq_min + st->freq_max) / 2;
+
+ if (st->debug)
+ printf("auto threshold: %dHz\n",
+ st->freq_threshold * FRAME_RATE);
+}
+
+/*
+ * Ignore silence / near silence.
+ * Both FIR windows will be near zero when no signal exists.
+ */
+static int
+silent_signal(struct decoder_state *st)
+{
+ return (st->freq_data <= 2 &&
+ st->freq_separator <= 2);
+}
+
+/*
+ * Choose the lowest active tone.
+ * Separator frames carry tone in the separator window,
+ * data frames carry tone in the data window.
+ */
+static void
+select_low_tone(struct decoder_state *st)
+{
+ int f;
+
+ f = st->freq_data;
+
+ if (f <= 0 || (st->freq_separator > 0 &&
+ st->freq_separator < f))
+ f = st->freq_separator;
+
+ if (f <= 0)
+ return;
+
+ if (f < st->freq_min)
+ st->freq_min = f;
+
+ if (f > st->freq_max)
+ st->freq_max = f;
+}
+
+static void
+print_stats(struct decoder_state *st)
+{
+ long pos;
+
+ int data_hz = st->freq_data * FRAME_RATE;
+ int sep_hz = st->freq_separator * FRAME_RATE;
+ int sep_hz_min = st->sep_min * FRAME_RATE;
+ int sep_hz_max = st->sep_max * FRAME_RATE;
+
+ if ((pos = ftell(stdin)) == -1) {
+ printf("%d %d %d data=%dHz sep=%dHz(min %dHz %dHz)\n",
+ st->freq_data,
+ st->freq_separator,
+ st->freq_threshold,
+ data_hz,
+ sep_hz,
+ sep_hz_min,
+ sep_hz_max);
+ return;
+ }
+
+ printf("%d %d %d @%ld data=%dHz sep=%dHz(min %dHz %dHz)\n",
+ st->freq_data,
+ st->freq_separator,
+ st->freq_threshold,
+ pos - SAMPLE_OFFSET,
+ data_hz,
+ sep_hz,
+ sep_hz_min,
+ sep_hz_max);
+}
+
+static void
+err(int errval, const char *msg, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "%s: ", progname());
+
+ va_start(ap, msg);
+ vfprintf(stderr, msg, ap);
+ va_end(ap);
+
+ fprintf(stderr, ": %s\n", strerror(errval));
+ exit(EXIT_FAILURE);
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: %s [-d]\n", progname());
+ exit(EXIT_FAILURE);
+}
+
+static const char *
+progname(void)
+{
+ const char *p;
+
+ if (argv0 == NULL || *argv0 == '\0')
+ return "";
+
+ p = strrchr(argv0, '/');
+
+ if (p)
+ return p + 1;
+ else
+ return argv0;
+}
diff --git a/util/spkmodem_recv/.gitignore b/util/spkmodem_recv/.gitignore
deleted file mode 100644
index 2f5c946c..00000000
--- a/util/spkmodem_recv/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-spkmodem-recv
diff --git a/util/spkmodem_recv/Makefile b/util/spkmodem_recv/Makefile
deleted file mode 100644
index 3c5dc51f..00000000
--- a/util/spkmodem_recv/Makefile
+++ /dev/null
@@ -1,14 +0,0 @@
-# SPDX-License-Identifier: GPL-2.0-or-later
-CC?=cc
-CFLAGS?=-Os -Wall -Wextra -Werror -pedantic
-DESTDIR?=
-PREFIX?=/usr/local
-INSTALL?=install
-
-spkmodem-recv:
- $(CC) $(CFLAGS) -o $@ $@.c
-install: spkmodem-recv
- $(INSTALL) -d $(DESTDIR)$(PREFIX)/bin/
- $(INSTALL) $< -t $(DESTDIR)$(PREFIX)/bin/
-clean:
- rm -f spkmodem-recv
diff --git a/util/spkmodem_recv/spkmodem-recv.c b/util/spkmodem_recv/spkmodem-recv.c
deleted file mode 100644
index 38ceb72a..00000000
--- a/util/spkmodem_recv/spkmodem-recv.c
+++ /dev/null
@@ -1,117 +0,0 @@
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-/* SPDX-FileCopyrightText: 2013 Free Software Foundation, Inc. */
-/* Usage: parec --channels=1 --rate=48000 --format=s16le | ./spkmodem-recv */
-
-/* Forked from coreboot's version, at util/spkmodem_recv/ in coreboot.git,
- * revision 5c2b5fcf2f9c9259938fd03cfa3ea06b36a007f0 as of 3 January 2022.
- * This version is heavily modified, re-written based on OpenBSD Kernel Source
- * File Style Guide (KNF); this change is Copyright 2023,2026 Leah Rowe. */
-
-#include <err.h>
-#include <errno.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <string.h>
-#include <unistd.h>
-
-#define SAMPLES_PER_FRAME 240
-#define MAX_SAMPLES (2 * SAMPLES_PER_FRAME)
-#define FREQ_SEP_MIN 5
-#define FREQ_SEP_MAX 15
-#define FREQ_DATA_MIN 15
-#define FREQ_DATA_THRESHOLD 25
-#define FREQ_DATA_MAX 60
-#define THRESHOLD 500
-
-#define ERR() (errno = errno ? errno : ECANCELED)
-#define reset_char() ascii = 0, ascii_bit = 7
-#define err_if(x) if (x) err(ERR(), NULL)
-
-signed short frame[MAX_SAMPLES], pulse[MAX_SAMPLES];
-int ringpos, debug, freq_data, freq_separator, sample_count, ascii_bit = 7;
-char ascii = 0;
-
-int set_ascii_bit(void);
-void handle_audio(void), decode_pulse(void), print_char(void),
- print_stats(void);
-
-int
-main(int argc, char *argv[])
-{
-#ifdef __OpenBSD__
- err_if (pledge("stdio", NULL) == -1);
-#endif
- for (int c; (c = getopt(argc, argv, "d")) != -1;)
- err_if (!(debug = (c == 'd')));
- setvbuf(stdout, NULL, _IONBF, 0);
- while (!feof(stdin))
- handle_audio();
- if (errno && debug)
- err(errno, "Unhandled error, errno %d", errno);
- return errno;
-}
-
-void
-handle_audio(void)
-{
- if (sample_count > (3 * SAMPLES_PER_FRAME))
- sample_count = reset_char();
- if ((freq_separator <= FREQ_SEP_MIN) || (freq_separator >= FREQ_SEP_MAX)
- || (freq_data <= FREQ_DATA_MIN) || (freq_data >= FREQ_DATA_MAX)) {
- decode_pulse();
- return;
- }
- if (set_ascii_bit() < 0)
- print_char();
- sample_count = 0;
- for (int sample = 0; sample < SAMPLES_PER_FRAME; sample++)
- decode_pulse();
-}
-
-void
-decode_pulse(void)
-{
- int next_ringpos = (ringpos + SAMPLES_PER_FRAME) % MAX_SAMPLES;
- freq_data -= pulse[ringpos];
- freq_data += pulse[next_ringpos];
- freq_separator -= pulse[next_ringpos];
-
- fread(frame + ringpos, 1, sizeof(frame[0]), stdin);
- if (ferror(stdin) != 0)
- err(ERR(), "Could not read from frame.");
-
- pulse[ringpos] = (abs(frame[ringpos]) > THRESHOLD) ? 1 : 0;
- freq_separator += pulse[ringpos++];
- ringpos %= MAX_SAMPLES;
- ++sample_count;
-}
-
-int
-set_ascii_bit(void)
-{
- if (debug)
- print_stats();
- if (freq_data < FREQ_DATA_THRESHOLD)
- ascii |= (1 << ascii_bit);
- --ascii_bit;
- return ascii_bit;
-}
-
-void
-print_stats(void)
-{
- long stdin_pos;
- err_if ((stdin_pos = ftell(stdin)) == -1);
- printf ("%d %d %d @%ld\n", freq_data, freq_separator,
- FREQ_DATA_THRESHOLD, stdin_pos - sizeof(frame));
-}
-
-void
-print_char(void)
-{
- if (debug)
- printf("<%c, %x>", ascii, ascii);
- else
- printf("%c", ascii);
- reset_char();
-}