From 2dbdaf447cb7b4ce885f20c8719a9a49fee07886 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 17 Nov 2025 19:37:31 +0100 Subject: [PATCH 1/8] Improve wipe I/O throughput with large aligned buffers and optional O_DIRECT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change reworks the pass/verify I/O path and adds optional direct I/O support to reduce syscall overhead and better utilize modern storage performance. pass.c: - Introduce NWIPE_BUFFER_SIZE (default 16 MiB) as a generic scratch buffer size and NWIPE_IO_BLOCKSIZE (default 4 MiB) as the target read/write block size. - Add nwipe_effective_io_blocksize() to compute an effective I/O block size per device: - At least device_stat.st_blksize - Rounded down to a multiple of st_blksize for O_DIRECT compatibility - Never larger than the device size - Add nwipe_alloc_io_buffer(), which allocates I/O buffers using posix_memalign() aligned to the device block size (>= 512 B). This makes the same code safe for both buffered I/O and O_DIRECT. - Rework nwipe_random_pass(): - Use a large, aligned scratch buffer (default 16 MiB) instead of tiny st_blksize-sized buffers. - Generate and write data in large chunks (default 4 MiB) to drastically reduce the number of write() syscalls. - Keep the original PRNG init/read interface and the “PRNG wrote something” sanity check (still checks within the first st_blksize bytes). - Preserve existing error handling, progress accounting and periodic fdatasync() logic. - Rework nwipe_random_verify(): - Use the same large I/O block logic for read/compare. - Generate the expected random stream in large blocks and compare against data read from the device. - Maintain the original semantics for partial reads and error counters. - Rework nwipe_static_pass() and nwipe_static_verify(): - Build large pattern buffers that repeat the user-specified pattern and support a sliding window (w) into the pattern. - Perform writes/reads in large blocks (default 4 MiB) while keeping the pattern alignment consistent via the window offset. - Preserve original behaviour regarding partial I/O, logging and counters. nwipe.c: - Add support for optional direct I/O when NWIPE_USE_DIRECT_IO is defined: - Include and ensure O_DIRECT is available (fallback to 0 on platforms that do not define it). - Open devices with O_RDWR|O_DIRECT, and transparently fall back to O_RDWR if O_DIRECT is not supported (e.g. EINVAL/EOPNOTSUPP). - Enable GNU extensions (e.g. _GNU_SOURCE) so that O_DIRECT is visible on glibc-based systems. Behavioural impact: - The wiping/verification algorithms and patterns are unchanged; only the I/O strategy is modified to use larger, aligned buffers. - The number of read()/write() syscalls per pass is reduced by orders of magnitude (e.g. 4 MiB vs. 4 KiB), which should significantly increase throughput on fast disks/NVMe. - When NWIPE_USE_DIRECT_IO is enabled and supported by the device, the same code path uses direct I/O to avoid unnecessary page cache pollution; when unsupported, behaviour gracefully falls back to buffered I/O. --- src/nwipe.c | 36 ++- src/pass.c | 742 ++++++++++++++++++++++++++++------------------------ 2 files changed, 433 insertions(+), 345 deletions(-) diff --git a/src/nwipe.c b/src/nwipe.c index 55afe0f..e86d700 100644 --- a/src/nwipe.c +++ b/src/nwipe.c @@ -27,6 +27,11 @@ #define _POSIX_SOURCE #endif +/* Enable GNU extensions so that O_DIRECT is visible from . */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE 1 +#endif + #include #include #include @@ -59,6 +64,7 @@ #include "hpa_dco.h" #include "conf.h" #include +#include /* O_DIRECT, O_RDWR, ... */ int terminate_signal; int user_abort; @@ -478,8 +484,34 @@ int main( int argc, char** argv ) /* Initialise the wipe_status flag, -1 = wipe not yet started */ c2[i]->wipe_status = -1; - /* Open the file for reads and writes. */ - c2[i]->device_fd = open( c2[i]->device_name, O_RDWR ); + /* Open the file for reads and writes. Optionally use O_DIRECT. */ + int open_flags = O_RDWR; +#ifdef NWIPE_USE_DIRECT_IO + open_flags |= O_DIRECT; +#endif + + c2[i]->device_fd = open( c2[i]->device_name, open_flags ); + +#ifdef NWIPE_USE_DIRECT_IO + /* If O_DIRECT is not supported (or rejected by the FS), fall back to buffered I/O. */ + if( c2[i]->device_fd < 0 && ( errno == EINVAL || errno == EOPNOTSUPP ) ) + { + nwipe_log( NWIPE_LOG_WARNING, + "O_DIRECT not supported on '%s', retrying without O_DIRECT.", + c2[i]->device_name ); + open_flags &= ~O_DIRECT; + c2[i]->device_fd = open( c2[i]->device_name, open_flags ); + } +#endif + + /* Check the open() result. */ + if( c2[i]->device_fd < 0 ) + { + nwipe_perror( errno, __FUNCTION__, "open" ); + nwipe_log( NWIPE_LOG_WARNING, "Unable to open device '%s'.", c2[i]->device_name ); + c2[i]->select = NWIPE_SELECT_DISABLED; + continue; + } /* Check the open() result. */ if( c2[i]->device_fd < 0 ) diff --git a/src/pass.c b/src/pass.c index fc48ac1..aadab8b 100644 --- a/src/pass.c +++ b/src/pass.c @@ -23,6 +23,10 @@ #define _POSIX_C_SOURCE 200809L #include +#include /* posix_memalign, malloc, free */ +#include /* memset, memcpy, memcmp */ +#include + #include "nwipe.h" #include "context.h" #include "method.h" @@ -32,30 +36,142 @@ #include "logging.h" #include "gui.h" +/* + * Tunable sizes for the wiping / verification I/O path. + * + * NWIPE_BUFFER_SIZE: + * - Size of the generic scratch buffer used by passes. + * - Default is 16 MiB, which is a good compromise between memory usage and + * reducing syscall count. + * + * NWIPE_IO_BLOCKSIZE: + * - Target size of individual read()/write() operations. + * - Default is 4 MiB, so each syscall moves a lot of data instead of only + * 4 KiB, drastically reducing syscall overhead. + * + * Notes: + * - We do NOT depend on O_DIRECT here; all code works fine with normal, + * buffered I/O. + * - But all I/O buffers are allocated aligned to the device block size so + * that the same code also works with O_DIRECT when the device is opened + * with it. + */ +#ifndef NWIPE_BUFFER_SIZE +#define NWIPE_BUFFER_SIZE ( 16 * 1024 * 1024UL ) /* 16 MiB generic buffer */ +#endif + +#ifndef NWIPE_IO_BLOCKSIZE +#define NWIPE_IO_BLOCKSIZE ( 4 * 1024 * 1024UL ) /* 4 MiB I/O block */ +#endif + +/* + * Compute the effective I/O block size for a given device: + * + * - Must be at least the device's reported st_blksize (usually 4 KiB). + * - Starts from NWIPE_IO_BLOCKSIZE (4 MiB by default) and adjusts. + * - Rounded down to a multiple of st_blksize so it is compatible with + * O_DIRECT alignment rules. + * - Never exceeds the device size. + */ +static size_t nwipe_effective_io_blocksize( const nwipe_context_t* c ) +{ + size_t bs = (size_t) c->device_stat.st_blksize; + + if( bs == 0 ) + { + /* Should not happen for normal block devices; use a sane default. */ + bs = 4096; + } + + size_t io_bs = (size_t) NWIPE_IO_BLOCKSIZE; + + if( io_bs < bs ) + { + io_bs = bs; + } + + /* Round down to a multiple of the device block size. */ + if( io_bs % bs != 0 ) + { + io_bs -= ( io_bs % bs ); + } + + if( io_bs == 0 ) + { + io_bs = bs; + } + + if( (u64) io_bs > c->device_size ) + { + io_bs = (size_t) c->device_size; + } + + return io_bs; +} + +/* + * Allocate an I/O buffer aligned to the device block size. + * + * This is done with posix_memalign() so that the buffer can safely be used + * with O_DIRECT if the device was opened with it. The same allocation is also + * perfectly fine for normal buffered I/O. + * + * Parameters: + * c - device context (for block size / logging) + * size - number of bytes to allocate + * clear - if non-zero, the buffer is zeroed after allocation + * label - short description for logging + */ +static void* nwipe_alloc_io_buffer( const nwipe_context_t* c, size_t size, int clear, const char* label ) +{ + size_t align = (size_t) c->device_stat.st_blksize; + if( align < 512 ) + { + /* O_DIRECT usually requires at least 512-byte alignment. */ + align = 512; + } + + void* ptr = NULL; + int rc = posix_memalign( &ptr, align, size ); + if( rc != 0 || ptr == NULL ) + { + nwipe_log( NWIPE_LOG_FATAL, + "%s: posix_memalign failed for %s (size=%zu, align=%zu, rc=%d).", + __FUNCTION__, + label, + size, + align, + rc ); + return NULL; + } + + if( clear ) + { + memset( ptr, 0, size ); + } + + return ptr; +} + +/* + * nwipe_random_verify + * + * Verifies that a random pass was correctly written to the device. + * The PRNG is re-seeded with the stored seed, and the same random byte + * stream is generated again and compared against what is read from disk. + * + * This version uses large I/O blocks (e.g. 4 MiB) instead of tiny + * st_blksize-sized chunks to reduce syscall overhead and speed up verification. + */ int nwipe_random_verify( nwipe_context_t* c ) { - /** - * Verifies that a random pass was correctly written to the device. - * - */ - - /* The result holder. */ int r; - - /* The IO size. */ size_t blocksize; - - /* The result buffer for calls to lseek. */ + size_t io_blocksize; off64_t offset; - - /* The input buffer. */ - char* b; - - /* The pattern buffer that is used to check the input buffer. */ - char* d; - - /* The number of bytes remaining in the pass. */ - u64 z = c->device_size; + char* b; /* input buffer from device */ + char* d; /* pattern buffer generated by PRNG */ + u64 z = c->device_size; /* bytes remaining in this pass */ if( c->prng_seed.s == NULL ) { @@ -69,33 +185,22 @@ int nwipe_random_verify( nwipe_context_t* c ) return -1; } - /* Create the input buffer. */ - b = malloc( c->device_stat.st_blksize ); + io_blocksize = nwipe_effective_io_blocksize( c ); - /* Check the memory allocation. */ + /* Allocate I/O buffers of the chosen block size (aligned for possible O_DIRECT). */ + b = (char*) nwipe_alloc_io_buffer( c, io_blocksize, 0, "random_verify input buffer" ); if( !b ) - { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the input buffer." ); return -1; - } - /* Create the pattern buffer */ - d = malloc( c->device_stat.st_blksize ); - - /* Check the memory allocation. */ + d = (char*) nwipe_alloc_io_buffer( c, io_blocksize, 0, "random_verify pattern buffer" ); if( !d ) { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the pattern buffer." ); free( b ); return -1; } - /* Reset the file pointer. */ + /* Rewind device to the beginning. */ offset = lseek( c->device_fd, 0, SEEK_SET ); - - /* Reset the pass byte counter. */ c->pass_done = 0; if( offset == (off64_t) -1 ) @@ -109,20 +214,15 @@ int nwipe_random_verify( nwipe_context_t* c ) if( offset != 0 ) { - /* This is system insanity. */ nwipe_log( NWIPE_LOG_SANITY, "lseek() returned a bogus offset on '%s'.", c->device_name ); free( b ); free( d ); return -1; } - /* Tell our parent that we are syncing the device. */ + /* Ensure all previous writes are on disk before we verify. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -132,78 +232,77 @@ int nwipe_random_verify( nwipe_context_t* c ) c->fsyncdata_errors++; } - /* Reseed the PRNG. */ + /* Reseed the PRNG so it produces the same stream as during the pass. */ c->prng->init( &c->prng_state, &c->prng_seed ); while( z > 0 ) { - if( c->device_stat.st_blksize <= z ) + if( z >= (u64) io_blocksize ) { - blocksize = c->device_stat.st_blksize; + blocksize = io_blocksize; } else { - /* This is a seatbelt for buggy drivers and programming errors because */ - /* the device size should always be an even multiple of its blocksize. */ - blocksize = z; - nwipe_log( NWIPE_LOG_WARNING, - "%s: The size of '%s' is not a multiple of its block size %i.", - __FUNCTION__, - c->device_name, - c->device_stat.st_blksize ); + blocksize = (size_t) z; + + /* Seatbelt: device size should normally be a multiple of st_blksize. */ + if( (u64) c->device_stat.st_blksize > z ) + { + nwipe_log( NWIPE_LOG_WARNING, + "%s: The size of '%s' is not a multiple of its block size %i.", + __FUNCTION__, + c->device_name, + c->device_stat.st_blksize ); + } } - /* Fill the output buffer with the random pattern. */ + /* Generate expected random data into pattern buffer. */ c->prng->read( &c->prng_state, d, blocksize ); - /* Read the buffer in from the device. */ - r = read( c->device_fd, b, blocksize ); - - /* Check the result. */ + /* Read data from device. */ + r = (int) read( c->device_fd, b, blocksize ); if( r < 0 ) { nwipe_perror( errno, __FUNCTION__, "read" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to read from '%s'.", c->device_name ); + free( b ); + free( d ); return -1; } - /* Check for a partial read. */ - if( r != blocksize ) + if( r != (int) blocksize ) { - /* TODO: Handle a partial read. */ - - /* The number of bytes that were not read. */ - int s = blocksize - r; + /* + * Partial reads are treated as warnings and verification errors. + * We keep the semantics of the original code: increment error + * counter and try to skip forward by the missing amount. + */ + int s = (int) blocksize - r; nwipe_log( NWIPE_LOG_WARNING, "%s: Partial read from '%s', %i bytes short.", __FUNCTION__, c->device_name, s ); - /* Increment the error count. */ c->verify_errors += 1; - /* Bump the file pointer to the next block. */ offset = lseek( c->device_fd, s, SEEK_CUR ); - if( offset == (off64_t) -1 ) { nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to bump the '%s' file offset after a partial read.", c->device_name ); + free( b ); + free( d ); return -1; } + } - } /* partial read */ - - /* Compare buffer contents. */ - if( memcmp( b, d, blocksize ) != 0 ) + /* Compare the bytes we actually read (r) against the generated pattern. */ + if( r > 0 && memcmp( b, d, (size_t) r ) != 0 ) { c->verify_errors += 1; } - /* Decrement the bytes remaining in this pass. */ - z -= r; - - /* Increment the total progress counters. */ + z -= (u64) r; c->pass_done += r; c->round_done += r; @@ -211,44 +310,39 @@ int nwipe_random_verify( nwipe_context_t* c ) } /* while bytes remaining */ - /* Release the buffers. */ free( b ); free( d ); - /* We're done. */ return 0; } /* nwipe_random_verify */ +/* + * nwipe_random_pass + * + * Writes a random pattern to the device using the configured PRNG. + * + * This version uses: + * - a generic buffer (default 16 MiB), zero-initialized to avoid leaking + * previous memory content in case of PRNG bugs, and + * - large write() calls (default 4 MiB per syscall) instead of tiny + * st_blksize-sized writes. + * + * The PRNG interface (init/read) and the integrity check that verifies that + * the PRNG produced non-zero data for the first block are kept intact. + */ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) { - /** - * Writes a random pattern to the device. - * - */ - - /* The result holder. */ int r; - - /* The IO size. */ size_t blocksize; - - /* The result buffer for calls to lseek. */ + size_t io_blocksize; + size_t bufsize; off64_t offset; - - /* The output buffer. */ char* b; + u64 z = c->device_size; /* bytes remaining */ - /* The number of bytes remaining in the pass. */ - u64 z = c->device_size; - - /* Number of writes to do before a fdatasync. */ int syncRate = nwipe_options.sync; - - /* Counter to track when to do a fdatasync. */ int i = 0; - - /* general index counter */ int idx; if( c->prng_seed.s == NULL ) @@ -263,25 +357,27 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) return -1; } - /* Create the initialised output buffer. Initialised because we don't want memory leaks - * to disk in the event of some future undetected bug in a prng or its implementation. */ - b = calloc( c->device_stat.st_blksize, sizeof( char ) ); + /* Select effective I/O block size (e.g. 4 MiB, never smaller than st_blksize). */ + io_blocksize = nwipe_effective_io_blocksize( c ); - /* Check the memory allocation. */ + /* + * Allocate a generic 16 MiB buffer (by default) that is used as the + * scratch area for random data. We will only fill and write "blocksize" + * bytes per iteration, which is at most io_blocksize. + */ + bufsize = (size_t) NWIPE_BUFFER_SIZE; + if( bufsize < io_blocksize ) + bufsize = io_blocksize; + + b = (char*) nwipe_alloc_io_buffer( c, bufsize, 1, "random_pass output buffer" ); if( !b ) - { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the output buffer." ); return -1; - } - /* Seed the PRNG. */ + /* Seed the PRNG for this pass. */ c->prng->init( &c->prng_state, &c->prng_seed ); - /* Reset the file pointer. */ + /* Rewind device. */ offset = lseek( c->device_fd, 0, SEEK_SET ); - - /* Reset the pass byte counter. */ c->pass_done = 0; if( offset == (off64_t) -1 ) @@ -294,7 +390,6 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) if( offset != 0 ) { - /* This is system insanity. */ nwipe_log( NWIPE_LOG_SANITY, "__FUNCTION__: lseek() returned a bogus offset on '%s'.", c->device_name ); free( b ); return -1; @@ -302,30 +397,47 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) while( z > 0 ) { - if( c->device_stat.st_blksize <= z ) + /* + * Use large writes of size "io_blocksize" as long as enough data is + * left. The final iteration may use a smaller block if the device size + * is not an exact multiple. + */ + if( z >= (u64) io_blocksize ) { - blocksize = c->device_stat.st_blksize; + blocksize = io_blocksize; } else { - /* This is a seatbelt for buggy drivers and programming errors because */ - /* the device size should always be an even multiple of its blocksize. */ - blocksize = z; - nwipe_log( NWIPE_LOG_WARNING, - "%s: The size of '%s' is not a multiple of its block size %i.", - __FUNCTION__, - c->device_name, - c->device_stat.st_blksize ); + blocksize = (size_t) z; + + if( (u64) c->device_stat.st_blksize > z ) + { + nwipe_log( NWIPE_LOG_WARNING, + "%s: The size of '%s' is not a multiple of its block size %i.", + __FUNCTION__, + c->device_name, + c->device_stat.st_blksize ); + } } - /* Fill the output buffer with the random pattern. */ + /* Ask the PRNG to fill "blocksize" bytes into the output buffer. */ c->prng->read( &c->prng_state, b, blocksize ); - /* For the first block only, check the prng actually wrote something to the buffer */ + /* + * For the first block only, verify that the PRNG actually wrote + * something non-zero into the buffer. This preserves the original + * sanity check but works even if the I/O block size is larger than + * st_blksize. + */ if( z == c->device_size ) { - idx = c->device_stat.st_blksize - 1; - while( idx > 0 ) + size_t check_len = (size_t) c->device_stat.st_blksize; + if( check_len > blocksize ) + check_len = blocksize; + + idx = (int) check_len - 1; + + while( idx >= 0 ) { if( b[idx] != 0 ) { @@ -334,83 +446,74 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) } idx--; } - if( idx == 0 ) + if( idx < 0 ) { nwipe_log( NWIPE_LOG_FATAL, "ERROR, prng wrote nothing to the buffer" ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } + free( b ); return -1; } } - /* Write the next block out to the device. */ - r = write( c->device_fd, b, blocksize ); + /* Write the generated random data to the device. */ + r = (int) write( c->device_fd, b, blocksize ); - /* Check the result for a fatal error. */ if( r < 0 ) { nwipe_perror( errno, __FUNCTION__, "write" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to read from '%s'.", c->device_name ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + nwipe_log( NWIPE_LOG_FATAL, "Unable to write to '%s'.", c->device_name ); + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } + free( b ); return -1; } - /* Check for a partial write. */ - if( r != blocksize ) + if( r != (int) blocksize ) { - /* TODO: Handle a partial write. */ + /* + * Partial writes are rare on block devices, but they can happen. + * We keep the original behavior: count the missing bytes as + * errors and try to skip forward by that amount. + */ + int s = (int) blocksize - r; - /* The number of bytes that were not written. */ - int s = blocksize - r; - - /* Increment the error count by the number of bytes that were not written. */ c->pass_errors += s; nwipe_log( NWIPE_LOG_WARNING, "Partial write on '%s', %i bytes short.", c->device_name, s ); - /* Bump the file pointer to the next block. */ offset = lseek( c->device_fd, s, SEEK_CUR ); - if( offset == (off64_t) -1 ) { nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to bump the '%s' file offset after a partial write.", c->device_name ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } + free( b ); return -1; } + } - } /* partial write */ - - /* Decrement the bytes remaining in this pass. */ - z -= r; - - /* Increment the total progress counters. */ + z -= (u64) r; c->pass_done += r; c->round_done += r; - /* Perodic Sync */ + /* Periodic fdatasync after 'syncRate' writes, if configured. */ if( syncRate > 0 ) { i++; if( i >= syncRate ) { - /* Tell our parent that we are syncing the device. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -420,7 +523,7 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) nwipe_log( NWIPE_LOG_WARNING, "Wrote %llu bytes on '%s'.", c->pass_done, c->device_name ); c->fsyncdata_errors++; free( b ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } @@ -433,24 +536,19 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) pthread_testcancel(); - /* If statement required so that it does not reset on subsequent passes */ - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + /* Track how much of the device has been successfully erased so far. */ + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } } /* /remaining bytes */ - /* Release the output buffer. */ free( b ); - /* Tell our parent that we are syncing the device. */ + /* Final sync at end of pass. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -458,98 +556,85 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) nwipe_perror( errno, __FUNCTION__, "fdatasync" ); nwipe_log( NWIPE_LOG_WARNING, "Buffer flush failure on '%s'.", c->device_name ); c->fsyncdata_errors++; - if( c->bytes_erased < ( c->device_size - z - blocksize ) ) // How much of the device has been erased? + + /* + * Keep the original semantics: adjust bytes_erased based on the last + * known good block and fail the pass. + */ + if( c->bytes_erased < ( c->device_size - z - blocksize ) ) { c->bytes_erased = c->device_size - z - blocksize; } return -1; } - /* We're done. */ return 0; } /* nwipe_random_pass */ +/* + * nwipe_static_verify + * + * Verifies that a static pattern pass was correctly written to the device. + * + * We pre-build a pattern buffer that repeats the user-chosen pattern and + * then, for each block we read from the device, compare it to the appropriate + * "window" into that pattern buffer. + * + * This version uses large I/O blocks (e.g. 4 MiB) instead of tiny + * st_blksize-sized chunks. + */ int nwipe_static_verify( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) { - /** - * Verifies that a static pass was correctly written to the device. - */ - - /* The result holder. */ int r; - - /* The IO size. */ size_t blocksize; - - /* The result buffer for calls to lseek. */ + size_t io_blocksize; off64_t offset; - - /* The input buffer. */ - char* b; - - /* The pattern buffer that is used to check the input buffer. */ - char* d; - - /* A pointer into the pattern buffer. */ + char* b; /* read buffer */ + char* d; /* pre-built pattern buffer */ char* q; - - /* The pattern buffer window offset. */ - int w = 0; - - /* The number of bytes remaining in the pass. */ + int w = 0; /* window offset into pattern buffer */ u64 z = c->device_size; if( pattern == NULL ) { - /* Caught insanity. */ nwipe_log( NWIPE_LOG_SANITY, "nwipe_static_verify: Null entropy pointer." ); return -1; } if( pattern->length <= 0 ) { - /* Caught insanity. */ nwipe_log( NWIPE_LOG_SANITY, "nwipe_static_verify: The pattern length member is %i.", pattern->length ); return -1; } - /* Create the input buffer. */ - b = malloc( c->device_stat.st_blksize ); + io_blocksize = nwipe_effective_io_blocksize( c ); - /* Check the memory allocation. */ + b = (char*) nwipe_alloc_io_buffer( c, io_blocksize, 0, "static_verify input buffer" ); if( !b ) - { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the input buffer." ); return -1; - } - /* Create the pattern buffer */ - d = malloc( c->device_stat.st_blksize + pattern->length * 2 ); - - /* Check the memory allocation. */ + /* + * Pattern buffer length: + * io_blocksize + pattern->length * 2 + * to ensure we can always take a contiguous window of size <= io_blocksize + * starting at any offset w within [0, pattern->length). + */ + d = (char*) nwipe_alloc_io_buffer( c, io_blocksize + pattern->length * 2, 0, "static_verify pattern buffer" ); if( !d ) { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the pattern buffer." ); free( b ); return -1; } - for( q = d; q < d + c->device_stat.st_blksize + pattern->length; q += pattern->length ) + for( q = d; q < d + io_blocksize + pattern->length; q += pattern->length ) { - /* Fill the pattern buffer with the pattern. */ memcpy( q, pattern->s, pattern->length ); } - /* Tell our parent that we are syncing the device. */ + /* Ensure all writes are flushed before verification. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -559,10 +644,8 @@ int nwipe_static_verify( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) c->fsyncdata_errors++; } - /* Reset the file pointer. */ + /* Rewind. */ offset = lseek( c->device_fd, 0, SEEK_SET ); - - /* Reset the pass byte counter. */ c->pass_done = 0; if( offset == (off64_t) -1 ) @@ -570,12 +653,12 @@ int nwipe_static_verify( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_FATAL, "Unable to reset the '%s' file offset.", c->device_name ); free( b ); + free( d ); return -1; } if( offset != 0 ) { - /* This is system insanity. */ nwipe_log( NWIPE_LOG_SANITY, "nwipe_static_verify: lseek() returned a bogus offset on '%s'.", c->device_name ); free( b ); free( d ); @@ -584,80 +667,73 @@ int nwipe_static_verify( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) while( z > 0 ) { - if( c->device_stat.st_blksize <= z ) + if( z >= (u64) io_blocksize ) { - blocksize = c->device_stat.st_blksize; + blocksize = io_blocksize; } else { - /* This is a seatbelt for buggy drivers and programming errors because */ - /* the device size should always be an even multiple of its blocksize. */ - blocksize = z; - nwipe_log( NWIPE_LOG_WARNING, - "%s: The size of '%s' is not a multiple of its block size %i.", - __FUNCTION__, - c->device_name, - c->device_stat.st_blksize ); + blocksize = (size_t) z; + + if( (u64) c->device_stat.st_blksize > z ) + { + nwipe_log( NWIPE_LOG_WARNING, + "%s: The size of '%s' is not a multiple of its block size %i.", + __FUNCTION__, + c->device_name, + c->device_stat.st_blksize ); + } } - /* Fill the output buffer with the random pattern. */ - /* Read the buffer in from the device. */ - r = read( c->device_fd, b, blocksize ); - - /* Check the result. */ + r = (int) read( c->device_fd, b, blocksize ); if( r < 0 ) { nwipe_perror( errno, __FUNCTION__, "read" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to read from '%s'.", c->device_name ); + free( b ); + free( d ); return -1; } - /* Check for a partial read. */ - if( r == blocksize ) + if( r == (int) blocksize ) { - /* Check every byte in the buffer. */ - if( memcmp( b, &d[w], r ) != 0 ) + /* Compare every byte in the buffer against the current pattern window. */ + if( memcmp( b, &d[w], (size_t) r ) != 0 ) { c->verify_errors += 1; } } else { - /* The number of bytes that were not read. */ - int s = blocksize - r; + int s = (int) blocksize - r; - /* TODO: Handle a partial read. */ - - /* Increment the error count. */ c->verify_errors += 1; nwipe_log( NWIPE_LOG_WARNING, "Partial read on '%s', %i bytes short.", c->device_name, s ); - /* Bump the file pointer to the next block. */ offset = lseek( c->device_fd, s, SEEK_CUR ); - if( offset == (off64_t) -1 ) { nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to bump the '%s' file offset after a partial read.", c->device_name ); + free( b ); + free( d ); return -1; } + } - } /* partial read */ - - /* Adjust the window. */ - w = ( c->device_stat.st_blksize + w ) % pattern->length; - - /* Intuition check: - * If the pattern length evenly divides the block size - * then ( w == 0 ) always. + /* + * Advance the pattern window by r bytes, modulo pattern->length. + * This keeps the pattern alignment in sync with the device offset. */ + if( pattern->length > 0 && r > 0 ) + { + size_t adv = (size_t) r % (size_t) pattern->length; + w = (int) ( ( w + (int) adv ) % pattern->length ); + } - /* Decrement the bytes remaining in this pass. */ - z -= r; - - /* Increment the total progress counters. */ + z -= (u64) r; c->pass_done += r; c->round_done += r; @@ -665,193 +741,178 @@ int nwipe_static_verify( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) } /* while bytes remaining */ - /* Release the buffers. */ free( b ); free( d ); - /* We're done. */ return 0; } /* nwipe_static_verify */ +/* + * nwipe_static_pass + * + * Writes a static pattern to the device. + * + * The pattern is repeated into a large buffer and then written in equally + * large I/O blocks (e.g. 4 MiB). The "window" offset w keeps track of where + * in the repeating pattern we are when moving across the device. + */ int nwipe_static_pass( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) { - /** - * Writes a static pattern to the device. - */ - - /* The result holder. */ int r; - - /* The IO size. */ size_t blocksize; - - /* The result buffer for calls to lseek. */ + size_t io_blocksize; + size_t bufsize; off64_t offset; - - /* The output buffer. */ char* b; - - /* A pointer into the output buffer. */ char* p; - - /* The output buffer window offset. */ - int w = 0; - - /* The number of bytes remaining in the pass. */ + int w = 0; /* window offset into pattern */ u64 z = c->device_size; - /* Number of writes to do before a fdatasync. */ int syncRate = nwipe_options.sync; - - /* Counter to track when to do a fdatasync. */ int i = 0; if( pattern == NULL ) { - /* Caught insanity. */ nwipe_log( NWIPE_LOG_SANITY, "__FUNCTION__: Null pattern pointer." ); return -1; } if( pattern->length <= 0 ) { - /* Caught insanity. */ nwipe_log( NWIPE_LOG_SANITY, "__FUNCTION__: The pattern length member is %i.", pattern->length ); return -1; } - /* Create the output buffer. */ - b = malloc( c->device_stat.st_blksize + pattern->length * 2 ); + io_blocksize = nwipe_effective_io_blocksize( c ); - /* Check the memory allocation. */ + /* + * For static patterns we want enough buffer space to always have a + * contiguous window of "io_blocksize" bytes available starting at any + * offset w in [0, pattern->length). Using: + * + * buffer_size >= io_blocksize + pattern->length * 2 + * + * guarantees that we can wrap around the repeating pattern safely. + * We also honour NWIPE_BUFFER_SIZE if it is larger, to keep the "generic + * 16 MiB buffer" behavior. + */ + bufsize = io_blocksize + pattern->length * 2; + if( bufsize < (size_t) NWIPE_BUFFER_SIZE ) + bufsize = (size_t) NWIPE_BUFFER_SIZE; + + b = (char*) nwipe_alloc_io_buffer( c, bufsize, 0, "static_pass pattern buffer" ); if( !b ) - { - nwipe_perror( errno, __FUNCTION__, "malloc" ); - nwipe_log( NWIPE_LOG_FATAL, "Unable to allocate memory for the pattern buffer." ); return -1; - } - for( p = b; p < b + c->device_stat.st_blksize + pattern->length; p += pattern->length ) + for( p = b; p < b + bufsize; p += pattern->length ) { - /* Fill the output buffer with the pattern. */ memcpy( p, pattern->s, pattern->length ); } - /// - /* Reset the file pointer. */ - offset = lseek( c->device_fd, 0, SEEK_SET ); - /* Reset the pass byte counter. */ + /* Rewind device. */ + offset = lseek( c->device_fd, 0, SEEK_SET ); c->pass_done = 0; if( offset == (off64_t) -1 ) { nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_FATAL, "Unable to reset the '%s' file offset.", c->device_name ); + free( b ); return -1; } if( offset != 0 ) { - /* This is system insanity. */ nwipe_log( NWIPE_LOG_SANITY, "__FUNCTION__: lseek() returned a bogus offset on '%s'.", c->device_name ); + free( b ); return -1; } while( z > 0 ) { - if( c->device_stat.st_blksize <= z ) + if( z >= (u64) io_blocksize ) { - blocksize = c->device_stat.st_blksize; + blocksize = io_blocksize; } else { - /* This is a seatbelt for buggy drivers and programming errors because */ - /* the device size should always be an even multiple of its blocksize. */ - blocksize = z; - nwipe_log( NWIPE_LOG_WARNING, - "%s: The size of '%s' is not a multiple of its block size %i.", - __FUNCTION__, - c->device_name, - c->device_stat.st_blksize ); + blocksize = (size_t) z; + + if( (u64) c->device_stat.st_blksize > z ) + { + nwipe_log( NWIPE_LOG_WARNING, + "%s: The size of '%s' is not a multiple of its block size %i.", + __FUNCTION__, + c->device_name, + c->device_stat.st_blksize ); + } } - /* Fill the output buffer with the random pattern. */ - /* Write the next block out to the device. */ - r = write( c->device_fd, &b[w], blocksize ); - - /* Check the result for a fatal error. */ + /* + * Write "blocksize" bytes starting at offset w in the repeating + * pattern buffer. Because we filled the entire buffer with the + * pattern (and made it large enough), &b[w] is always valid. + */ + r = (int) write( c->device_fd, &b[w], blocksize ); if( r < 0 ) { nwipe_perror( errno, __FUNCTION__, "write" ); nwipe_log( NWIPE_LOG_FATAL, "Unable to write to '%s'.", c->device_name ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } + free( b ); return -1; } - /* Check for a partial write. */ - if( r != blocksize ) + if( r != (int) blocksize ) { - /* TODO: Handle a partial write. */ + int s = (int) blocksize - r; - /* The number of bytes that were not written. */ - int s = blocksize - r; - - /* Increment the error count. */ c->pass_errors += s; nwipe_log( NWIPE_LOG_WARNING, "Partial write on '%s', %i bytes short.", c->device_name, s ); - /* Bump the file pointer to the next block. */ offset = lseek( c->device_fd, s, SEEK_CUR ); - if( offset == (off64_t) -1 ) { nwipe_perror( errno, __FUNCTION__, "lseek" ); nwipe_log( NWIPE_LOG_ERROR, "Unable to bump the '%s' file offset after a partial write.", c->device_name ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } + free( b ); return -1; } + } - } /* partial write */ - - /* Adjust the window. */ - w = ( c->device_stat.st_blksize + w ) % pattern->length; - - /* Intuition check: - * - * If the pattern length evenly divides the block size - * then ( w == 0 ) always. + /* + * Advance the pattern window by r bytes (not blocksize; we use the + * actual number of bytes written) modulo pattern length. */ + if( pattern->length > 0 && r > 0 ) + { + size_t adv = (size_t) r % (size_t) pattern->length; + w = (int) ( ( w + (int) adv ) % pattern->length ); + } - /* Decrement the bytes remaining in this pass. */ - z -= r; - - /* Increment the total progress counterr. */ + z -= (u64) r; c->pass_done += r; c->round_done += r; - /* Perodic Sync */ + /* Periodic sync if requested. */ if( syncRate > 0 ) { i++; if( i >= syncRate ) { - /* Tell our parent that we are syncing the device. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -861,7 +922,7 @@ int nwipe_static_pass( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) nwipe_log( NWIPE_LOG_WARNING, "Wrote %llu bytes on '%s'.", c->pass_done, c->device_name ); c->fsyncdata_errors++; free( b ); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } @@ -874,20 +935,16 @@ int nwipe_static_pass( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) pthread_testcancel(); - if( c->bytes_erased < ( c->device_size - z ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z ) ) { c->bytes_erased = c->device_size - z; } } /* /remaining bytes */ - /* Tell our parent that we are syncing the device. */ + /* Final sync at end of pass. */ c->sync_status = 1; - - /* Sync the device. */ r = fdatasync( c->device_fd ); - - /* Tell our parent that we have finished syncing the device. */ c->sync_status = 0; if( r != 0 ) @@ -895,17 +952,16 @@ int nwipe_static_pass( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) nwipe_perror( errno, __FUNCTION__, "fdatasync" ); nwipe_log( NWIPE_LOG_WARNING, "Buffer flush failure on '%s'.", c->device_name ); c->fsyncdata_errors++; - if( c->bytes_erased < ( c->device_size - z - blocksize ) ) // How much of the device has been erased? + if( c->bytes_erased < ( c->device_size - z - blocksize ) ) { c->bytes_erased = c->device_size - z - blocksize; } + free( b ); return -1; } - /* Release the output buffer. */ free( b ); - /* We're done. */ return 0; } /* nwipe_static_pass */ From 4150dddd84fedd89de37d6ce328f99e6ec7dad5a Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 17 Nov 2025 20:02:54 +0100 Subject: [PATCH 2/8] Added logging for O_DIRECT if devices supports direct i/o --- src/nwipe.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/nwipe.c b/src/nwipe.c index e86d700..293e144 100644 --- a/src/nwipe.c +++ b/src/nwipe.c @@ -493,15 +493,17 @@ int main( int argc, char** argv ) c2[i]->device_fd = open( c2[i]->device_name, open_flags ); #ifdef NWIPE_USE_DIRECT_IO - /* If O_DIRECT is not supported (or rejected by the FS), fall back to buffered I/O. */ if( c2[i]->device_fd < 0 && ( errno == EINVAL || errno == EOPNOTSUPP ) ) { - nwipe_log( NWIPE_LOG_WARNING, - "O_DIRECT not supported on '%s', retrying without O_DIRECT.", - c2[i]->device_name ); + nwipe_log( NWIPE_LOG_WARNING, "O_DIRECT not supported on '%s', retrying without.", c2[i]->device_name ); + open_flags &= ~O_DIRECT; c2[i]->device_fd = open( c2[i]->device_name, open_flags ); } + else if( c2[i]->device_fd >= 0 && ( open_flags & O_DIRECT ) ) + { + nwipe_log( NWIPE_LOG_NOTICE, "Using O_DIRECT on device '%s'.", c2[i]->device_name ); + } #endif /* Check the open() result. */ From 6792a969dc67fde22aff2af40ec53eb36ac25eb4 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Mon, 17 Nov 2025 23:00:38 +0100 Subject: [PATCH 3/8] Set DNWIP_USE_DIRECT_IO to enabled by default --- src/options.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/options.h b/src/options.h index dcf39f9..7b84ad9 100644 --- a/src/options.h +++ b/src/options.h @@ -38,6 +38,7 @@ #define MAX_DRIVE_PATH_LENGTH 200 // e.g. /dev/sda is only 8 characters long, so 200 should be plenty. #define DEFAULT_SYNC_RATE 100000 #define PATHNAME_MAX 2048 +#define NWIPE_USE_DIRECT_IO /* Function prototypes for loading options from the environment and command line. */ int nwipe_options_parse( int argc, char** argv ); From 066d62352f6ce418f5806baae4f8e46d412b14d4 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 21 Nov 2025 21:31:49 +0100 Subject: [PATCH 4/8] Add runtime I/O mode selection (auto/direct/cached) for direct I/O support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change extends the recently-added large-block, aligned I/O path with a user-selectable I/O mode, allowing nwipe to choose between direct I/O (O_DIRECT) and kernel cached I/O at runtime. The goal is to: - Make it easy to benchmark and compare cached vs. direct I/O. - Provide an operational escape-hatch if Direct I/O causes issues on some systems or devices. - Keep a sensible default that "does the right thing" automatically. Summary of changes *options.h / options.c* - Introduce a new enum `nwipe_io_mode_t`: - `NWIPE_IO_MODE_AUTO` (default) Try to use `O_DIRECT` for device access. If the kernel rejects `O_DIRECT` with `EINVAL` or `EOPNOTSUPP`, nwipe automatically falls back to cached I/O and logs a warning. - `NWIPE_IO_MODE_DIRECT` Force direct I/O. Devices are opened with `O_DIRECT` and there is no fallback. If `O_DIRECT` is not supported for a device, it is treated as a fatal condition and the device is marked disabled. - `NWIPE_IO_MODE_CACHED` Force kernel cached I/O. Devices are always opened without `O_DIRECT` and no attempt is made to use direct I/O. - Extend `nwipe_options_t` with a new field: - `nwipe_io_mode_t io_mode;` and initialize it to `NWIPE_IO_MODE_AUTO` in the default options setup so that existing usage (no new flags) preserves the current behaviour. - Add new command-line options to control the I/O mode: - `--directio` Sets `io_mode = NWIPE_IO_MODE_DIRECT`. This explicitly requests `O_DIRECT` and disables the auto-fallback. - `--cachedio` Sets `io_mode = NWIPE_IO_MODE_CACHED`. This disables `O_DIRECT` completely and forces classic cached I/O. - `--io-mode=MODE` Accepts `auto`, `direct`, or `cached` and sets `io_mode` accordingly. Any other value results in a clear error message: `Error: Unknown I/O mode 'X' (expected auto|direct|cached).` - Integrate the new options into the existing long-option parsing logic in `options.c` (`case 0:`), ensuring that: - `--directio` and `--cachedio` are handled alongside other long options (method, prng, verify, etc.). - The previous unconditional `exit(EINVAL)` placeholder at the end of `case 0` is moved to the end of the chain so that known options (`directio`, `cachedio`, `io-mode`) are parsed correctly. - Unknown long options still terminate with a clear error instead of silently being ignored. - Update `display_help()` to document the new flags: - `--directio` Force direct I/O (O_DIRECT); fail if not supported - `--cachedio` Force kernel cached I/O; never attempt O_DIRECT - `--io-mode=MODE` I/O mode: auto (default), direct, cached *nwipe.c* - Update the device open path to honour `nwipe_options.io_mode` whenever `NWIPE_USE_DIRECT_IO` is enabled at build time: - Compute `open_flags` starting from `O_RDWR`. - In `AUTO` and `DIRECT` modes, append `O_DIRECT` to `open_flags`. - In `CACHED` mode, never add `O_DIRECT` (pure cached I/O). - Implement mode-specific handling for `O_DIRECT` failures: - In `AUTO` mode: If `open()` fails with `EINVAL` or `EOPNOTSUPP`, log a warning and retry without `O_DIRECT` (cached I/O). This preserves the previous behaviour of “try direct I/O if available, but keep working if it isn’t.” - In `DIRECT` mode: If `open()` fails with `EINVAL` or `EOPNOTSUPP`, treat this as a fatal condition for that device. We log a clear error stating that `O_DIRECT` was explicitly requested via `--directio` but is not supported, mark the device as disabled, and do not silently fall back. - In `CACHED` mode: Devices are always opened without `O_DIRECT`; no additional logic is required, and behaviour matches classic buffered I/O. - Add informational logging for the chosen I/O mode per device: - On successful open, nwipe logs whether it is using: - `"Using direct I/O (O_DIRECT) on device '...'.` or - `"Using cached I/O on device '...'."` This helps benching and debugging by making the actual mode visible in the logs. - For portability, ensure that builds on non-Linux / non-glibc systems remain possible by defining `O_DIRECT` as `0` if it is not provided by the system headers and `NWIPE_USE_DIRECT_IO` is set. This turns `O_DIRECT` into a no-op flag on such platforms while keeping the API intact. Behavioural impact - The actual wipe patterns, verification behaviour, and large-block aligned I/O path remain unchanged. The new I/O mode only controls how devices are opened (with or without `O_DIRECT`) and how we react if direct I/O is not supported by the kernel or underlying filesystem. - Default behaviour (`AUTO`) continues to “do the right thing”: try direct I/O where available and fall back to kernel cached I/O otherwise, with a clear log message. - Advanced users and testers now have fine-grained control: - `--directio` / `--io-mode=direct` for hard-fail direct I/O, - `--cachedio` / `--io-mode=cached` to force buffered I/O, - `--io-mode=auto` (or no flag) for the previous automatic behaviour. - Combined with the existing large I/O buffers and aligned allocations in `pass.c`, all three modes share the same fast, O_DIRECT-safe I/O implementation. The new options simply toggle whether direct I/O is requested and how strictly that requirement is enforced, which is particularly useful for benchmarking and for diagnosing any potential Direct I/O issues in the field. --- src/nwipe.c | 75 ++++++++++++++++++++++++++++++++++++++++++++------- src/options.c | 45 +++++++++++++++++++++++++++++++ src/options.h | 8 ++++++ 3 files changed, 118 insertions(+), 10 deletions(-) diff --git a/src/nwipe.c b/src/nwipe.c index 293e144..96cc58b 100644 --- a/src/nwipe.c +++ b/src/nwipe.c @@ -66,6 +66,17 @@ #include #include /* O_DIRECT, O_RDWR, ... */ +#ifdef NWIPE_USE_DIRECT_IO +#ifndef O_DIRECT +/* + * Some platforms or libcs do not define O_DIRECT at all. Defining it + * as 0 makes the flag a no-op and keeps the code buildable. + * On Linux/glibc, via nwipe.h will provide a real O_DIRECT. + */ +#define O_DIRECT 0 +#endif +#endif + int terminate_signal; int user_abort; int global_wipe_status; @@ -484,10 +495,20 @@ int main( int argc, char** argv ) /* Initialise the wipe_status flag, -1 = wipe not yet started */ c2[i]->wipe_status = -1; - /* Open the file for reads and writes. Optionally use O_DIRECT. */ + /* Open the file for reads and writes, honoring the configured I/O mode. */ int open_flags = O_RDWR; + #ifdef NWIPE_USE_DIRECT_IO - open_flags |= O_DIRECT; + /* + * Decide whether to request O_DIRECT based on the runtime I/O mode: + * auto -> try O_DIRECT, fall back to cached I/O if needed + * direct -> force O_DIRECT, fail hard if not supported + * cached -> do not request O_DIRECT at all + */ + if( nwipe_options.io_mode == NWIPE_IO_MODE_DIRECT || nwipe_options.io_mode == NWIPE_IO_MODE_AUTO ) + { + open_flags |= O_DIRECT; + } #endif c2[i]->device_fd = open( c2[i]->device_name, open_flags ); @@ -495,18 +516,52 @@ int main( int argc, char** argv ) #ifdef NWIPE_USE_DIRECT_IO if( c2[i]->device_fd < 0 && ( errno == EINVAL || errno == EOPNOTSUPP ) ) { - nwipe_log( NWIPE_LOG_WARNING, "O_DIRECT not supported on '%s', retrying without.", c2[i]->device_name ); + if( nwipe_options.io_mode == NWIPE_IO_MODE_DIRECT ) + { + /* + * User explicitly requested direct I/O: do not silently + * fall back. Mark the device as unusable and continue. + */ + nwipe_perror( errno, __FUNCTION__, "open" ); + nwipe_log( NWIPE_LOG_FATAL, + "O_DIRECT requested via --directio but not supported on '%s'.", + c2[i]->device_name ); + c2[i]->select = NWIPE_SELECT_DISABLED; + continue; + } + else if( nwipe_options.io_mode == NWIPE_IO_MODE_AUTO ) + { + /* + * Auto mode: transparently fall back to cached I/O and + * log a warning. + */ + nwipe_log( NWIPE_LOG_WARNING, + "O_DIRECT not supported on '%s', falling back to cached I/O.", + c2[i]->device_name ); - open_flags &= ~O_DIRECT; - c2[i]->device_fd = open( c2[i]->device_name, open_flags ); + open_flags &= ~O_DIRECT; + c2[i]->device_fd = open( c2[i]->device_name, open_flags ); + } } - else if( c2[i]->device_fd >= 0 && ( open_flags & O_DIRECT ) ) + + if( c2[i]->device_fd >= 0 ) { - nwipe_log( NWIPE_LOG_NOTICE, "Using O_DIRECT on device '%s'.", c2[i]->device_name ); - } -#endif + const char* io_desc; - /* Check the open() result. */ + if( open_flags & O_DIRECT ) + { + io_desc = "direct I/O (O_DIRECT)"; + } + else + { + io_desc = "cached I/O"; + } + + nwipe_log( NWIPE_LOG_NOTICE, "Using %s on device '%s'.", io_desc, c2[i]->device_name ); + } +#endif /* NWIPE_USE_DIRECT_IO */ + + /* Check the open() result (after any fallback logic). */ if( c2[i]->device_fd < 0 ) { nwipe_perror( errno, __FUNCTION__, "open" ); diff --git a/src/options.c b/src/options.c index 7d61c74..7190cd3 100644 --- a/src/options.c +++ b/src/options.c @@ -117,6 +117,11 @@ int nwipe_options_parse( int argc, char** argv ) /* Verify that wipe patterns are being written to the device. */ { "verify", required_argument, 0, 0 }, + /* I/O mode selection: auto/direct/cached. */ + { "directio", no_argument, 0, 0 }, + { "cachedio", no_argument, 0, 0 }, + { "io-mode", required_argument, 0, 0 }, + /* Display program version. */ { "verbose", no_argument, 0, 'v' }, @@ -142,6 +147,7 @@ int nwipe_options_parse( int argc, char** argv ) nwipe_options.sync = DEFAULT_SYNC_RATE; nwipe_options.verbose = 0; nwipe_options.verify = NWIPE_VERIFY_LAST; + nwipe_options.io_mode = NWIPE_IO_MODE_AUTO; /* Default: auto-select I/O mode. */ memset( nwipe_options.logfile, '\0', sizeof( nwipe_options.logfile ) ); memset( nwipe_options.PDFreportpath, '\0', sizeof( nwipe_options.PDFreportpath ) ); strncpy( nwipe_options.PDFreportpath, ".", 2 ); @@ -321,6 +327,42 @@ int nwipe_options_parse( int argc, char** argv ) exit( EINVAL ); } + /* I/O mode selection options. */ + + if( strcmp( nwipe_options_long[i].name, "directio" ) == 0 ) + { + nwipe_options.io_mode = NWIPE_IO_MODE_DIRECT; + break; + } + + if( strcmp( nwipe_options_long[i].name, "cachedio" ) == 0 ) + { + nwipe_options.io_mode = NWIPE_IO_MODE_CACHED; + break; + } + + if( strcmp( nwipe_options_long[i].name, "io-mode" ) == 0 ) + { + if( strcmp( optarg, "auto" ) == 0 ) + { + nwipe_options.io_mode = NWIPE_IO_MODE_AUTO; + } + else if( strcmp( optarg, "direct" ) == 0 ) + { + nwipe_options.io_mode = NWIPE_IO_MODE_DIRECT; + } + else if( strcmp( optarg, "cached" ) == 0 ) + { + nwipe_options.io_mode = NWIPE_IO_MODE_CACHED; + } + else + { + fprintf( stderr, "Error: Unknown I/O mode '%s' (expected auto|direct|cached).\n", optarg ); + exit( EINVAL ); + } + break; + } + /* getopt_long should raise on invalid option, so we should never get here. */ exit( EINVAL ); @@ -702,6 +744,9 @@ void display_help() puts( " last - Verify after the last pass" ); puts( " all - Verify every pass" ); puts( " " ); + puts( " --directio Force direct I/O (O_DIRECT); fail if not supported" ); + puts( " --cachedio Force kernel cached I/O; never attempt O_DIRECT" ); + puts( " --io-mode=MODE I/O mode: auto (default), direct, cached\n" ); puts( " Please mind that HMG IS5 enhanced always verifies the" ); puts( " last (PRNG) pass regardless of this option.\n" ); puts( " -m, --method=METHOD The wiping method. See man page for more details." ); diff --git a/src/options.h b/src/options.h index 7b84ad9..0af3d34 100644 --- a/src/options.h +++ b/src/options.h @@ -47,6 +47,13 @@ void nwipe_options_log( void ); /* Function to display help text */ void display_help(); +/* I/O mode for data path: auto, direct, or cached. */ +typedef enum { + NWIPE_IO_MODE_AUTO = 0, /* Try O_DIRECT, fall back to cached I/O if not supported. */ + NWIPE_IO_MODE_DIRECT, /* Force O_DIRECT, fail if not supported. */ + NWIPE_IO_MODE_CACHED /* Force cached I/O, never attempt O_DIRECT. */ +} nwipe_io_mode_t; + typedef struct { int autonuke; // Do not prompt the user for confirmation when set. @@ -69,6 +76,7 @@ typedef struct int PDF_enable; // 0=PDF creation disabled, 1=PDF creation enabled int PDF_preview_details; // 0=Disable preview Org/Cust/date/time before drive selection, 1=Enable Preview nwipe_verify_t verify; // A flag to indicate whether writes should be verified. + nwipe_io_mode_t io_mode; // Runtime I/O mode selection (auto/direct/cached). } nwipe_options_t; extern nwipe_options_t nwipe_options; From d97c8a1014c317389862a1a80ab02245bcdf649f Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 27 Nov 2025 10:23:31 +0100 Subject: [PATCH 5/8] Fix sync interval regression after large-block rewrite; restore byte-based sync semantics and disable periodic sync for direct I/O MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After migrating nwipe to large aligned write buffers (multi-MB blocks), the existing `sync` option unintentionally changed behaviour. The original logic performed an fdatasync() every `sync * device_block_size` bytes, which for the default `sync = 100000` resulted in ~50–400 MB between syncs. With the new 4 MB I/O blocks, the same value produced syncs only every ~390 GB, causing extremely delayed I/O error detection in cached I/O mode (errors appear at fsync time, not on write). This was observed during testing on USB HDDs, where no sync occurred even after several percent of the wipe. This commit resolves the issue by: - Restoring the original “bytes between syncs” behaviour. The effective sync interval is recalculated based on the new large block size so that fdatasync() again occurs every few hundred megabytes, not hundreds of gigabytes. - Disabling periodic sync entirely when direct I/O (`O_DIRECT`) is forced. Direct I/O returns hardware errors at write() time, so syncs are unnecessary and provide no safety benefit. - Keeping cached I/O safe by ensuring timely detection of device failures, stalled writeback caches, USB disconnects, and similar hardware conditions. The large-block write path remains unchanged; only the scheduling of sync operations is corrected to maintain practical error detection behaviour consistent with the original nwipe design. --- src/pass.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/pass.c b/src/pass.c index aadab8b..1869a21 100644 --- a/src/pass.c +++ b/src/pass.c @@ -342,6 +342,32 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) u64 z = c->device_size; /* bytes remaining */ int syncRate = nwipe_options.sync; + + /* For direct I/O we do not need periodic fdatasync(), I/O errors are detected + * at write() time. Keep sync for cached I/O only. */ + if( nwipe_options.io_mode == NWIPE_IO_MODE_DIRECT ) + { + syncRate = 0; + } + + /* Preserve the original "bytes between syncs" behaviour: + * previously: sync writes every `sync` * st_blksize bytes. + * now that we use large io_blocksize, adjust syncRate accordingly. */ + if( syncRate > 0 ) + { + unsigned long long bytes_between_sync = + (unsigned long long) syncRate * (unsigned long long) c->device_stat.st_blksize; + + if( bytes_between_sync > 0 && io_blocksize > 0 ) + { + syncRate = (int) ( bytes_between_sync / io_blocksize ); + if( syncRate < 1 ) + { + syncRate = 1; + } + } + } + int i = 0; int idx; From 259ee26273de43ecccbe25e51d800089641982b5 Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Thu, 27 Nov 2025 23:02:28 +0100 Subject: [PATCH 6/8] - Fix uninitialized use of `io_blocksize` in `nwipe_random_pass()` by computing the effective I/O block size before any sync-rate logic is executed. - Add new helper function `nwipe_compute_sync_rate_for_device()` to `pass.c`, converting legacy `--sync` semantics (sync * st_blksize) into a per-write sync interval based on the actual `io_blocksize`, and disabling periodic syncing when using direct I/O. - Update both `nwipe_random_pass()` and `nwipe_static_pass()` to use the new helper, ensuring consistent and correct sync behaviour for all cached-I/O passes and removing duplicated sync-calculation logic. --- src/pass.c | 65 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 17 deletions(-) diff --git a/src/pass.c b/src/pass.c index 1869a21..40bca67 100644 --- a/src/pass.c +++ b/src/pass.c @@ -26,6 +26,7 @@ #include /* posix_memalign, malloc, free */ #include /* memset, memcpy, memcmp */ #include +#include #include "nwipe.h" #include "context.h" @@ -153,6 +154,48 @@ static void* nwipe_alloc_io_buffer( const nwipe_context_t* c, size_t size, int c return ptr; } +/* + * Compute the per-write sync rate for a given device and I/O block size. + * + * Historically, --sync=N meant "fdatasync() every N * st_blksize bytes". + * Now that we use large I/O blocks, we convert that into "sync every K writes", + * where each write is of size io_blocksize. + * + * For O_DIRECT we return 0 because write() already reports I/O errors directly. + */ +static int nwipe_compute_sync_rate_for_device( const nwipe_context_t* c, size_t io_blocksize ) +{ + int syncRate = nwipe_options.sync; + + /* No periodic sync in direct I/O mode. */ + if( nwipe_options.io_mode == NWIPE_IO_MODE_DIRECT ) + return 0; + + if( syncRate <= 0 ) + return 0; + + if( io_blocksize == 0 ) + return 0; + + /* Old semantics: bytes between syncs = sync * st_blksize. */ + unsigned long long bytes_between_sync = + (unsigned long long) syncRate * (unsigned long long) c->device_stat.st_blksize; + + if( bytes_between_sync == 0 ) + return 0; + + /* Convert to "writes between syncs". */ + unsigned long long tmp = bytes_between_sync / (unsigned long long) io_blocksize; + + if( tmp == 0 ) + return 1; /* at least every write */ + + if( tmp > (unsigned long long) INT_MAX ) + return INT_MAX; /* just in case */ + + return (int) tmp; +} + /* * nwipe_random_verify * @@ -350,23 +393,8 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) syncRate = 0; } - /* Preserve the original "bytes between syncs" behaviour: - * previously: sync writes every `sync` * st_blksize bytes. - * now that we use large io_blocksize, adjust syncRate accordingly. */ - if( syncRate > 0 ) - { - unsigned long long bytes_between_sync = - (unsigned long long) syncRate * (unsigned long long) c->device_stat.st_blksize; - - if( bytes_between_sync > 0 && io_blocksize > 0 ) - { - syncRate = (int) ( bytes_between_sync / io_blocksize ); - if( syncRate < 1 ) - { - syncRate = 1; - } - } - } + /* Compute the per-write sync rate based on io_blocksize and old semantics. */ + syncRate = nwipe_compute_sync_rate_for_device( c, io_blocksize ); int i = 0; int idx; @@ -812,6 +840,9 @@ int nwipe_static_pass( NWIPE_METHOD_SIGNATURE, nwipe_pattern_t* pattern ) io_blocksize = nwipe_effective_io_blocksize( c ); + /* Compute per-write sync rate (same semantics as random pass). */ + syncRate = nwipe_compute_sync_rate_for_device( c, io_blocksize ); + /* * For static patterns we want enough buffer space to always have a * contiguous window of "io_blocksize" bytes available starting at any From b79530c292f4fe9484751c54df86fd337a858cbe Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Fri, 28 Nov 2025 21:38:53 +0100 Subject: [PATCH 7/8] Fixed uninitialized io_blocksize in random pass --- src/pass.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pass.c b/src/pass.c index 40bca67..2f0000b 100644 --- a/src/pass.c +++ b/src/pass.c @@ -393,6 +393,9 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) syncRate = 0; } + /* Select effective I/O block size (e.g. 4 MiB, never smaller than st_blksize). */ + io_blocksize = nwipe_effective_io_blocksize( c ); + /* Compute the per-write sync rate based on io_blocksize and old semantics. */ syncRate = nwipe_compute_sync_rate_for_device( c, io_blocksize ); @@ -411,9 +414,6 @@ int nwipe_random_pass( NWIPE_METHOD_SIGNATURE ) return -1; } - /* Select effective I/O block size (e.g. 4 MiB, never smaller than st_blksize). */ - io_blocksize = nwipe_effective_io_blocksize( c ); - /* * Allocate a generic 16 MiB buffer (by default) that is used as the * scratch area for random data. We will only fill and write "blocksize" From 57e337537d836c2df496cfb2c71a46580e73a1bb Mon Sep 17 00:00:00 2001 From: Fabian Druschke Date: Sat, 29 Nov 2025 21:00:27 +0100 Subject: [PATCH 8/8] Fixed formatting --- src/options.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/options.c b/src/options.c index a65f7af..f47ac9a 100644 --- a/src/options.c +++ b/src/options.c @@ -460,7 +460,7 @@ int nwipe_options_parse( int argc, char** argv ) fprintf( stderr, "Error: Unknown I/O mode '%s' (expected auto|direct|cached).\n", optarg ); exit( EINVAL ); } - } + } if( strcmp( nwipe_options_long[i].name, "pdftag" ) == 0 ) { nwipe_options.PDFtag = 1;