How to Fix: write success, but not expected.
A successful write() call does not guarantee the data landed where your test expected. In this class of bugs, the syscall reports success because bytes were accepted by the kernel, but the file descriptor state, open flags, offset, or target object do not match the assumption made by the test case.
Table of Contents
When a C test involving write, pwrite, writev, or duplicated file descriptors says “write success, but not expected,” the real problem is usually not the return value itself. It is the mismatch between what was written, where it was written, and how the test verifies the result.
Understanding the Root Cause
At the syscall layer, write() returning a positive number only means the kernel accepted that many bytes for the provided file descriptor. It does not automatically mean:
- the bytes were written at the offset your test expected,
- the destination file descriptor referenced the intended file,
- the file was opened with the expected flags,
- the test read back from the correct position, or
- buffered I/O and raw file descriptor I/O stayed in sync.
This issue commonly appears in test cases using open(), dup(), fork(), writev(), or mixed stdio and low-level syscalls. The most common root causes are:
- Shared file offset: duplicated descriptors created by dup() or inherited across fork() often share the same underlying file description. A write from one descriptor moves the offset seen by the other.
- O_APPEND behavior: if the file is opened with O_APPEND, every write goes to the end of file, regardless of the current offset.
- Wrong verification logic: the test checks from offset 0 without calling lseek(), even though the current offset already moved.
- Short writes: a positive return value may be smaller than the requested byte count, especially with pipes, sockets, or interrupted operations.
- Buffered vs unbuffered mismatch: mixing fprintf()/fwrite() with write() on the same file without careful flushing causes confusing results.
- Unexpected target: the descriptor may point to a different file than expected because of reopen logic, truncation, symbolic links, or descriptor reuse.
So the bug is rarely “write succeeded incorrectly.” More often, the bug is “the test assumed file descriptor semantics that are not true.”
Step-by-Step Solution
Use this checklist to make the test deterministic and align it with actual POSIX behavior.
1. Verify the exact file open flags
Make sure the descriptor is opened the way the test intends. If you need deterministic writes at specific offsets, avoid accidental O_APPEND.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int get_fd(const char *filename, int flags) {
int fd = open(filename, flags, 0644);
if (fd < 0) {
perror("open");
exit(EXIT_FAILURE);
}
return fd;
}
If the issue is caused by append mode, switch from:
int fd = get_fd("test.txt", O_WRONLY | O_CREAT | O_APPEND);
to:
int fd = get_fd("test.txt", O_WRONLY | O_CREAT | O_TRUNC);
2. Reset or control the file offset explicitly
If the test writes and then reads, do not assume the offset is still where you want it.
if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
perror("lseek");
exit(EXIT_FAILURE);
}
For fixed-position writes that must ignore the shared current offset, prefer pwrite():
ssize_t n = pwrite(fd, "ABC", 3, 0);
if (n != 3) {
perror("pwrite");
exit(EXIT_FAILURE);
}
This avoids offset-related surprises entirely.
3. Handle partial writes correctly
Never assume one write() call writes the full buffer. Loop until everything is written.
ssize_t write_all(int fd, const void *buf, size_t len) {
const char *p = (const char *)buf;
size_t total = 0;
while (total < len) {
ssize_t n = write(fd, p + total, len - total);
if (n < 0) {
perror("write");
return -1;
}
total += (size_t)n;
}
return (ssize_t)total;
}
Then use it like this:
if (write_all(fd, "hello", 5) != 5) {
exit(EXIT_FAILURE);
}
4. Do not mix stdio and raw descriptors carelessly
If one part of the test uses FILE * and another uses write(), flush first and keep ownership clear.
FILE *fp = fdopen(fd, "w+");
if (!fp) {
perror("fdopen");
exit(EXIT_FAILURE);
}
fprintf(fp, "hello");
fflush(fp);
if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
perror("lseek");
exit(EXIT_FAILURE);
}
Better still, use either stdio or syscalls consistently in the same test.
5. Confirm whether descriptors share state
If the test duplicates a descriptor, both handles may reference the same open file description.
int fd1 = get_fd("test.txt", O_RDWR | O_CREAT | O_TRUNC);
int fd2 = dup(fd1);
write(fd1, "ABC", 3);
write(fd2, "DEF", 3);
The resulting file is typically ABCDEF, not two independent writes at offset 0, because fd1 and fd2 share the file offset.
If independent offsets are required, open the file twice instead of using dup():
int fd1 = get_fd("test.txt", O_RDWR | O_CREAT | O_TRUNC);
int fd2 = get_fd("test.txt", O_RDWR);
6. Read back with deterministic validation
After writing, seek to the intended location and verify the exact bytes.
char buf[16] = {0};
if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
perror("lseek");
exit(EXIT_FAILURE);
}
ssize_t r = read(fd, buf, sizeof(buf) - 1);
if (r < 0) {
perror("read");
exit(EXIT_FAILURE);
}
printf("content: %s\n", buf);
7. Use a corrected minimal pattern
This example avoids the most common causes of “write success, but not expected.”
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
int main(void) {
const char *file = "test.txt";
int fd = open(file, O_RDWR | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
const char *msg = "hello";
ssize_t n = write(fd, msg, strlen(msg));
if (n != (ssize_t)strlen(msg)) {
perror("write");
close(fd);
return 1;
}
if (lseek(fd, 0, SEEK_SET) == (off_t)-1) {
perror("lseek");
close(fd);
return 1;
}
char buf[32] = {0};
ssize_t r = read(fd, buf, sizeof(buf) - 1);
if (r < 0) {
perror("read");
close(fd);
return 1;
}
printf("read back: %s\n", buf);
close(fd);
return 0;
}
If your original test still reports success where failure was expected, inspect whether the test expectation itself conflicts with POSIX descriptor semantics.
Common Edge Cases
- Writing to a pipe or socket: partial writes are normal, and success may not mean the full message was sent.
- Signals interrupting I/O: EINTR can interrupt a write, so retry logic may be needed.
- Non-blocking descriptors: with O_NONBLOCK, a write may succeed partially or fail with EAGAIN.
- O_APPEND with concurrent writers: the write succeeds, but file layout may differ from a test that assumes a stable offset.
- Descriptor reuse: after close(), a later open() may reuse the same integer descriptor, confusing debug output.
- Memory-backed or special files: device files, procfs entries, and pseudo-filesystems may accept writes with semantics different from regular files.
- writev() expectations: vectored writes can succeed even if the test incorrectly assumes separate offsets per iovec; all buffers are written as one logical operation.
- Forked processes: parent and child may share descriptor state after fork(), causing offsets to move in ways that surprise the test.
FAQ
Why does write() return success if the file content is not what I expected?
Because success only means the kernel accepted bytes for that descriptor. If the file offset, append mode, shared descriptor state, or readback logic is wrong, the content can differ from what the test expected.
How do I make writes deterministic in tests?
Open files with explicit flags, avoid accidental O_APPEND, use lseek() before validation, and prefer pwrite() when writing to fixed offsets. Also avoid mixing buffered and unbuffered I/O in the same scenario.
Why do two file descriptors affect each other’s write position?
If one descriptor was created with dup() or inherited after fork(), both may reference the same open file description and therefore share a single file offset. Open the file separately if independent offsets are required.
The fix for “write success, but not expected” is almost always to correct the test’s assumptions about descriptor behavior, not to treat a positive write() result as suspicious by itself.