[RFC PATCH] tools: nvmem: add nvmemctl for userspace NVMEM device management

Kuan-Wei Chiu posted 1 patch 3 weeks, 1 day ago
MAINTAINERS            |   6 +
tools/Makefile         |  17 ++-
tools/nvmem/Makefile   |  22 +++
tools/nvmem/nvmemctl.c | 340 +++++++++++++++++++++++++++++++++++++++++
4 files changed, 382 insertions(+), 3 deletions(-)
create mode 100644 tools/nvmem/Makefile
create mode 100644 tools/nvmem/nvmemctl.c
[RFC PATCH] tools: nvmem: add nvmemctl for userspace NVMEM device management
Posted by Kuan-Wei Chiu 3 weeks, 1 day ago
Introduce nvmemctl, a userspace CLI tool designed to cleanly interact
with the NVMEM subsystem. It eliminates the need for manual sysfs
traversal and raw binary parsing by providing a standard interface
strictly based on the documented ABIs:
  - Documentation/ABI/stable/sysfs-bus-nvmem
  - Documentation/ABI/testing/sysfs-nvmem-cells

Supported operations:
  - list: Discovers and displays NVMEM devices, types, read-only
    status, sizes, and device tree cells in a tree topology.
  - dump: Safely hexdumps the entire binary content of a device.
  - read: Parses the cells directory and hexdumps a specific cell.
  - lock/unlock: Toggles the force_ro attribute to prevent or
    allow writes.

Signed-off-by: Kuan-Wei Chiu <visitorckw@gmail.com>
---
 MAINTAINERS            |   6 +
 tools/Makefile         |  17 ++-
 tools/nvmem/Makefile   |  22 +++
 tools/nvmem/nvmemctl.c | 340 +++++++++++++++++++++++++++++++++++++++++
 4 files changed, 382 insertions(+), 3 deletions(-)
 create mode 100644 tools/nvmem/Makefile
 create mode 100644 tools/nvmem/nvmemctl.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 61bf550fd37c..4bc40b6fe4ff 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -18998,6 +18998,12 @@ F:	include/dt-bindings/nvmem/
 F:	include/linux/nvmem-consumer.h
 F:	include/linux/nvmem-provider.h
 
+NVMEM TOOL
+M:	Kuan-Wei Chiu <visitorckw@gmail.com>
+L:	linux-kernel@vger.kernel.org
+S:	Maintained
+F:	tools/nvmem/
+
 NXP BLUETOOTH WIRELESS DRIVERS
 M:	Amitkumar Karwar <amitkumar.karwar@nxp.com>
 M:	Neeraj Kale <neeraj.sanjaykale@nxp.com>
diff --git a/tools/Makefile b/tools/Makefile
index cb40961a740f..59b2f245dd39 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -43,6 +43,7 @@ help:
 	@echo '  wmi			- WMI interface examples'
 	@echo '  x86_energy_perf_policy - Intel energy policy tool'
 	@echo '  ynl			- ynl headers, library, and python tool'
+	@echo '  nvmem                  - NVMEM tools'
 	@echo ''
 	@echo 'You can do:'
 	@echo ' $$ make -C tools/ <tool>_install'
@@ -123,11 +124,14 @@ kvm_stat: FORCE
 ynl: FORCE
 	$(call descend,net/ynl)
 
+nvmem: FORCE
+	$(call descend,nvmem)
+
 all: acpi counter cpupower dma gpio hv firewire \
 		perf selftests bootconfig spi turbostat usb \
 		virtio mm bpf x86_energy_perf_policy \
 		tmon freefall iio objtool kvm_stat wmi \
-		debugging tracing thermal thermometer thermal-engine ynl
+		debugging tracing thermal thermometer thermal-engine ynl nvmem
 
 acpi_install:
 	$(call descend,power/$(@:_install=),install)
@@ -165,13 +169,17 @@ kvm_stat_install:
 ynl_install:
 	$(call descend,net/$(@:_install=),install)
 
+nvmem_install:
+	$(call descend,nvmem,install)
+
 install: acpi_install counter_install cpupower_install dma_install gpio_install \
 		hv_install firewire_install iio_install \
 		perf_install selftests_install turbostat_install usb_install \
 		virtio_install mm_install bpf_install x86_energy_perf_policy_install \
 		tmon_install freefall_install objtool_install kvm_stat_install \
 		wmi_install debugging_install intel-speed-select_install \
