diff --git a/src/Makefile.am b/src/Makefile.am index 17c212b..803cd21 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,6 +1,7 @@ -AM_CFLAGS = -std=c99 -pedantic -W -Wall -Wextra -Wno-unused-parameter -Wshadow +AM_CFLAGS = -std=c99 -pedantic -W -Wall -Wextra -Wno-unused-parameter -Wshadow -Wundef +AM_CFLAGS += -Werror sbin_PROGRAMS = bootcount -bootcount_SOURCES = bootcount.c am33xx.c stm32mp1.c i2c_eeprom.c memory.c \ +bootcount_SOURCES = bootcount.c am33xx.c stm32mp1.c i2c_eeprom.c dm_eeprom.c memory.c \ dt.c imx8m.c imx93.c diff --git a/src/bootcount.c b/src/bootcount.c index 7023fe0..ded4bfc 100644 --- a/src/bootcount.c +++ b/src/bootcount.c @@ -34,6 +34,7 @@ #include "imx93.h" #include "stm32mp1.h" #include "i2c_eeprom.h" +#include "dm_eeprom.h" struct platform { const char *name; @@ -65,6 +66,11 @@ static const struct platform platforms[] = { .read_bootcount = stm32mp1_read_bootcount, .write_bootcount = stm32mp1_write_bootcount }, + {.name = DM_EEPROM_NAME, + .detect = dm_eeprom_exists, + .read_bootcount = dm_eeprom_read_bootcount, + .write_bootcount = dm_eeprom_write_bootcount + }, {.name = EEPROM_NAME, .detect = eeprom_exists, .read_bootcount = eeprom_read_bootcount, diff --git a/src/constants.h b/src/constants.h index b72b5e5..8aed52f 100644 --- a/src/constants.h +++ b/src/constants.h @@ -4,6 +4,10 @@ #include #include +#ifndef PATH_MAX +#define PATH_MAX 4096 +#endif + #define DEBUG false #define BOOTCOUNT_MAGIC 0xB001C041ul // from u-boot include/common.h diff --git a/src/dm_eeprom.c b/src/dm_eeprom.c new file mode 100644 index 0000000..73a800e --- /dev/null +++ b/src/dm_eeprom.c @@ -0,0 +1,209 @@ + +/** + * dm-eeprom bootcount implementation for linux userspace. + * Ref: https://github.com/u-boot/u-boot/blob/master/drivers/bootcount/bootcount_dm_i2c.c + * + * This file is part of the uboot-bootcount (https://github.com/VoltServer/uboot-bootcount). + * Copyright (c) 2018 VoltServer. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "dt.h" +#include "constants.h" + +#define DM_I2C_MAGIC 0xbc +#define I2C_SYSFS_DEVICES "/sys/bus/i2c/devices" + +/* + * Runtime discovery of the EEPROM used for bootcount via the flattened + * device tree exported at /proc/device-tree. + * + * The definition looks like this: + * + * chosen { + * // see: u-boot/drivers/bootcount/bootcount-uclass.c + * u-boot,bootcount-device = &bootcount_i2c_eeprom; + * }; + * bootcount_i2c_eeprom: bc_i2c_eeprom { + * // see: u-boot/drivers/bootcount/i2c-eeprom.c + * compatible = "u-boot,bootcount-i2c-eeprom"; + * i2c-eeprom = <&eeprom0>; + * offset = <0x30>; + * }; + * + * see: https://github.com/u-boot/u-boot/blob/master/drivers/bootcount/i2c-eeprom.c + * + * Read the phandle /sys/firmware/devicetree/base//i2c-eeprom + * Look for the device at /sys/bus/i2c/devices/-/eeprom + * + */ + +/* Global cached discovered path and offset */ +static bool g_inited = false; +static char g_eeprom_sysfs_path[PATH_MAX]; +static off_t g_offset = 0; + +static bool discover_dm_eeprom(void) +{ + if (g_inited) + return true; + DEBUG_PRINTF("Discovering DM I2C EEPROM bootcount device...\n"); + + char bc_node[PATH_MAX]; + if(!dt_get_chosen_bootcount_node("u-boot,bootcount-i2c-eeprom", bc_node, sizeof(bc_node))) { + return false; + } + DEBUG_PRINTF(" Found bootcount node %s\n", bc_node); + + /* Read offset (optional) */ + uint32_t offset = 0; + dt_node_read_u32(bc_node, "offset", &offset); /* ignore failure => 0 */ + g_offset = (off_t)offset; + DEBUG_PRINTF(" Using offset 0x%lx\n", (unsigned long)g_offset); + + /* Read i2c-eeprom phandle */ + uint32_t eeprom_phandle; + if (!dt_node_read_u32(bc_node, "i2c-eeprom", &eeprom_phandle)) + return false; + DEBUG_PRINTF(" Found i2c-eeprom phandle %u\n", eeprom_phandle); + + char eeprom_device_path[PATH_MAX]; + if (!dt_find_phandle_node(eeprom_phandle, eeprom_device_path, sizeof(eeprom_device_path))) + return false; + DEBUG_PRINTF(" Found eeprom node %s\n", eeprom_device_path); + struct stat target_st; + if (stat(eeprom_device_path, &target_st) != 0) { + DEBUG_PRINTF(" stat() failed on target node %s\n", eeprom_device_path); + return false; + } + /* Iterate /sys/bus/i2c/devices//of_node and compare the + * symlink target to the eeprom DT node path we resolved above + */ + bool matched = false; + DIR *dev_dir = opendir(I2C_SYSFS_DEVICES); + if (!dev_dir) + return false; + DEBUG_PRINTF(" Scanning " I2C_SYSFS_DEVICES " for matching device ...\n"); + // iterate all devices under /sys/bus/i2c/devices/ and find a matching of_node + struct dirent *de2; + while ((de2 = readdir(dev_dir))) { + if (de2->d_name[0] == '.') + continue; /* skip dot entries */ + char link_path[PATH_MAX]; + if (snprintf(link_path, sizeof(link_path), I2C_SYSFS_DEVICES "/%s/of_node", de2->d_name) >= (int)sizeof(link_path)) { + DEBUG_PRINTF(" ERROR Path truncated constructing of_node path\n"); + continue; + } + if (!same_fs_node(link_path, eeprom_device_path)) { + continue; + } + DEBUG_PRINTF(" Matched device %s\n", link_path); + snprintf(g_eeprom_sysfs_path, sizeof(g_eeprom_sysfs_path), I2C_SYSFS_DEVICES "/%s/eeprom", de2->d_name); + + /* verify the eeprom node exists: */ + struct stat sb; + if (stat(g_eeprom_sysfs_path, &sb) != 0) { + DEBUG_PRINTF(" WARN EEPROM sysfs path %s does not exist, continuing...\n", g_eeprom_sysfs_path); + continue; + } + matched = true; + DEBUG_PRINTF(" Chose EEPROM device %s\n", g_eeprom_sysfs_path); + break; + } + closedir(dev_dir); + if (!matched) + return false; + + /* Validate file exists */ + struct stat sb; + if (stat(g_eeprom_sysfs_path, &sb) != 0) + return false; + + g_inited = true; + return true; +} + +static int dm_eeprom_open(void) +{ + if (!discover_dm_eeprom()) + return E_DEVICE; + int fd = open(g_eeprom_sysfs_path, O_RDWR); + if (fd < 0) + return E_DEVICE; + if (lseek(fd, g_offset, SEEK_SET) == -1) { + close(fd); + return E_DEVICE; + } + return fd; +} + +bool dm_eeprom_exists(void) +{ + return discover_dm_eeprom(); +} + +int dm_eeprom_read_bootcount(uint16_t *val) +{ + int fd = dm_eeprom_open(); + if (fd < 0) + return fd; + + unsigned char bytes[2]; + if (read(fd, bytes, sizeof(bytes)) != (ssize_t)sizeof(bytes)) { + close(fd); + return E_DEVICE; + } + close(fd); + + if (bytes[1] != DM_I2C_MAGIC) { + /* Upstream DM driver resets counter to 0 on invalid magic. + We have a reset command, so do not write on a read operation + */ + return E_BADMAGIC; + } + *val = bytes[0]; + return 0; +} + +int dm_eeprom_write_bootcount(uint16_t val) +{ + int fd = dm_eeprom_open(); + if (fd < 0) + return fd; + unsigned char bytes[2]; + bytes[0] = (unsigned char)(val & 0xff); + bytes[1] = DM_I2C_MAGIC; + ssize_t written = write(fd, bytes, sizeof(bytes)); + if (written != (ssize_t)sizeof(bytes)) { + close(fd); + return E_DEVICE; + } + close(fd); + return 0; +} + + diff --git a/src/dm_eeprom.h b/src/dm_eeprom.h new file mode 100644 index 0000000..969722c --- /dev/null +++ b/src/dm_eeprom.h @@ -0,0 +1,7 @@ +#pragma once + +#define DM_EEPROM_NAME "DM I2C EEPROM" + +bool dm_eeprom_exists(void); +int dm_eeprom_read_bootcount(uint16_t *val); +int dm_eeprom_write_bootcount(uint16_t val); diff --git a/src/dt.c b/src/dt.c index 72eb026..8a794c5 100644 --- a/src/dt.c +++ b/src/dt.c @@ -21,6 +21,10 @@ #include #include #include +#include +#include +#include +#include #include "constants.h" #include "dt.h" @@ -70,3 +74,169 @@ bool is_compatible_soc(const char* compat_str) { } return false; } + +bool dt_root_available(void) +{ + struct stat sb; + return (stat(DT_ROOT, &sb) == 0 && S_ISDIR(sb.st_mode)); +} + +bool dt_read_u32(const char *path, uint32_t *val) +{ + FILE *f = fopen(path, "rb"); + if (!f) + return false; + + unsigned char b[4]; + size_t r = fread(b, 1, 4, f); + fclose(f); + if (r != 4) + return false; + + *val = (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; + return true; +} + +/* Recursive directory traversal to locate a node directory that owns a given phandle */ +static bool dt_scan_dir_for_phandle(const char *dir, uint32_t target, + char *out, size_t outlen, int depth) +{ + if (depth > 8) /* safety recursion limit */ + return false; + + DIR *d = opendir(dir); + if (!d) + return false; + + struct dirent *e; + bool found = false; + while (!found && (e = readdir(d))) { + if (e->d_name[0] == '.') + continue; /* skip dot entries */ + + char path[PATH_MAX]; + int plen = snprintf(path, sizeof(path), "%s/%s", dir, e->d_name); + if (plen < 0 || plen >= (int)sizeof(path)) + continue; /* truncated */ + + struct stat st; + if (stat(path, &st) != 0 || !S_ISDIR(st.st_mode)) + continue; /* not a DT node directory */ + + char ph_path[PATH_MAX]; + uint32_t val; + + /* Try primary 'phandle' */ + plen = snprintf(ph_path, sizeof(ph_path), "%s/phandle", path); + if (plen >= 0 && plen < (int)sizeof(ph_path)) { + if (dt_read_u32(ph_path, &val) && val == target) { + strncpy(out, path, outlen - 1); + out[outlen - 1] = 0; + found = true; + break; + } + } + + /* Try legacy 'linux,phandle' */ + plen = snprintf(ph_path, sizeof(ph_path), "%s/linux,phandle", path); + if (!found && plen >= 0 && plen < (int)sizeof(ph_path)) { + if (dt_read_u32(ph_path, &val) && val == target) { + strncpy(out, path, outlen - 1); + out[outlen - 1] = 0; + found = true; + break; + } + } + + /* Recurse if not matched here */ + if (!found && dt_scan_dir_for_phandle(path, target, out, outlen, depth + 1)) { + found = true; + break; + } + } + + closedir(d); + return found; +} + +bool dt_find_phandle_node(uint32_t phandle, char *out, size_t outlen) +{ + return dt_scan_dir_for_phandle(DT_ROOT, phandle, out, outlen, 0); +} + +bool dt_node_read_u32(const char *node_dir, const char *prop, uint32_t *val) +{ + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", node_dir, prop); + if (n < 0 || n >= (int)sizeof(path)) + return false; + return dt_read_u32(path, val); +} + +/* + * dt_node_read_str + * Returns: number of bytes read (excluding terminator) on success. + * -E_DEVICE (re-using existing error codes) on I/O failure or empty. + * NOTE: The string is always null-terminated on success (and on partial reads). + */ +int dt_node_read_str(const char *node_dir, const char *prop, char *out, size_t outlen) +{ + if (!out || outlen == 0) + return E_DEVICE; + + char path[PATH_MAX]; + int n = snprintf(path, sizeof(path), "%s/%s", node_dir, prop); + if (n < 0 || n >= (int)sizeof(path)) + return E_DEVICE; + + FILE *f = fopen(path, "rb"); + if (!f) + return E_DEVICE; + + size_t r = fread(out, 1, outlen - 1, f); + fclose(f); + if (r == 0) { + out[0] = 0; + return E_DEVICE; + } + out[r] = 0; + return (int)r; +} + +/* Compare two filesystem objects for identity (same underlying node). */ +bool same_fs_node(const char *a, const char *b) +{ + struct stat sa, sb; + if (stat(a, &sa) != 0) + return false; + if (stat(b, &sb) != 0) + return false; + return (sa.st_dev == sb.st_dev) && (sa.st_ino == sb.st_ino); +} + +bool dt_get_chosen_bootcount_node(const char *compat_str, char* bc_node, size_t bc_node_len) +{ + if (!dt_root_available()) + return false; + + char bc_path[128]; + if (!dt_node_read_str(DT_ROOT "/chosen", "u-boot,bootcount-device", bc_path, sizeof(bc_path))) { + DEBUG_PRINTF(" No chosen/u-boot,bootcount-device in device tree\n"); + return false; /* No chosen bootcount device */ + // TODO find the first bootcount node with compatible = compat_str and use it + } + if (snprintf(bc_node, bc_node_len, DT_ROOT "%s", bc_path) >= (int)bc_node_len) { + DEBUG_PRINTF(" ERROR Path truncated building device node path for %s\n", bc_path); + return false; + } + + /* if the bc_node/compatible does not match "u-boot,bootcount-rtc" + then this is not the correct driver: */ + char compatible[100]; + int bc_compat_len = dt_node_read_str(bc_node, "compatible", compatible, sizeof(compatible)); + if (bc_compat_len <= 0 || strstr(compatible, compat_str) == NULL) { + DEBUG_PRINTF(" Chosen bootcount node is not compatible: '%s'\n", compatible); + return false; + } + return true; +} diff --git a/src/dt.h b/src/dt.h index af29e85..ca6879e 100644 --- a/src/dt.h +++ b/src/dt.h @@ -21,5 +21,30 @@ #pragma once #include +#include +#include +/* Root of flattened DT in sysfs (preferred for runtime property access) */ +#define DT_ROOT "/sys/firmware/devicetree/base" + +/* /proc helper: check if SoC compatible string is present */ bool is_compatible_soc(const char* compat); + +/* Returns true if DT_ROOT exists */ +bool dt_root_available(void); + +/* Read big-endian u32 from a property file path. Returns true on success. */ +bool dt_read_u32(const char *path, uint32_t *val); + +/* Find node directory (full path) for a given phandle. Returns true on success. */ +bool dt_find_phandle_node(uint32_t phandle, char *out, size_t outlen); + +/* Read a u32 property (big-endian) from inside a node directory. Returns true on success. */ +bool dt_node_read_u32(const char *node_dir, const char *prop, uint32_t *val); + +int dt_node_read_str(const char *node_dir, const char *prop, char *out, size_t outlen); + +/* Compare two filesystem objects for identity (same underlying node). */ +bool same_fs_node(const char *a, const char *b); + +bool dt_get_chosen_bootcount_node(const char *compat_str, char* bc_node, size_t bc_node_len);