Skip to content

OpenOptions::open InvalidInput error for read(true).create(true) is unclear (or check is redundant) #140621

Open
@0xdeafbeef

Description

@0xdeafbeef

Problem

When attempting to open a file using std::fs::OpenOptions with both .read(true) and .create(true) set, but without .write(true) or .append(true), the open() method returns an io::Error with kind: InvalidInput (corresponding to EINVAL / OS error 22 on Linux).

My intention was to create a marker file that indicates the process started, without writing anything into it.

MRE:
playground

use std::fs::OpenOptions;
use std::io;
use std::path::Path;

fn main() -> io::Result<()> {
    let path = Path::new("/tmp/rust_test_marker_file");

    println!("Attempting: OpenOptions::new().read(true).create(true).open(...)");

    // This combination fails
    let file = OpenOptions::new()
        .create(true) // Create if not exists
        // .write(true) // NOTE: Adding this makes it work
        .open(path);

    match file {
        Ok(f) => {
            println!("Success! Opened file: {:?}", f);
            Ok(())
        }
        Err(e) => {
            eprintln!("----------------------------------------");
            eprintln!("Failed to open file!");
            eprintln!("Error: {:?}", e);
            eprintln!("Kind: {:?}", e.kind());
            eprintln!("OS Error: {:?}", e.raw_os_error());
            eprintln!("----------------------------------------");
            Err(e)
        }
    }
}

Output:

Error: Os { code: 22, kind: InvalidInput, message: "Invalid argument" }

Debugging & Context

Running the above code produces Error: Os { code: 22, kind: InvalidInput, message: "Invalid argument" }.

Because this snippet was part of a larger system, I spent considerable time debugging my logic and, eventually, using strace to determine what was wrong.
strace confirmed that the error occurs before any open or openat system call is attempted for the target file, indicating that the validation failure happens within the Rust standard library.

Analysis of the std::fs::sys::unix::fs source reveals a check in OpenOptions::get_creation_mode that explicitly returns EINVAL if create, create_new, or truncate is set without write or append also being set.

Crucially, the underlying Linux syscall does permit this combination, as the following C code demonstrates:
It successfully executes open(path, O_RDONLY | O_CREAT, 0644).

Working C Example:

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/stat.h>
#include <libgen.h>
#include <errno.h>

int main() {
    const char *path = "/tmp/c_test_marker_file";
    char *p = strdup(path);
    if (!p) { perror("strdup"); return 1; }
    char *dir = dirname(p);
    if (dir && strcmp(dir, ".") && strcmp(dir, "/")) {
        if (mkdir(dir, 0777) == -1 && errno != EEXIST) {
            perror("mkdir"); return 1;
        }
    }

    printf("Attempting open(\"%s\", O_RDONLY | O_CREAT, 0644)\n", path);
    int fd = open(path, O_RDONLY | O_CREAT, 0644);
    if (fd == -1) {
        perror("open");
        return 1;
    }
    printf("Opened file with fd: %d\n", fd);

    printf("Calling fsync...\n");
    if (fsync(fd) == -1) {
        perror("fsync");
    } else {
        printf("fsync succeeded.\n");
    }
    
    return 0;
}

Output:

gcc test.c && ./a.out && ls -lah /tmp/c_test_marker_file
Attempting open("/tmp/c_test_marker_file", O_RDONLY | O_CREAT, 0644)
Opened file with fd: 3
Calling fsync...
fsync succeeded.
-rw-r--r-- 1 user user 0 May  3 17:22 /tmp/c_test_marker_file

Similarly, Python's standard file opening modes can create files for reading (with "a+"):

import os

path =  "/tmp/py_test_marker_file"

parent = os.path.dirname(path)

try:
    os.makedirs(parent, exist_ok=True)
except Exception as e:
    print("mkdir failed:", e)
    exit(1)

print(f'Opening "{path}" for reading/creating...')
try:
    f = open(path, "a+")
except Exception as e:
    print("open failed:", e)
    exit(1)

print(f"Opened file: {f}")

print("Calling fsync...")
try:
    f.flush()
    os.fsync(f.fileno())
    print("fsync succeeded.")
except Exception as e:
    print("fsync failed:", e)

Documentation Issue

The current documentation for OpenOptions::open lists potential errors, including:

InvalidInput: Invalid combinations of open options (truncate without write access, no access mode set, etc.).

While the failing case is considered an invalid combination by the Rust standard library, it is not explicitly listed. The phrase "etc." does not make it clear that requesting creation inherently requires requesting write or append access within the OpenOptions builder—even though the underlying OS (like Linux) doesn't enforce this restriction for O_RDONLY | O_CREAT.
This can be surprising for users expecting behavior aligned with OS syscalls.

Note

The .create() method’s documentation does specify that .write(true) or .append(true) must also be set for .create(true) to actually create a file. However, this restriction is not repeated in the .open() documentation, nor is it reflected in error messages. This makes it easy to overlook, leading to confusion and unnecessary debugging.

Suggestion

Please consider updating the documentation for OpenOptions::open and/or the io::ErrorKind::InvalidInput description to explicitly state that setting .create(true), .create_new(true), or .truncate(true) also requires .write(true) or .append(true) to be set. This would make the behavior much less surprising for users expecting alignment with the underlying OS.

Alternatively, is this validation check actually necessary? If not, perhaps it could be relaxed to match OS behavior. At minimum, returning a more specific error message (rather than "Invalid argument") would also help prevent user confusion.

upd

removed read from mre because it is set by default

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-docsArea: Documentation for any part of the project, including the compiler, standard library, and toolsA-filesystemArea: `std::fs`T-libsRelevant to the library team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions