How to Fix: `fd_tell` not working when file is opened with `fdflags::append`

7 min read

`fd_tell` returning the wrong offset on append-opened files is a semantics bug, not just a formatting quirk. When a file is opened with `fdflags::append`, writes must go to the end of the file, but the file descriptor still needs to report a correct current position. If the runtime forces every append write to end-of-file without updating or preserving the logical seek position correctly, `fd_tell` can appear broken even though the write itself succeeds.

This issue commonly appears in WASI implementations when code compiled with wasi-sdk opens a file using append semantics and then relies on `fd_tell` to verify the current offset. The expected behavior is subtle: append mode affects where writes land, but it does not give implementations permission to return an arbitrary cursor value.

Understanding the Root Cause

At the OS and runtime level, append mode means each write is committed at the file’s current end, regardless of the descriptor’s prior offset. On POSIX systems this is typically implemented with `O_APPEND`. In WASI, the equivalent behavior comes from `fdflags::append`.

The bug usually happens when the runtime conflates two separate concepts:

  • Logical file offset: the position associated with the file descriptor and reported by `fd_tell`.
  • Append write target: the actual position chosen at write time, which must be end-of-file.

A correct implementation must preserve both semantics. In practice, broken implementations often do one of the following:

  • Always write at end-of-file but never update the descriptor offset after the write.
  • Return a stale offset that reflects the last explicit seek rather than the post-write position.
  • Treat append descriptors as effectively non-seekable for reporting purposes, even though `fd_tell` still needs to work consistently.

Why this matters: many C libraries, test suites, and portability layers expect that after a successful append write, querying the position via `lseek(…, SEEK_CUR)` or its WASI equivalent will reflect the descriptor’s effective current position. If the WASI host or libc shim fails to synchronize that state, code that checks offsets, retries writes, or mixes append with reads can behave incorrectly.

In short, the root cause is an incorrect implementation of append semantics in the WASI runtime or syscall layer, not a bug in user C code.

Step-by-Step Solution

The fix belongs in the layer implementing `fd_write`, `fd_seek`, and `fd_tell`. The runtime must ensure that append writes are performed at end-of-file and that the descriptor offset observed afterward is correct.

1. Reproduce the bug with a minimal test

Use a focused C test that opens a file in append mode, writes data, and checks the resulting position.

#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    int f = open("tmp/a", O_CREAT | O_WRONLY | O_APPEND, 0644);
    if (f < 0) {
        perror("open");
        return 1;
    }

    if (write(f, "abc", 3) != 3) {
        perror("write");
        return 1;
    }

    off_t pos = lseek(f, 0, SEEK_CUR);
    if (pos < 0) {
        perror("lseek");
        return 1;
    }

    printf("position=%lld\n", (long long)pos);
    close(f);
    return 0;
}

Compile it with your wasi-sdk toolchain and run it in the affected runtime. If append behavior is broken, the write may succeed but the reported offset may remain unchanged or otherwise incorrect.

2. Inspect how append is mapped internally

Find the code path where `O_APPEND` or `fdflags::append` is translated into runtime descriptor state. Verify:

  • The descriptor stores append mode as a flag.
  • Writes on that descriptor do not blindly use the cached offset.
  • The post-write offset is updated to the byte immediately after the appended data.

A flawed pattern often looks like this conceptually:

if (fdflags.append) {
    write_at(end_of_file, buffer);
    return success;
}

write_at(current_offset, buffer);
current_offset += bytes_written;

The problem is that the append branch writes correctly but does not advance the tracked offset.

3. Implement the correct append behavior

The correct logic is:

  1. Resolve the current end-of-file at write time.
  2. Write the bytes there.
  3. Update the descriptor’s current offset to end-of-file + bytes-written.

Pseudocode:

if (fdflags.append) {
    uint64_t eof = file_size(fd);
    size_t written = write_at(fd, eof, iovecs);
    if (written_successfully) {
        fd.current_offset = eof + written;
    }
    return written;
} else {
    size_t written = write_at(fd, fd.current_offset, iovecs);
    if (written_successfully) {
        fd.current_offset += written;
    }
    return written;
}

