From 0a4626e7d133ecc87c1af004a450267a3c9790ff Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:23:33 +0100 Subject: [PATCH 01/31] Fix formatting when error occurs --- writeimg.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/writeimg.c b/writeimg.c index 3d6c04b..90c7323 100644 --- a/writeimg.c +++ b/writeimg.c @@ -175,7 +175,8 @@ int perform_write(write_job_t *job) { int cmp = memcmp((const void *)job->buffer2, (const void *)job->buffer, (size_t)read_file); if (0 != cmp) { - fprintf(stderr, "WARNING: File did not verify correctly!\n"); + fflush(stdout); + fprintf(stderr, "\nWARNING: File did not verify correctly!\n"); exit(EXIT_FAILURE); } } From a1556ca383b5dedf802e0dcb3f4d524a83bb6e50 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:23:59 +0100 Subject: [PATCH 02/31] Set (y/n) prompt to (y/N) to better reflect behaviour --- writeimg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/writeimg.c b/writeimg.c index 90c7323..a369075 100644 --- a/writeimg.c +++ b/writeimg.c @@ -265,7 +265,7 @@ int main(int argc, char *argv[]) { printf("Writing %s to %s\n", wjob.filename, wjob.dev_name); if (ask_permission && !wjob.verify_only) { - printf("Is this okay? (y/n): "); + printf("Is this okay? (y/N): "); fflush(stdout); if ('y' != getchar()) { printf("Aborting...\n"); From 53239324ef9b1ec738372e0994eb9e05db29715b Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:24:32 +0100 Subject: [PATCH 03/31] Tell the user how much data will be written beforehand --- writeimg.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/writeimg.c b/writeimg.c index a369075..7bcd467 100644 --- a/writeimg.c +++ b/writeimg.c @@ -261,8 +261,14 @@ int main(int argc, char *argv[]) { } close(fd); + wjob.total_bytes = file_stat.st_size; + assert(file_stat.st_size >= 0); + if (!wjob.verify_only) - printf("Writing %s to %s\n", wjob.filename, wjob.dev_name); + printf("Writing \"%s\" (%.1f MiB) to \"%s\"\n", + basename(wjob.filename), + BYTES_TO_MIB(wjob.total_bytes), + wjob.dev_name); if (ask_permission && !wjob.verify_only) { printf("Is this okay? (y/N): "); @@ -281,9 +287,6 @@ int main(int argc, char *argv[]) { wjob.bufsize = BLOCKSIZE; wjob.block_size = BLOCKSIZE; - wjob.total_bytes = file_stat.st_size; - assert(file_stat.st_size >= 0); - perform_write(&wjob); if (wjob.buffer) From 9d2645ad65688140c645c489b8c4855217906825 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:38:48 +0100 Subject: [PATCH 04/31] Readme --- README.txt | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/README.txt b/README.txt index da4be03..2af80c3 100644 --- a/README.txt +++ b/README.txt @@ -1,4 +1,24 @@ -Simple dd-like image writer with additional security checks. +# Simple dd-like image writer with additional security checks. + +``` +writeimg v0.2.0, Rev. 5323932-dirty +In honor of SwePwnage - the OG disk destroyer +Copyright (C) 2026 Imbus, BSD-2-Clause +Build date: 2026-02-07 + +Usage: + writeimg [-v] -d + +Args: + Binary image file + -v Verify only + -d device Target block device + -h, --help Print this help message + -n, --noconfirm Do not ask for premission + -V, --version Print version +``` + +## Testing dd if=/dev/zero of=./disk.img bs=1M count=1024 losetup -fP ./disk.img From 008e9e7ca9b6698ca5da7de1d9f7ab6412bdd6fe Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:39:18 +0100 Subject: [PATCH 05/31] Readme txt -> md --- README.txt => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.txt => README.md (100%) diff --git a/README.txt b/README.md similarity index 100% rename from README.txt rename to README.md From 5cbfc38e62ec542bb82b69c6283f04edd5f7f0d8 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:42:38 +0100 Subject: [PATCH 06/31] Add some introduction to readme and brush up the formatting --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2af80c3..9d84da2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ # Simple dd-like image writer with additional security checks. +This tool verifies your image automatically. The block size is currently set to +1 MiB, which should be enough to avoid trouble. + ``` writeimg v0.2.0, Rev. 5323932-dirty In honor of SwePwnage - the OG disk destroyer @@ -20,12 +23,16 @@ Args: ## Testing +``` dd if=/dev/zero of=./disk.img bs=1M count=1024 losetup -fP ./disk.img losetup -a losedup -d /dev/loop0 +``` + +## Inspiration See: - https://github.com/landley/toybox/blob/master/toys/posix/dd.c - https://github.com/illiliti/libudev-zero - https://github.com/LoupVaillant/Monocypher + - https://github.com/landley/toybox/blob/master/toys/posix/dd.c + - https://github.com/illiliti/libudev-zero + - https://github.com/LoupVaillant/Monocypher From 692f89d6c24d78181f322ec0819bcf832a58e970 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:44:40 +0100 Subject: [PATCH 07/31] Formatting --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9d84da2..6dfebd5 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,6 @@ losedup -d /dev/loop0 ## Inspiration See: - - https://github.com/landley/toybox/blob/master/toys/posix/dd.c - - https://github.com/illiliti/libudev-zero - - https://github.com/LoupVaillant/Monocypher + - https://github.com/landley/toybox/blob/master/toys/posix/dd.c + - https://github.com/illiliti/libudev-zero + - https://github.com/LoupVaillant/Monocypher From 117d01fcbea40ca937d4742dc4b89124654b72ea Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 7 Feb 2026 18:52:56 +0100 Subject: [PATCH 08/31] v0.2.1 bump --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 58199f1..416e4c1 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GITREV ?= $(shell git describe --dirty --always) BLDDATE ?= $(shell date -I) CR_YEAR ?= $(shell date +%Y) -VERSION ?= "v0.2.0" +VERSION ?= "v0.2.1" CFLAGS ?= -Wall -Wextra -Wpedantic -O2 -std=gnu99 CFLAGS += -DGITREV='"$(GITREV)"' From 734c1c10ecff98130c5f45493dc5295f5b2ab7b5 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 13:36:59 +0100 Subject: [PATCH 09/31] Use bitmasking to keep track of flags --- writeimg.c | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/writeimg.c b/writeimg.c index 7bcd467..932a20d 100644 --- a/writeimg.c +++ b/writeimg.c @@ -32,6 +32,10 @@ #define BLOCKSIZE (1024 * 1024) #endif +#define WI_VERIFY (1 << 0) +#define WI_WRITE (1 << 1) +#define WI_ASK (1 << 2) + #define BYTES_TO_MIB(bts) ((double)bts / (1024 * 1024)) #define BAR_WIDTH 50 @@ -76,7 +80,7 @@ struct write_job { size_t bufsize; size_t block_size; size_t total_bytes; - char verify_only; + char flags; } wjob = {0}; typedef struct write_job write_job_t; @@ -114,7 +118,7 @@ int perform_write(write_job_t *job) { if (read_bytes == 0) { crc = crc32_finalize(crc); - if (!job->verify_only) { + if (job->flags & WI_WRITE) { printf("\nWriting done...\n"); assert(job->total_bytes == b_written); } else @@ -131,7 +135,7 @@ int perform_write(write_job_t *job) { crc = crc32_update(crc, job->buffer, read_bytes); - if (!job->verify_only) { + if (job->flags & WI_WRITE) { ssize_t written_bytes = write(block_fd, job->buffer, read_bytes); if (written_bytes < 0) { fprintf(stderr, "%s: Write error\n", job->dev_name); @@ -206,14 +210,18 @@ int main(int argc, char *argv[]) { signal(SIGHUP, int_handler); signal(SIGTERM, int_handler); - int ask_permission = 1; + wjob.flags = WI_VERIFY | WI_WRITE | WI_ASK; + int c = {0}; while ((c = getopt_long(argc, argv, "vd:hnV", longopts, 0)) != -1) { switch (c) { - case 'v': ++wjob.verify_only; continue; + case 'v': + wjob.flags |= WI_VERIFY; + wjob.flags &= ~WI_WRITE; + continue; case 'd': wjob.dev_name = optarg; continue; case 'h': break; - case 'n': --ask_permission; continue; + case 'n': wjob.flags &= ~WI_ASK; continue; case 'V': exit(EXIT_SUCCESS); } printf("In honor of SwePwnage - the OG disk destroyer\n"); @@ -264,13 +272,13 @@ int main(int argc, char *argv[]) { wjob.total_bytes = file_stat.st_size; assert(file_stat.st_size >= 0); - if (!wjob.verify_only) + if (wjob.flags & WI_WRITE) printf("Writing \"%s\" (%.1f MiB) to \"%s\"\n", basename(wjob.filename), BYTES_TO_MIB(wjob.total_bytes), wjob.dev_name); - if (ask_permission && !wjob.verify_only) { + if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) { printf("Is this okay? (y/N): "); fflush(stdout); if ('y' != getchar()) { From 078ba792f065284104b3ab28fc8ab25e8b78f232 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 13:52:57 +0100 Subject: [PATCH 10/31] Get version string from git, instead of hardcoded in makefile --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 416e4c1..c26b039 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ GITREV ?= $(shell git describe --dirty --always) BLDDATE ?= $(shell date -I) CR_YEAR ?= $(shell date +%Y) -VERSION ?= "v0.2.1" +VERSION ?= "$(shell git describe --tags --always --abbrev=0 2>/dev/null || git rev-parse --short HEAD)" CFLAGS ?= -Wall -Wextra -Wpedantic -O2 -std=gnu99 CFLAGS += -DGITREV='"$(GITREV)"' From d0a3db77e914128a3482470c363bc548afc56d99 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 13:53:26 +0100 Subject: [PATCH 11/31] Remove commented CFLAGS appends for libudev --- Makefile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Makefile b/Makefile index c26b039..5f38eca 100644 --- a/Makefile +++ b/Makefile @@ -10,10 +10,6 @@ CFLAGS += -DCR_YEAR='"$(CR_YEAR)"' CFLAGS += -DVERSION='$(VERSION)' CFLAGS += $(EXTRA_CFLAGS) -# Soon... -# CFLAGS += $(shell pkg-config --cflags libudev) -# LIBS += $(shell pkg-config --libs libudev) - PREFIX ?= /usr/local DESTDIR ?= From 97239242226b91541ad5fcc95e097b2c7f7caee6 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 13:53:46 +0100 Subject: [PATCH 12/31] Use stylized name in help section --- writeimg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/writeimg.c b/writeimg.c index 932a20d..1c500b4 100644 --- a/writeimg.c +++ b/writeimg.c @@ -199,7 +199,7 @@ static const struct option longopts[] = { }; int main(int argc, char *argv[]) { - printf("%s %s, Rev. %s\n", basename(argv[0]), VERSION, GITREV); + printf("%s %s, Rev. %s\n", "WriteIMG", VERSION, GITREV); /* Line buffering, system allocated */ setvbuf(stdout, NULL, _IOLBF, 0); From 36a98b26308610a84efb2da5cd97994cf16fd948 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 13:59:10 +0100 Subject: [PATCH 13/31] Simple test --- test.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 test.sh diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..6aa0b58 --- /dev/null +++ b/test.sh @@ -0,0 +1,43 @@ +#!/usr/env/bin bash + +set -e + +DISKFILE="/tmp/disk.img" +BINFILE="/tmp/file.bin" +LOOPNUM=$((RANDOM % 156 + 100)) +LOOPDEV="/dev/loop${LOOPNUM}" + +echo "Using device: ${LOOPDEV}" + +cleanup() { + echo "Cleaning up..." + set +e + sudo losetup -d ${LOOPDEV} + sudo rm ${LOOPDEV} + sudo rm ${BINFILE} ${DISKFILE} +} + +trap cleanup EXIT INT TERM + +if losetup ${LOOPDEV} >/dev/null 2>&1; then + echo "${LOOPDEV} already in use" >&2 + cleanup + exit 1 +fi + +if [ ! -f ${DISKFILE} ]; then + dd if=/dev/zero of=${DISKFILE} bs=1M count=256 +fi + +if [ ! -f ${BINFILE} ]; then + dd if=/dev/urandom of=${BINFILE} bs=1M count=64 +fi + +if [ ! -e ${LOOPDEV} ]; then + sudo losetup ${LOOPDEV} ${DISKFILE} +fi + +sudo ./writeimg -nd ${LOOPDEV} ${BINFILE} +sudo ./writeimg -vnd ${LOOPDEV} ${BINFILE} + +echo "Looks good!" From d1d3b3cd453e18a83ac690543c76922cae9660f2 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 14:42:28 +0100 Subject: [PATCH 14/31] Rename dev_name and filename to iname/oname in preparation for enabling file outputs --- writeimg.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/writeimg.c b/writeimg.c index 1c500b4..2d27a98 100644 --- a/writeimg.c +++ b/writeimg.c @@ -73,8 +73,8 @@ const char help[] = const char copyright[] = "Copyright (C) %s Imbus, BSD-2-Clause\n"; struct write_job { - char *filename; - char *dev_name; + char *iname; + char *oname; char *buffer; char *buffer2; /* For memcmp integrity checks */ size_t bufsize; @@ -98,8 +98,8 @@ void int_handler(int signum) { } int perform_write(write_job_t *job) { - int block_fd = open(job->dev_name, O_RDWR); - int file_fd = open(job->filename, O_RDONLY); + int block_fd = open(job->oname, O_RDWR); + int file_fd = open(job->iname, O_RDONLY); assert(block_fd >= 0); assert(file_fd >= 0); @@ -128,7 +128,7 @@ int perform_write(write_job_t *job) { } if (read_bytes < 0) { - fprintf(stderr, "%s: Read error\n", job->filename); + fprintf(stderr, "%s: Read error\n", job->iname); perror("Read"); exit(EXIT_FAILURE); } @@ -138,7 +138,7 @@ int perform_write(write_job_t *job) { if (job->flags & WI_WRITE) { ssize_t written_bytes = write(block_fd, job->buffer, read_bytes); if (written_bytes < 0) { - fprintf(stderr, "%s: Write error\n", job->dev_name); + fprintf(stderr, "%s: Write error\n", job->oname); perror("Write"); exit(EXIT_FAILURE); } @@ -219,7 +219,7 @@ int main(int argc, char *argv[]) { wjob.flags |= WI_VERIFY; wjob.flags &= ~WI_WRITE; continue; - case 'd': wjob.dev_name = optarg; continue; + case 'd': wjob.oname = optarg; continue; case 'h': break; case 'n': wjob.flags &= ~WI_ASK; continue; case 'V': exit(EXIT_SUCCESS); @@ -244,27 +244,27 @@ int main(int argc, char *argv[]) { exit(EXIT_FAILURE); } - wjob.filename = argv[0]; + wjob.iname = argv[0]; struct stat file_stat = {0}; - if (0 != stat(wjob.filename, &file_stat)) { + if (0 != stat(wjob.iname, &file_stat)) { printf("File does not exist...\n"); exit(EXIT_FAILURE); } - if (NULL == wjob.dev_name) { + if (NULL == wjob.oname) { printf("You need to specify a device.\n"); exit(EXIT_FAILURE); } - if (0 != strncmp(wjob.dev_name, "/dev/", 4)) { - printf("\"%s\" does not appear to be a block device...\n", wjob.dev_name); + if (!(wjob.flags & WI_FILE) && 0 != strncmp(wjob.oname, "/dev/", 4)) { + printf("\"%s\" does not appear to be a block device...\n", wjob.oname); exit(EXIT_FAILURE); } /* Seems to be the cleanest way to check for write perm on a blockdev */ - int fd = open(wjob.dev_name, O_WRONLY); + int fd = open(wjob.oname, O_WRONLY); if (fd < 0) { - printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.dev_name); + printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.oname); exit(1); } close(fd); @@ -273,10 +273,8 @@ int main(int argc, char *argv[]) { assert(file_stat.st_size >= 0); if (wjob.flags & WI_WRITE) - printf("Writing \"%s\" (%.1f MiB) to \"%s\"\n", - basename(wjob.filename), - BYTES_TO_MIB(wjob.total_bytes), - wjob.dev_name); + printf( + "Writing \"%s\" (%.1f MiB) to \"%s\"\n", basename(wjob.iname), BYTES_TO_MIB(wjob.total_bytes), wjob.oname); if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) { printf("Is this okay? (y/N): "); From 4127ad6333f345c2e304e634bb0c54d50405ece4 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 14:42:56 +0100 Subject: [PATCH 15/31] More tests Include some notes about cleanup and accidentally deleting loop-control --- test.sh | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/test.sh b/test.sh index 6aa0b58..70cfae9 100644 --- a/test.sh +++ b/test.sh @@ -1,6 +1,16 @@ #!/usr/env/bin bash set -e +# set -x # For debugging + +# If you ever mess this up: +# $ sudo mknod /dev/loop-control c 10 237 +# $ sudo chmod 600 /dev/loop-control +# $ sudo chown root:root /dev/loop-control +# +# For cleanup: +# $ sudo find /dev -maxdepth 1 -type b -name 'loop[0-9]*' -exec rm -f {} \; + DISKFILE="/tmp/disk.img" BINFILE="/tmp/file.bin" @@ -11,7 +21,7 @@ echo "Using device: ${LOOPDEV}" cleanup() { echo "Cleaning up..." - set +e + set +e -x sudo losetup -d ${LOOPDEV} sudo rm ${LOOPDEV} sudo rm ${BINFILE} ${DISKFILE} @@ -40,4 +50,16 @@ fi sudo ./writeimg -nd ${LOOPDEV} ${BINFILE} sudo ./writeimg -vnd ${LOOPDEV} ${BINFILE} -echo "Looks good!" +sudo ./writeimg -nd ${LOOPDEV} ./writeimg +sudo ./writeimg -vnd ${LOOPDEV} ./writeimg + +sudo ./writeimg -nd ${LOOPDEV} ./LICENSE +sudo ./writeimg -vnd ${LOOPDEV} ./LICENSE + +# Redirect this to avoid confusion +! sudo ./writeimg -vnd ${LOOPDEV} ./crc32.h 2>/dev/null + +GREEN="\e[32m" +RESET="\e[0m" + +echo -e "\n\n${GREEN}Looks good!${RESET}" From f37e28c4ef2fd1ecd04f8d69e44103754d970607 Mon Sep 17 00:00:00 2001 From: Imbus Date: Mon, 9 Feb 2026 14:44:42 +0100 Subject: [PATCH 16/31] Remove accidental inclusion of unfinished WI_FILE check --- writeimg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/writeimg.c b/writeimg.c index 2d27a98..38cf3d0 100644 --- a/writeimg.c +++ b/writeimg.c @@ -256,7 +256,7 @@ int main(int argc, char *argv[]) { exit(EXIT_FAILURE); } - if (!(wjob.flags & WI_FILE) && 0 != strncmp(wjob.oname, "/dev/", 4)) { + if (0 != strncmp(wjob.oname, "/dev/", 4)) { printf("\"%s\" does not appear to be a block device...\n", wjob.oname); exit(EXIT_FAILURE); } From a62728e9dbea887a59ae42e1a1ae433e03685c19 Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 13 Feb 2026 17:30:42 +0100 Subject: [PATCH 17/31] Initial manpage --- writeimg.1 | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 writeimg.1 diff --git a/writeimg.1 b/writeimg.1 new file mode 100644 index 0000000..017796f --- /dev/null +++ b/writeimg.1 @@ -0,0 +1,38 @@ +.TH WRITEIMG 1 "February 2026" "writeimg" "User Commands" +.SH NAME +writeimg \- Write an image to a block device +.SH SYNOPSIS +.B writeimg +\fB\-d\fR \fIDEVICE\fR \fIIMAGE\fR +.br +.B writeimg +\fB\-\-device\fR \fIDEVICE\fR \fIIMAGE\fR + +.SH DESCRIPTION +.B writeimg +reads the input +.I IMAGE +and writes its contents to +.I DEVICE + +The output device must be specified explicitly using the +.B \-d +or +.B \-\-device +option. The image file is given as a positional argument. +.SH OPTIONS +.TP +.BR \-d ", " \-\-device" " DEVICE +Device to write to. +.TP +.BR \-n ", " \-\-noconfirm +Do not require confirmation from user. +.TP +.BR \-v ", " \-\-verify +Verify only, do not write +.SH EXAMPLES +Write an image to a block device: +.PP +.nf + writeimg -d /dev/sdx file.img +.fi From 71ef502cea7a74bf9472626b62c66132d65b1c47 Mon Sep 17 00:00:00 2001 From: Imbus Date: Fri, 13 Feb 2026 17:33:43 +0100 Subject: [PATCH 18/31] Include manpage in 'make install' --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index 5f38eca..6e7b732 100644 --- a/Makefile +++ b/Makefile @@ -21,4 +21,6 @@ clean: install: writeimg install -d $(DESTDIR)$(PREFIX)/bin + install -d $(DESTDIR)$(PREFIX)/share/man/man1 install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$< + install -m 0755 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1 From d0728a87c032b88844c2451ededb0ff834ec2952 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 14 Feb 2026 15:55:35 +0100 Subject: [PATCH 19/31] Manpage permissions to 644 --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 6e7b732..fcee27d 100644 --- a/Makefile +++ b/Makefile @@ -23,4 +23,4 @@ install: writeimg install -d $(DESTDIR)$(PREFIX)/bin install -d $(DESTDIR)$(PREFIX)/share/man/man1 install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$< - install -m 0755 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1 + install -m 0644 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1 From c1e38b365c43fe94589a55f8dc427ad2691e4622 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sat, 14 Feb 2026 16:02:47 +0100 Subject: [PATCH 20/31] Uninstall target, PHONY --- Makefile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile b/Makefile index fcee27d..eb6a5cd 100644 --- a/Makefile +++ b/Makefile @@ -24,3 +24,9 @@ install: writeimg install -d $(DESTDIR)$(PREFIX)/share/man/man1 install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$< install -m 0644 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1 + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/bin/writeimg + rm -f $(DESTDIR)$(PREFIX)/share/man/man1/writeimg.1 + +.PHONY: clean install uninstall From e038ab224d801d71ae3c7a42b6406002f47da683 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 15 Feb 2026 02:45:12 +0100 Subject: [PATCH 21/31] Simplify: fprintf -> printf --- writeimg.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/writeimg.c b/writeimg.c index 38cf3d0..f606624 100644 --- a/writeimg.c +++ b/writeimg.c @@ -225,7 +225,7 @@ int main(int argc, char *argv[]) { case 'V': exit(EXIT_SUCCESS); } printf("In honor of SwePwnage - the OG disk destroyer\n"); - fprintf(stdout, copyright, CR_YEAR); + printf(copyright, CR_YEAR); #ifdef BLDDATE printf("Build date: %s\n", BLDDATE); #endif From bf55c2d0a95722ca617c8633b72d8fa817ab2fef Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 15 Feb 2026 03:10:54 +0100 Subject: [PATCH 22/31] Performance: Update progress bar only when changed --- writeimg.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/writeimg.c b/writeimg.c index f606624..1e473bf 100644 --- a/writeimg.c +++ b/writeimg.c @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -41,8 +42,12 @@ #define BAR_WIDTH 50 void print_progress(int current, int total) { - float fraction = (float)current / total; - int filled = (int)(fraction * BAR_WIDTH); + static int last = INT_MAX; + float fraction = (float)current / total; + int filled = (int)(fraction * BAR_WIDTH); + + if (filled == last) + return; /* Avoid unnecessary io/flushes */ printf("\r["); for (int i = 0; i < BAR_WIDTH; i++) { From 60e7179766d95cb56540759d6eea4dcd9b9a17f8 Mon Sep 17 00:00:00 2001 From: Imbus <> Date: Sun, 15 Feb 2026 03:22:26 +0100 Subject: [PATCH 23/31] Fix forgotten assignment to last --- writeimg.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/writeimg.c b/writeimg.c index 1e473bf..245e6c4 100644 --- a/writeimg.c +++ b/writeimg.c @@ -49,6 +49,8 @@ void print_progress(int current, int total) { if (filled == last) return; /* Avoid unnecessary io/flushes */ + last = filled; + printf("\r["); for (int i = 0; i < BAR_WIDTH; i++) { if (i < filled) From 2e02a056cc510ef7276cd675a3ae35347e46db5d Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 03:57:30 +0100 Subject: [PATCH 24/31] Now also reports the size of the block device before asking to continue --- writeimg.c | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/writeimg.c b/writeimg.c index 245e6c4..e839930 100644 --- a/writeimg.c +++ b/writeimg.c @@ -268,20 +268,34 @@ int main(int argc, char *argv[]) { exit(EXIT_FAILURE); } - /* Seems to be the cleanest way to check for write perm on a blockdev */ - int fd = open(wjob.oname, O_WRONLY); - if (fd < 0) { - printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.oname); - exit(1); + uint64_t device_size; + { + /* Seems to be the cleanest way to check for write perm on a blockdev */ + int fd = open(wjob.oname, O_WRONLY); + if (fd < 0) { + printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.oname); + close(fd); + exit(EXIT_FAILURE); + } + + if (ioctl(fd, BLKGETSIZE64, &device_size) < 0) { + perror("ioctl"); + close(fd); + exit(EXIT_FAILURE); + } + + close(fd); } - close(fd); wjob.total_bytes = file_stat.st_size; assert(file_stat.st_size >= 0); if (wjob.flags & WI_WRITE) - printf( - "Writing \"%s\" (%.1f MiB) to \"%s\"\n", basename(wjob.iname), BYTES_TO_MIB(wjob.total_bytes), wjob.oname); + printf("Writing \"%s\" (%.1f MiB) to \"%s\" (%.1f MiB)\n", + basename(wjob.iname), + BYTES_TO_MIB(wjob.total_bytes), + wjob.oname, + BYTES_TO_MIB(device_size)); if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) { printf("Is this okay? (y/N): "); From d02fc5f386664d19c9d791cd60e10c207ad7614c Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 06:03:41 +0100 Subject: [PATCH 25/31] Provide a detailed description of design considerations and future plans --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/README.md b/README.md index 6dfebd5..94f7535 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,14 @@ Args: -V, --version Print version ``` +We also provide a manpage, which targets OpenBSD-level quality. Incorrect or +outdated information is a bug. + ## Testing +For testing, there is a shell script "test.sh" included. It sets up a block dev +and writes (and verifies) a bunch of nonsense to it. It boils down to: + ``` dd if=/dev/zero of=./disk.img bs=1M count=1024 losetup -fP ./disk.img @@ -30,6 +36,75 @@ losetup -a losedup -d /dev/loop0 ``` +Writes to a regular files is not currently in scope, although it would simplify +testing. + +## Design Considerations + +Most of the sanity checking is currently highly Linux specific. We should +prefer general/posix solutions that reach **at least** FreeBSD, preferably +OpenBSD and Solaris as well. FreeBSD did implement procfs, but its a Linux-ism +and it has since been deprecated. I would prefer not to turn this rather simple +code into macro-mozaic, as i've seen other similar projects do. After all, this +is just a juiced-up dd-rewrite at its core. + +**Apple products are unsupported.** Im simply not interested in ensuring +compatibility with a walled-garden ecosystem. If *you* are, we can change that. + +At the time of writing, my FreeBSD server is down for maintenance, which means +all of my development and testing is focused on AMD64, AArch64 and AArch32 +Linux. + +In the setup phase of the program, we can absolutely afford to do lots of +sanity checking via syscalls. Between each block write, we flush the disk +buffers. These flushes are larger (1 MiB when buffer is full) than the sector +size (often 4k or 64k) of the flash, so as far as i know, this is a gentle way +to write flash, and should not incur any significant performance overhead. 1 +MiB is also a multiple of the most common sector sizes. We could write it all +with no flushing, but that would mean the progress indicator will measure +buffered writes, which is useless. + +It should be possible to induce optimal block size, but this ide has not been +explored yet. + +Currently when verifying the written data, we read from both the input file and +the output block device and do a byte-by-byte comparison. A CRC32 is also +calculated in the first pass of the input file read. In the second pass (the +verification stage), we calculate the CRC32 of the block device data and +compare that to our previous result. **This means that we currently use two +separate methods of verification**. The program allocates **two** separate +buffers in the startup phase for comparisons. This will change in the coming +releases, and we will rely only on the CRC. + +De-allocation is handled in the interrupt vector as well as in the 'sad-paths', +but ultimately this program can be regarded as samurai-principled. We try to +handle deallocation, but exit on failure and let the kernel handle the rest. + +The use of a crypto-grade checksumming algorithm was considered, but was +ultimately rejected in favour of a lookup based CRC32. Its simpler, faster and +easier to understand (See: [crc32.h](./crc32.h)). We may include a compile-time +option to disable the lookup table to reduce size for really small targets, but +we speculate that those targets are already satisfied with busybox-dd. + +We also considered using libudev or any of its analogues, to determine the type +of block device (spinning or flash), but my somewhat inconclusive research +indicates that it does not include functionality to determine the medium type +(usb/sd/sata), which is ultimately what i would like to warn the user about. +The libudev library is also Linux specific. + +We also need completion scripts for the most common shells. This includes csh, +bash and zsh. Should be easy enough when we set our minds to it. + +See: + - `grep -nE 'BLK[A-Za-z0-9]+' /usr/include/linux/fs.h` + In particular, we're interested in BLKGETSIZE64 and BLKFLSBUF for now. + +We can read the device info from: + - /sys/class/block/[name]/* +Like: + - /sys/class/block/[name]/device/model + - /sys/class/block/[name]/size + ## Inspiration See: From 5e9245f059b23aa6a4eb9bece595b1ede919abb6 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 06:05:02 +0100 Subject: [PATCH 26/31] Bump the help section in the readme to latest version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 94f7535..069db9d 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,10 @@ This tool verifies your image automatically. The block size is currently set to 1 MiB, which should be enough to avoid trouble. ``` -writeimg v0.2.0, Rev. 5323932-dirty +WriteIMG v0.2.3, Rev. d02fc5f In honor of SwePwnage - the OG disk destroyer Copyright (C) 2026 Imbus, BSD-2-Clause -Build date: 2026-02-07 +Build date: 2026-02-21 Usage: writeimg [-v] -d From febcedc2f3cf6b338662c4042fdf5aeffc544864 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 06:11:40 +0100 Subject: [PATCH 27/31] Provide a more descriptive introduction and a summary of writeimg's features --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 069db9d..0c30f7c 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,11 @@ -# Simple dd-like image writer with additional security checks. +# WriteIMG Image Writer -This tool verifies your image automatically. The block size is currently set to -1 MiB, which should be enough to avoid trouble. +WriteIMG is a simple dd-like image writer with additional security checks for +Linux and soon FreeBSD. It tells you what you're about to do and warns you +about potential problems before they occur. It also features automatic image +verifications, and uses a suitable block size for fast and gentle writing. + +Say goodbye to fried flash and overwritten bootloaders, no more footguns! ``` WriteIMG v0.2.3, Rev. d02fc5f From 4b853f9c1651cd037b46df9aa4ca4b647fd10438 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 06:17:07 +0100 Subject: [PATCH 28/31] Spelling errors --- README.md | 4 ++-- writeimg.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0c30f7c..f864c59 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Args: -v Verify only -d device Target block device -h, --help Print this help message - -n, --noconfirm Do not ask for premission + -n, --noconfirm Do not ask for permission -V, --version Print version ``` @@ -68,7 +68,7 @@ MiB is also a multiple of the most common sector sizes. We could write it all with no flushing, but that would mean the progress indicator will measure buffered writes, which is useless. -It should be possible to induce optimal block size, but this ide has not been +It should be possible to induce optimal block size, but this idea has not been explored yet. Currently when verifying the written data, we read from both the input file and diff --git a/writeimg.c b/writeimg.c index e839930..f0139ad 100644 --- a/writeimg.c +++ b/writeimg.c @@ -72,7 +72,7 @@ const char help[] = " -v Verify only\n" " -d device Target block device\n" " -h, --help Print this help message\n" - " -n, --noconfirm Do not ask for premission\n" + " -n, --noconfirm Do not ask for permission\n" " -V, --version Print version\n" "\0"; // clang-format on From 8601db8811a54717719702effe3bed94bc2ce86f Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 07:24:25 +0100 Subject: [PATCH 29/31] Initial bash completion --- Makefile | 7 +++++++ writeimg_completion_bash.sh | 25 +++++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 writeimg_completion_bash.sh diff --git a/Makefile b/Makefile index eb6a5cd..7663e96 100644 --- a/Makefile +++ b/Makefile @@ -25,8 +25,15 @@ install: writeimg install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$< install -m 0644 $<.1 $(DESTDIR)$(PREFIX)/share/man/man1/$<.1 +# Note that this bypasses PREFIX, since +# bash does not source /usr/local by default +install_bash: writeimg_completion_bash.sh + install -d $(DESTDIR)/etc/bash_completion.d + install -m 0644 $< $(DESTDIR)/etc/bash_completion.d/$< + uninstall: rm -f $(DESTDIR)$(PREFIX)/bin/writeimg rm -f $(DESTDIR)$(PREFIX)/share/man/man1/writeimg.1 + rm -f $(DESTDIR)/etc/bash_completion.d/writeimg* .PHONY: clean install uninstall diff --git a/writeimg_completion_bash.sh b/writeimg_completion_bash.sh new file mode 100644 index 0000000..305cf95 --- /dev/null +++ b/writeimg_completion_bash.sh @@ -0,0 +1,25 @@ +_writeimg_completion() { + local cur prev opts + + cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + + opts="-v -d -h --help -n --noconfirm -V --version" + + # Devices + if [[ "$prev" == "-d" ]]; then + COMPREPLY=( $(compgen -W "$(ls -d /dev/sd* /dev/nvme* /dev/mmcblk* 2>/dev/null)" -- "$cur") ) + return 0 + fi + + # Flags + if [[ "$cur" != -* ]]; then + COMPREPLY=( $(compgen -f -- "$cur") ) + return 0 + fi + + # Files + COMPREPLY=( $(compgen -W "$opts" -- "$cur") ) +} + +complete -F _writeimg_completion writeimg From d90c05ca77f4ddea252cbb0b899098b97747ed81 Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 08:05:09 +0100 Subject: [PATCH 30/31] Tests: test.sh does not use sudo internally, now runs under alpine with musl --- test.sh | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/test.sh b/test.sh index 70cfae9..1eb6280 100644 --- a/test.sh +++ b/test.sh @@ -4,13 +4,17 @@ set -e # set -x # For debugging # If you ever mess this up: -# $ sudo mknod /dev/loop-control c 10 237 -# $ sudo chmod 600 /dev/loop-control -# $ sudo chown root:root /dev/loop-control +# $ mknod /dev/loop-control c 10 237 +# $ chmod 600 /dev/loop-control +# $ chown root:root /dev/loop-control # # For cleanup: -# $ sudo find /dev -maxdepth 1 -type b -name 'loop[0-9]*' -exec rm -f {} \; +# $ find /dev -maxdepth 1 -type b -name 'loop[0-9]*' -exec rm -f {} \; +if [ "$(id -u)" -ne 0 ]; then + echo "Run as root. We need permissions to write and setup a loop device." + exit 1 +fi DISKFILE="/tmp/disk.img" BINFILE="/tmp/file.bin" @@ -22,9 +26,9 @@ echo "Using device: ${LOOPDEV}" cleanup() { echo "Cleaning up..." set +e -x - sudo losetup -d ${LOOPDEV} - sudo rm ${LOOPDEV} - sudo rm ${BINFILE} ${DISKFILE} + losetup -d ${LOOPDEV} + rm ${LOOPDEV} + rm ${BINFILE} ${DISKFILE} } trap cleanup EXIT INT TERM @@ -36,28 +40,31 @@ if losetup ${LOOPDEV} >/dev/null 2>&1; then fi if [ ! -f ${DISKFILE} ]; then + echo "Creating ${DISKFILE}" dd if=/dev/zero of=${DISKFILE} bs=1M count=256 fi if [ ! -f ${BINFILE} ]; then + echo "Creating ${BINFILE}" dd if=/dev/urandom of=${BINFILE} bs=1M count=64 fi if [ ! -e ${LOOPDEV} ]; then - sudo losetup ${LOOPDEV} ${DISKFILE} + mknod ${LOOPDEV} b 7 ${LOOPNUM} # busybox needs this + losetup ${LOOPDEV} ${DISKFILE} fi -sudo ./writeimg -nd ${LOOPDEV} ${BINFILE} -sudo ./writeimg -vnd ${LOOPDEV} ${BINFILE} +./writeimg -nd ${LOOPDEV} ${BINFILE} +./writeimg -vnd ${LOOPDEV} ${BINFILE} -sudo ./writeimg -nd ${LOOPDEV} ./writeimg -sudo ./writeimg -vnd ${LOOPDEV} ./writeimg +./writeimg -nd ${LOOPDEV} ./writeimg +./writeimg -vnd ${LOOPDEV} ./writeimg -sudo ./writeimg -nd ${LOOPDEV} ./LICENSE -sudo ./writeimg -vnd ${LOOPDEV} ./LICENSE +./writeimg -nd ${LOOPDEV} ./LICENSE +./writeimg -vnd ${LOOPDEV} ./LICENSE # Redirect this to avoid confusion -! sudo ./writeimg -vnd ${LOOPDEV} ./crc32.h 2>/dev/null +! ./writeimg -vnd ${LOOPDEV} ./crc32.h 2>/dev/null GREEN="\e[32m" RESET="\e[0m" From 1ca6f8dfdf0f62051839ee46d28038570212352e Mon Sep 17 00:00:00 2001 From: Imbus Date: Sat, 21 Feb 2026 08:06:37 +0100 Subject: [PATCH 31/31] Fix warning in musl related to ioctl Ioctl's in musl take ints, while glibc take unsigned longs. When not using glibc, default to casting the ioctl number to an int. Tests passing. --- writeimg.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/writeimg.c b/writeimg.c index f0139ad..2219acb 100644 --- a/writeimg.c +++ b/writeimg.c @@ -278,7 +278,11 @@ int main(int argc, char *argv[]) { exit(EXIT_FAILURE); } +#ifdef __GLIBC__ if (ioctl(fd, BLKGETSIZE64, &device_size) < 0) { +#else /* With musl, ioctl's take ints, kheaders provide unsigned longs. Passes tests. */ + if (ioctl(fd, (int)BLKGETSIZE64, &device_size) < 0) { +#endif perror("ioctl"); close(fd); exit(EXIT_FAILURE);