Compare commits

..

No commits in common. "master" and "v0.2.2" have entirely different histories.

6 changed files with 31 additions and 283 deletions

View file

@ -21,19 +21,4 @@ clean:
install: writeimg install: writeimg
install -d $(DESTDIR)$(PREFIX)/bin install -d $(DESTDIR)$(PREFIX)/bin
install -d $(DESTDIR)$(PREFIX)/share/man/man1
install -m 0755 $< $(DESTDIR)$(PREFIX)/bin/$< 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

View file

@ -1,17 +1,13 @@
# WriteIMG Image Writer # Simple dd-like image writer with additional security checks.
WriteIMG is a simple dd-like image writer with additional security checks for This tool verifies your image automatically. The block size is currently set to
Linux and soon FreeBSD. It tells you what you're about to do and warns you 1 MiB, which should be enough to avoid trouble.
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 writeimg v0.2.0, Rev. 5323932-dirty
In honor of SwePwnage - the OG disk destroyer In honor of SwePwnage - the OG disk destroyer
Copyright (C) 2026 Imbus, BSD-2-Clause Copyright (C) 2026 Imbus, BSD-2-Clause
Build date: 2026-02-21 Build date: 2026-02-07
Usage: Usage:
writeimg [-v] -d <device> <file.img> writeimg [-v] -d <device> <file.img>
@ -21,18 +17,12 @@ Args:
-v Verify only -v Verify only
-d device Target block device -d device Target block device
-h, --help Print this help message -h, --help Print this help message
-n, --noconfirm Do not ask for permission -n, --noconfirm Do not ask for premission
-V, --version Print version -V, --version Print version
``` ```
We also provide a manpage, which targets OpenBSD-level quality. Incorrect or
outdated information is a bug.
## Testing ## 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 dd if=/dev/zero of=./disk.img bs=1M count=1024
losetup -fP ./disk.img losetup -fP ./disk.img
@ -40,75 +30,6 @@ losetup -a
losedup -d /dev/loop0 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 idea 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 ## Inspiration
See: See:

72
test.sh
View file

@ -1,72 +0,0 @@
#!/usr/env/bin bash
set -e
# set -x # For debugging
# If you ever mess this up:
# $ mknod /dev/loop-control c 10 237
# $ chmod 600 /dev/loop-control
# $ chown root:root /dev/loop-control
#
# For cleanup:
# $ 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"
LOOPNUM=$((RANDOM % 156 + 100))
LOOPDEV="/dev/loop${LOOPNUM}"
echo "Using device: ${LOOPDEV}"
cleanup() {
echo "Cleaning up..."
set +e -x
losetup -d ${LOOPDEV}
rm ${LOOPDEV}
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
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
mknod ${LOOPDEV} b 7 ${LOOPNUM} # busybox needs this
losetup ${LOOPDEV} ${DISKFILE}
fi
./writeimg -nd ${LOOPDEV} ${BINFILE}
./writeimg -vnd ${LOOPDEV} ${BINFILE}
./writeimg -nd ${LOOPDEV} ./writeimg
./writeimg -vnd ${LOOPDEV} ./writeimg
./writeimg -nd ${LOOPDEV} ./LICENSE
./writeimg -vnd ${LOOPDEV} ./LICENSE
# Redirect this to avoid confusion
! ./writeimg -vnd ${LOOPDEV} ./crc32.h 2>/dev/null
GREEN="\e[32m"
RESET="\e[0m"
echo -e "\n\n${GREEN}Looks good!${RESET}"

View file

@ -1,38 +0,0 @@
.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

View file

@ -3,7 +3,6 @@
#include <fcntl.h> #include <fcntl.h>
#include <getopt.h> #include <getopt.h>
#include <libgen.h> #include <libgen.h>
#include <limits.h>
#include <signal.h> #include <signal.h>
#include <stdint.h> #include <stdint.h>
#include <stdio.h> #include <stdio.h>
@ -42,14 +41,8 @@
#define BAR_WIDTH 50 #define BAR_WIDTH 50
void print_progress(int current, int total) { void print_progress(int current, int total) {
static int last = INT_MAX; float fraction = (float)current / total;
float fraction = (float)current / total; int filled = (int)(fraction * BAR_WIDTH);
int filled = (int)(fraction * BAR_WIDTH);
if (filled == last)
return; /* Avoid unnecessary io/flushes */
last = filled;
printf("\r["); printf("\r[");
for (int i = 0; i < BAR_WIDTH; i++) { for (int i = 0; i < BAR_WIDTH; i++) {
@ -72,7 +65,7 @@ const char help[] =
" -v Verify only\n" " -v Verify only\n"
" -d device Target block device\n" " -d device Target block device\n"
" -h, --help Print this help message\n" " -h, --help Print this help message\n"
" -n, --noconfirm Do not ask for permission\n" " -n, --noconfirm Do not ask for premission\n"
" -V, --version Print version\n" " -V, --version Print version\n"
"\0"; "\0";
// clang-format on // clang-format on
@ -80,8 +73,8 @@ const char help[] =
const char copyright[] = "Copyright (C) %s Imbus, BSD-2-Clause\n"; const char copyright[] = "Copyright (C) %s Imbus, BSD-2-Clause\n";
struct write_job { struct write_job {
char *iname; char *filename;
char *oname; char *dev_name;
char *buffer; char *buffer;
char *buffer2; /* For memcmp integrity checks */ char *buffer2; /* For memcmp integrity checks */
size_t bufsize; size_t bufsize;
@ -105,8 +98,8 @@ void int_handler(int signum) {
} }
int perform_write(write_job_t *job) { int perform_write(write_job_t *job) {
int block_fd = open(job->oname, O_RDWR); int block_fd = open(job->dev_name, O_RDWR);
int file_fd = open(job->iname, O_RDONLY); int file_fd = open(job->filename, O_RDONLY);
assert(block_fd >= 0); assert(block_fd >= 0);
assert(file_fd >= 0); assert(file_fd >= 0);
@ -135,7 +128,7 @@ int perform_write(write_job_t *job) {
} }
if (read_bytes < 0) { if (read_bytes < 0) {
fprintf(stderr, "%s: Read error\n", job->iname); fprintf(stderr, "%s: Read error\n", job->filename);
perror("Read"); perror("Read");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
@ -145,7 +138,7 @@ int perform_write(write_job_t *job) {
if (job->flags & WI_WRITE) { if (job->flags & WI_WRITE) {
ssize_t written_bytes = write(block_fd, job->buffer, read_bytes); ssize_t written_bytes = write(block_fd, job->buffer, read_bytes);
if (written_bytes < 0) { if (written_bytes < 0) {
fprintf(stderr, "%s: Write error\n", job->oname); fprintf(stderr, "%s: Write error\n", job->dev_name);
perror("Write"); perror("Write");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
@ -226,13 +219,13 @@ int main(int argc, char *argv[]) {
wjob.flags |= WI_VERIFY; wjob.flags |= WI_VERIFY;
wjob.flags &= ~WI_WRITE; wjob.flags &= ~WI_WRITE;
continue; continue;
case 'd': wjob.oname = optarg; continue; case 'd': wjob.dev_name = optarg; continue;
case 'h': break; case 'h': break;
case 'n': wjob.flags &= ~WI_ASK; continue; case 'n': wjob.flags &= ~WI_ASK; continue;
case 'V': exit(EXIT_SUCCESS); case 'V': exit(EXIT_SUCCESS);
} }
printf("In honor of SwePwnage - the OG disk destroyer\n"); printf("In honor of SwePwnage - the OG disk destroyer\n");
printf(copyright, CR_YEAR); fprintf(stdout, copyright, CR_YEAR);
#ifdef BLDDATE #ifdef BLDDATE
printf("Build date: %s\n", BLDDATE); printf("Build date: %s\n", BLDDATE);
#endif #endif
@ -251,55 +244,39 @@ int main(int argc, char *argv[]) {
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
wjob.iname = argv[0]; wjob.filename = argv[0];
struct stat file_stat = {0}; struct stat file_stat = {0};
if (0 != stat(wjob.iname, &file_stat)) { if (0 != stat(wjob.filename, &file_stat)) {
printf("File does not exist...\n"); printf("File does not exist...\n");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
if (NULL == wjob.oname) { if (NULL == wjob.dev_name) {
printf("You need to specify a device.\n"); printf("You need to specify a device.\n");
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
if (0 != strncmp(wjob.oname, "/dev/", 4)) { if (0 != strncmp(wjob.dev_name, "/dev/", 4)) {
printf("\"%s\" does not appear to be a block device...\n", wjob.oname); printf("\"%s\" does not appear to be a block device...\n", wjob.dev_name);
exit(EXIT_FAILURE); exit(EXIT_FAILURE);
} }
uint64_t device_size; /* Seems to be the cleanest way to check for write perm on a blockdev */
{ int fd = open(wjob.dev_name, O_WRONLY);
/* Seems to be the cleanest way to check for write perm on a blockdev */ if (fd < 0) {
int fd = open(wjob.oname, O_WRONLY); printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.dev_name);
if (fd < 0) { exit(1);
printf("Cannot write to \"%s\", do you have write permissions?\n", wjob.oname);
close(fd);
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);
}
close(fd);
} }
close(fd);
wjob.total_bytes = file_stat.st_size; wjob.total_bytes = file_stat.st_size;
assert(file_stat.st_size >= 0); assert(file_stat.st_size >= 0);
if (wjob.flags & WI_WRITE) if (wjob.flags & WI_WRITE)
printf("Writing \"%s\" (%.1f MiB) to \"%s\" (%.1f MiB)\n", printf("Writing \"%s\" (%.1f MiB) to \"%s\"\n",
basename(wjob.iname), basename(wjob.filename),
BYTES_TO_MIB(wjob.total_bytes), BYTES_TO_MIB(wjob.total_bytes),
wjob.oname, wjob.dev_name);
BYTES_TO_MIB(device_size));
if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) { if ((wjob.flags & WI_ASK) && (wjob.flags & WI_WRITE)) {
printf("Is this okay? (y/N): "); printf("Is this okay? (y/N): ");

View file

@ -1,25 +0,0 @@
_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