Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
0xdeafbeef opened this issue May 3, 2025 · 4 comments
Labels
A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools A-filesystem Area: `std::fs` T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@0xdeafbeef
Copy link

0xdeafbeef commented May 3, 2025

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

@0xdeafbeef 0xdeafbeef added the C-bug Category: This is a bug. label May 3, 2025
@rustbot rustbot added the needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. label May 3, 2025
@Urgau Urgau added T-libs Relevant to the library team, which will review and decide on the PR/issue. A-filesystem Area: `std::fs` and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels May 3, 2025
@the8472
Copy link
Member

the8472 commented May 3, 2025

According to #31453 this is there to provide consistent (portable) behavior across platforms since not all of them support that.

@0xdeafbeef
Copy link
Author

Can we link rfc in docs + expected combinations or it is not a part of public api contract and can be changed in future?

creation mode read write read-write append read-append
not set (open existing) X X X X X
create X X X X
truncate X X
create and truncate X X
create_new X X X X

https://github.com/rust-lang/rfcs/blob/master/text/1252-open-options.md

@the8472
Copy link
Member

the8472 commented May 3, 2025

However, this restriction is not repeated in the .open() documentation

This... is not a scalable assumption. If one uses N methods one has to at least read N docs, not 1.

Can we link rfc in docs + expected combinations

We could, but I don't particularly see the value, in my experience it's not like a menu where one chooses from different options depending on appetite. I want to do a specific thing and then call the method(s) for that, not any other row in the table. And it's already documented on the methods.

nor is it reflected in error messages

Yes I think this can be improved, we now have a mechanism to encode static error messages for custom IO errors rather than reusing the standard OS messages. Since this is not an error generated by the OS it makes sense to use that.

@0xdeafbeef
Copy link
Author

This... is not a scalable assumption. If one uses N methods one has to at least read N docs, not 1.

open is a central method that returns a Result; you can just state the assumed preconditions in its description.

Anyway, I wouldn’t go digging through the docs just to find out if opening a file with create can actually create the file. That’s counterintuitive and doesn’t line up with any prior experience. And, honestly, users are spoiled by good error messages in Rust - they don’t expect that an error like this is actually coming from the std lib guts :)

I will try to file a pr with a nice error message

@jieyouxu jieyouxu added A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools and removed C-bug Category: This is a bug. labels May 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-docs Area: Documentation for any part of the project, including the compiler, standard library, and tools A-filesystem Area: `std::fs` T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

5 participants