If your runtime delegates to a host OS that already supports `O_APPEND`, another valid strategy is to rely on the host descriptor and call the host’s tell/seek APIs in a way that reflects the actual file position after write completion. The key requirement is still the same: `fd_tell` must report the correct resulting offset.

4. Make sure `fd_tell` reads authoritative state

If your implementation caches offsets, `fd_tell` must return the synchronized value. If your backend is authoritative, query it directly after append writes or when servicing tell requests.

__wasi_errno_t fd_tell(fd_t fd, __wasi_filesize_t *offset) {
    if (fd.invalid) return ERRNO_BADF;

    *offset = fd.current_offset;
    return ERRNO_SUCCESS;
}

This is only safe if `fd.current_offset` is always updated correctly after append writes, seeks, truncations, and any operation that changes observable file position.

5. Add regression tests

This bug needs regression coverage because append-mode offset bugs are easy to reintroduce during refactors.

Test case checklist:
1. Create file with existing contents.
2. Open with append mode.
3. Call fd_tell before write.
4. Write known bytes.
5. Call fd_tell after write.
6. Verify returned offset equals original file size + bytes written.
7. Reopen and verify file contents were appended, not overwritten.

Also add a variant that performs multiple append writes to ensure the offset advances cumulatively.

6. Validate behavior across libc boundaries

If the issue appears only in C code but not in lower-level tests, inspect the bridge between libc and the runtime. Functions such as `open`, `write`, and `lseek` may be translated through a compatibility layer that mishandles append state.

Good validation strategy:

  • Test raw WASI syscalls if available.
  • Test through libc wrappers.
  • Compare behavior in another known-good WASI runtime.

Common Edge Cases

  • Appending to a non-empty file: This is the most revealing case. Bugs are often hidden when the file starts at size zero.
  • Multiple writes on the same descriptor: The offset may be correct after the first write but stale after the second if only one code path updates cached state.
  • Interleaved seek and append: Even if a process seeks to an earlier location, append writes must still go to end-of-file. After the write, `fd_tell` should reflect the effective post-write offset.
  • Concurrent appenders: If multiple descriptors or processes append to the same file, relying on a cached file size is unsafe unless synchronized. The end-of-file must be determined at write time.
  • Buffered stdio: Using `fopen(…, “a”)` may involve stdio buffering. If the underlying runtime has broken append semantics, the symptom can be harder to diagnose because buffering delays writes.
  • Mixing `pwrite`-style operations with append mode: Some systems define special rules here. Your WASI layer should be explicit about whether positional writes ignore append or reject conflicting usage.
  • Truncation after open: If the file is truncated while an append descriptor remains open, cached offsets and sizes can become invalid unless refreshed.

FAQ

Does append mode mean `fd_tell` should always equal end-of-file?

Not automatically at every instant, but after a successful append write the reported offset should reflect the resulting file position. The critical rule is that append affects where writes occur, and the descriptor state exposed through `fd_tell` must remain coherent.

Is this a wasi-sdk compiler bug?

Usually no. wasi-sdk typically just compiles the program and links the libc/runtime interface. The bug is more commonly in the WASI host runtime, syscall emulation layer, or offset bookkeeping implementation.

Can I work around this in application code?

Yes, but only as a temporary measure. You can explicitly query file size after writes or reopen the file to verify the final position, but that hides the underlying standards-compliance issue. The proper fix is to correct append offset handling in the runtime so `fd_tell` behaves consistently.

For maintainers, the safest long-term resolution is simple: treat append as a write-placement rule, not as an excuse to stop maintaining the descriptor’s current offset. Once that invariant is enforced, `fd_tell` will report the expected value and append-mode behavior will align with developer expectations across WASI and libc layers.

Leave a Reply

Your email address will not be published. Required fields are marked *