How to simulate I/O errors when writing integration tests for a binary crate?

Before reaching for anything more complicated, make sure you've written all the tests you can with /dev/zero, /dev/null, and /dev/full!

...and that is actually what I did :grin:

But to simulate a File::read() error, we need a file that can be open()'ed successfully, but that causes any subsequentread() operation to fail. This does not work with /dev/null, because even though each read() will return zero bytes, the read()'s do not actually fail. It appears that Plan9 has a special file /dev/mordor, which does exactly that ("one does not simply read from mordor"), but this one does not exist in Linux!

As said before, the closest thing to the desired behavior that exists in a standard Linux appears to be /proc/self/mem, because file offset 0 (i.e., memory address 0x0) is inaccessible :thinking:

Furthermore, AFAIK, there is no way to simulate a ReadDir::next() error with what is readily available on a standard Linux, so using the LD_PRELOAD trick seems to be the way to go...

If FUSE is available, then you can launch a special filesystem and test against it.

You can write a FUSE server in rust: fuser crate
So for each test you can mount a custom filesystem with custom fault-injection behavior.

What would the advantage of FUSE be over LD_PRELOAD? They end up having similar APIs to spoof, so the developer experience would be similar (correct me if I'm wrong, I've never written a full FUSE filesystem).

You don't need to bother with building and injecting dynamic libraries and then communicating with them to do the injection. With FUSE you can write normal test code, spawn a helper thread that manages the filesystem and tear it down at the end of the test.
And FUSE also works with libraries using other mechanisms than the libc functions to talk to the filesystem, e.g. io_uring, newer syscalls that aren't covered by the interception, direct syscalls, etc.

You can simulate almost any IO failure by opening a file or directory from a file system and subsequently force-unmounting that file system. For instance, you can setup an SSHFS mount to your own localhost, open a directory on that mount, kill the SSH process and continue reading from that directory.