-		tracing_install thermometer_install thermal-engine_install ynl_install
+		tracing_install thermometer_install thermal-engine_install ynl_install \
+		nvmem_install
 
 acpi_clean:
 	$(call descend,power/acpi,clean)
@@ -225,12 +233,15 @@ build_clean:
 ynl_clean:
 	$(call descend,net/$(@:_clean=),clean)
 
+nvmem_clean:
+	$(call descend,nvmem,clean)
+
 clean: acpi_clean counter_clean cpupower_clean dma_clean hv_clean firewire_clean \
 		perf_clean selftests_clean turbostat_clean bootconfig_clean spi_clean usb_clean virtio_clean \
 		mm_clean bpf_clean iio_clean x86_energy_perf_policy_clean tmon_clean \
 		freefall_clean build_clean libbpf_clean libsubcmd_clean \
 		gpio_clean objtool_clean leds_clean wmi_clean firmware_clean debugging_clean \
 		intel-speed-select_clean tracing_clean thermal_clean thermometer_clean thermal-engine_clean \
-		sched_ext_clean ynl_clean
+		sched_ext_clean ynl_clean nvmem_clean
 
 .PHONY: FORCE
diff --git a/tools/nvmem/Makefile b/tools/nvmem/Makefile
new file mode 100644
index 000000000000..7ef25f616f7a
--- /dev/null
+++ b/tools/nvmem/Makefile
@@ -0,0 +1,22 @@
+# SPDX-License-Identifier: GPL-2.0-only
+include ../scripts/Makefile.include
+
+CC ?= $(CROSS_COMPILE)gcc
+CFLAGS += -Wall -Wextra -O2
+
+PROG := nvmemctl
+SRCS := nvmemctl.c
+
+all: $(PROG)
+
+$(PROG): $(SRCS)
+	$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^
+
+install: $(PROG)
+	install -d $(DESTDIR)$(PREFIX)/bin
+	install -m 755 $(PROG) $(DESTDIR)$(PREFIX)/bin/
+
+clean:
+	rm -f $(PROG)
+
+.PHONY: all install clean
diff --git a/tools/nvmem/nvmemctl.c b/tools/nvmem/nvmemctl.c
new file mode 100644
index 000000000000..cec70f99dc3d
--- /dev/null
+++ b/tools/nvmem/nvmemctl.c
@@ -0,0 +1,340 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * nvmemctl - Userspace tool to list and access NVMEM devices
+ *
+ * Copyright (C) 2026 Kuan-Wei Chiu <visitorckw@gmail.com>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <dirent.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/stat.h>
+#include <errno.h>
+#include <limits.h>
+
+#define NVMEM_SYSPATH "/sys/bus/nvmem/devices"
+
+struct nvmem_cell {
+	char name[256];
+	unsigned int byte_offset;
+	unsigned int bit_offset;
+	size_t size;
+	struct nvmem_cell *next;
+};
+
+struct nvmem_device {
+	char name[256];
+	char type[32];
+	bool force_ro;
+	size_t size;
+	struct nvmem_cell *cells;
+	struct nvmem_device *next;
+};
+
+static int read_sysfs_string(const char *dev_name, const char *attr, char *buf, size_t buf_size)
+{
+	char path[PATH_MAX];
+	size_t len;
+	FILE *f;
+
+	snprintf(path, sizeof(path), "%s/%s/%s", NVMEM_SYSPATH, dev_name, attr);
+	f = fopen(path, "r");
+	if (!f)
+		return -1;
+
+	if (fgets(buf, buf_size, f)) {
+		len = strlen(buf);
+		if (len > 0 && buf[len - 1] == '\n')
+			buf[len - 1] = '\0';
+	} else {
+		buf[0] = '\0';
+	}
+
+	fclose(f);
+	return 0;
+}
+
+static int write_sysfs_string(const char *dev_name, const char *attr, const char *val)
+{
+	char path[PATH_MAX];
+	int fd, ret;
+
+	snprintf(path, sizeof(path), "%s/%s/%s", NVMEM_SYSPATH, dev_name, attr);
+	fd = open(path, O_WRONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Failed to open %s for writing: %s\n", path, strerror(errno));
+		return -1;
+	}
+
+	ret = write(fd, val, strlen(val));
+	close(fd);
+
+	if (ret < 0) {
+		fprintf(stderr, "Failed to write to %s: %s\n", path, strerror(errno));
+		return -1;
+	}
+	return 0;
+}
+
+static struct nvmem_cell *scan_nvmem_cells(const char *dev_name)
+{
+	char cells_path[PATH_MAX];
+	DIR *dp;
+	struct dirent *entry;
+	struct nvmem_cell *head = NULL, *tail = NULL;
+
+	snprintf(cells_path, sizeof(cells_path), "%s/%s/cells", NVMEM_SYSPATH, dev_name);
+	dp = opendir(cells_path);
+	if (!dp)
+		return NULL;
+
+	while ((entry = readdir(dp)) != NULL) {
+		struct nvmem_cell *cell;
+		char cell_file_path[PATH_MAX];
+		struct stat st;
+		int parsed;
+
+		if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+			continue;
+
+		cell = calloc(1, sizeof(*cell));
+		if (!cell)
+			break;
+
+		parsed = sscanf(entry->d_name, "%[^@]@%x,%x", cell->name,
+				&cell->byte_offset, &cell->bit_offset);
+		if (parsed < 2) {
+			snprintf(cell->name, sizeof(cell->name), "%s", entry->d_name);
+			cell->byte_offset = 0;
+			cell->bit_offset = 0;
+		} else if (parsed == 2) {
+			cell->bit_offset = 0;
+		}
+
+		snprintf(cell_file_path, sizeof(cell_file_path), "%s/%s/cells/%s",
+			 NVMEM_SYSPATH, dev_name, entry->d_name);
+		if (stat(cell_file_path, &st) == 0)
+			cell->size = st.st_size;
+
+		if (!head) {
+			head = cell;
+			tail = cell;
+		} else {
+			tail->next = cell;
+			tail = cell;
+		}
+	}
+	closedir(dp);
+	return head;
+}
+
+static struct nvmem_device *scan_nvmem_devices(void)
+{
+	DIR *dp;
+	struct dirent *entry;
+	struct nvmem_device *head = NULL, *tail = NULL;
+
+	dp = opendir(NVMEM_SYSPATH);
+	if (!dp) {
+		perror("Failed to open NVMEM sysfs directory");
+		return NULL;
+	}
+
+	while ((entry = readdir(dp)) != NULL) {
+		struct nvmem_device *dev;
+		char ro_buf[8] = {0};
+		char nvmem_file_path[PATH_MAX];
+		struct stat st;
+
+		if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
+			continue;
+
+		dev = calloc(1, sizeof(*dev));
+		if (!dev)
+			break;
+
+		snprintf(dev->name, sizeof(dev->name), "%s", entry->d_name);
+
+		if (read_sysfs_string(dev->name, "type", dev->type, sizeof(dev->type)) < 0)
+			strncpy(dev->type, "Unknown", sizeof(dev->type));
+
+		if (read_sysfs_string(dev->name, "force_ro", ro_buf, sizeof(ro_buf)) == 0)
+			dev->force_ro = (ro_buf[0] == '1');
+
+		snprintf(nvmem_file_path, sizeof(nvmem_file_path), "%s/%s/nvmem",
+			 NVMEM_SYSPATH, dev->name);
+		if (stat(nvmem_file_path, &st) == 0)
+			dev->size = st.st_size;
+
+		dev->cells = scan_nvmem_cells(dev->name);
+
+		if (!head) {
+			head = dev;
+			tail = dev;
+		} else {
+			tail->next = dev;
+			tail = dev;
+		}
+	}
+	closedir(dp);
+	return head;
+}
+
+static void free_nvmem_devices(struct nvmem_device *devices)
+{
+	struct nvmem_device *tmp_dev;
+	struct nvmem_cell *c, *tmp_c;
+
+	while (devices) {
+		tmp_dev = devices;
+		c = devices->cells;
+
+		while (c) {
+			tmp_c = c;
+			c = c->next;
+			free(tmp_c);
+		}
+
+		devices = devices->next;
+		free(tmp_dev);
+	}
+}
+
+static void dump_binary_data(const char *path, const char *title)
+{
+	int fd, i;
+	unsigned char buf[16];
+	ssize_t bytes_read;
+	size_t offset = 0;
+
+	fd = open(path, O_RDONLY);
+	if (fd < 0) {
+		fprintf(stderr, "Failed to open %s: %s\n", path, strerror(errno));
+		return;
+	}
+
+	printf("Dumping %s:\n", title);
+	printf("Offset    00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F | ASCII\n");
+	printf("------------------------------------------------------------------\n");
+
+	while ((bytes_read = read(fd, buf, sizeof(buf))) > 0) {
+		printf("%08zX  ", offset);
+
+		for (i = 0; i < 16; i++) {
+			if (i < bytes_read)
+				printf("%02X ", buf[i]);
+			else
+				printf("   ");
+		}
+		printf(" | ");
+		for (i = 0; i < bytes_read; i++) {
+			if (buf[i] >= 32 && buf[i] <= 126)
+				printf("%c", buf[i]);
+			else
+				printf(".");
+		}
+		printf("\n");
+		offset += bytes_read;
+	}
+
+	if (bytes_read < 0)
+		perror("Error reading file");
+
+	printf("------------------------------------------------------------------\n");
+	close(fd);
+}
+
+static void cmd_list(void)
+{
+	struct nvmem_device *devices, *current;
+	struct nvmem_cell *c;
+
+	printf("%-15s | %-15s | %-5s | %-8s\n", "Device", "Type", "R/O", "Size(B)");
+	printf("------------------------------------------------------------------\n");
+
+	devices = scan_nvmem_devices();
+	if (!devices) {
+		printf("No NVMEM devices found.\n");
+		return;
+	}
+
+	for (current = devices; current; current = current->next) {
+		printf("%-15s | %-15s | %-5s | %-8zu\n",
+		       current->name, current->type,
+		       current->force_ro ? "Yes" : "No", current->size);
+
+		for (c = current->cells; c; c = c->next) {
+			printf("  |- Cell: %-15s (Offset: 0x%04X, Bit: %d, Size: %zu B)\n",
+			       c->name, c->byte_offset, c->bit_offset, c->size);
+		}
+	}
+	printf("------------------------------------------------------------------\n");
+	free_nvmem_devices(devices);
+}
+
+static void print_usage(const char *progname)
+{
+	printf("Usage: %s <command> [args]\n\n", progname);
+	printf("Commands:\n");
+	printf("  list                          List all NVMEM devices and cells\n");
+	printf("  dump   <device>               Hexdump the entire NVMEM device\n");
+	printf("  read   <device> <cell_name>   Hexdump a specific cell within a device\n");
+	printf("  lock   <device>               Set device read-only (force_ro=1)\n");
+	printf("  unlock <device>               Set device read-write (force_ro=0)\n");
+}
+
+int main(int argc, char *argv[])
+{
+	char path[PATH_MAX];
+	DIR *dp;
+	struct dirent *entry;
+	int found = 0;
+
+	if (argc < 2) {
+		print_usage(argv[0]);
+		return 1;
+	}
+
+	if (!strcmp(argv[1], "list")) {
+		cmd_list();
+	} else if (!strcmp(argv[1], "dump") && argc == 3) {
+		snprintf(path, sizeof(path), "%s/%s/nvmem", NVMEM_SYSPATH, argv[2]);
+		dump_binary_data(path, argv[2]);
+	} else if (!strcmp(argv[1], "read") && argc == 4) {
+		snprintf(path, sizeof(path), "%s/%s/cells", NVMEM_SYSPATH, argv[2]);
+		dp = opendir(path);
+		if (!dp) {
+			fprintf(stderr, "Could not open cells directory for %s\n", argv[2]);
+			return 1;
+		}
+
+		while ((entry = readdir(dp)) != NULL) {
+			if (strncmp(entry->d_name, argv[3], strlen(argv[3])) == 0) {
+				snprintf(path, sizeof(path), "%s/%s/cells/%s",
+					 NVMEM_SYSPATH, argv[2], entry->d_name);
+				dump_binary_data(path, argv[3]);
+				found = 1;
+				break;
+			}
+		}
+		closedir(dp);
+		if (!found)
+			fprintf(stderr, "Cell '%s' not found in device '%s'\n", argv[3], argv[2]);
+
+	} else if (!strcmp(argv[1], "lock") && argc == 3) {
+		if (write_sysfs_string(argv[2], "force_ro", "1") == 0)
+			printf("Successfully locked %s (Read-Only)\n", argv[2]);
+	} else if (!strcmp(argv[1], "unlock") && argc == 3) {
+		if (write_sysfs_string(argv[2], "force_ro", "0") == 0)
+			printf("Successfully unlocked %s (Read-Write)\n", argv[2]);
+	} else {
+		print_usage(argv[0]);
+		return 1;
+	}
+
+	return 0;
+}
-- 
2.53.0.851.ga537e3e6e9-goog