Reconsidering O_CREAT|O_DIRECTORY

7 min read Original article ↗

[LWN subscriber-only content]

Welcome to LWN.net

The following subscription-only content has been made available to you by an LWN subscriber. Thousands of subscribers depend on LWN for the best news from the Linux and free software communities. If you enjoy this article, please consider subscribing to LWN. Thank you for visiting LWN.net!

Linux provides a system call (mkdir()) to create a directory, and a few variants of open() that can open a directory. There is, however, no system call in Linux that can create and open a directory in a single, race-free call. Jori Koolstra has been working on remedying that situation, most recently by repurposing a set of open() flags that currently return an error. There are, however, concerns that show just how hard it can be to create user-space interfaces that do not present traps for application developers.

Creating and opening a directory in a single system call is simpler and more efficient than using two, of course. It also can guard against the possibility that some other process will, between the creation and open steps, replace a directory with something else. Detecting that case is possible on Linux now, but it requires some defensive programming of the type that application developers are not always good at. Thus the desire for a more straightforward way to accomplish that pair of operations.

In March, Koolstra attempted to address this problem with a patch series adding a new system call, mkdirat_fd(), that would return a file descriptor for the newly created directory. That system call was changed to mkdirat2() in a subsequent patch. There were a number of concerns about the implementation, but also about creating a new system call in the first place. Christian Brauner, in particular, thought that this problem was better solved with a modification to how open() handles a couple of existing flags.

Specifically, all of the open() variants support the O_DIRECTORY flag, which is necessary if the application is trying to open a directory rather than a normal file. Also supported is O_CREAT, which instructs open() to create the named file if it does not already exist. It would make sense to interpret the combination of those two flags as a request to create a directory and open it at the same time. Adding this capability to open(), Brauner said, would also make many of the other features of the system call, such as the ability to place restrictions on how the name is resolved, available for free. So, he concluded: "I think here it is pretty clear that O_DIRECTORY|O_CREAT is the right thing to do".

Koolstra duly implemented the new API as an RFC patch set, followed by three more RFC and three non-RFC versions; it was only after the last of those was posted that other developers started to take a serious look at the proposed API, and not all of them were convinced that it was the right approach. Pedro Falcato, in particular, argued that this API would be nearly impossible for applications to use in a portable way, given the number of different ways that O_CREAT|O_DIRECTORY has been interpreted in the past.

That story is, indeed, a bit complicated; LWN covered it in 2023. The ways in which Linux has handled an open() call with that flag combination include:

  • Older kernels would fail if the named file existed, returning ENOTDIR if it is a regular file and EISDIR if it is a directory. If, instead, nothing existed by the given name, open() would create a regular file, which seems unlikely to be what the caller wanted.
  • As of the 5.7 release in 2020, the kernel started returning an error in the last case, while still creating the file, which seemed even less likely to be what the developer was hoping for.
  • Brauner added a patch to 6.4 causing the kernel to fail with EINVAL for that flag combination in all cases.

Add in the fact that other systems supporting POSIX system calls have their own interpretation of that flag combination, Falcato said, and the result is going to be difficult to use properly:

If you're writing something that wants to be portable, or that intends to standardize on something, you have some 5 different behaviors all across the FOSS UNIX landscape, not considering everything else. It's also something that will, FWIW, probably never be included in POSIX because no one can agree on the semantics here.

TL;DR I don't see, given the reasons above, how users are supposed to use this without it being a total minefield.

He suggested that limiting the change to openat2() might be a better approach.

Brauner, though, dismissed the concern, saying: "I don't think any of this is really an argument worth considering". The behavior of that flag combination has been consistent on Linux for years, he said, so it makes sense to make better use of it now. Falcato pointed out that the older LTS kernels never received the 6.4 change and, as a result, do not have consistent behavior, and that user space would still have to perform "some pretty gnarly tests" to use the feature safely. Brauner suggested backporting the 6.4 fix, and added: "Feature testing has always been a pain that we forced onto userspace. I think we should continue with that proud tradition".

Christoph Hellwig disagreed:

We need an interface that is self discoverable on Linux. Any combination of flags that was accepted by previous kernels and gave different results than the new interface do not qualify for that.

He added that it was not possible to rely on a fix being backported to all of the old kernels in use.

Neil Brown suggested adding a new flag, OPENAT2_NEW_COMBINATION, that would cause openat2() to reject any flag combination that is not recognized. Any older kernels will immediately fail a call with that flag; new kernels can use it to enable combinations that were not previously supported, including O_CREAT|O_DIRECTORY. Brauner, though, once again dismissed Hellwig's concern as "a strawman". The fact that he was able to make that flag combination return an error without generating a single regression report, he said, was evidence that there is no risk to adding meaning to it now. Hellwig disagreed, saying that, once the combination is supported, applications will start using it, and they will break on older systems. It is, he said, necessary to add "APIs that don't accidentally do the wrong thing on any old kernel".

Koolstra had a different concern about the openat2() suggestion: seemingly a lot of seccomp() configurations still block openat2(). That could make the new feature unavailable on a lot of systems, reducing its value.

Toward the end of the discussion (so far), Brauner asked Koolstra to send a version with support in openat() and openat2(). He did not say whether the change to open() should remain, but complained that "we're deliberately crippling a useful extension for userspace". The latest version of the series from Koolstra enables that flag combination for all open() variants without any additional checks. Brauner has not yet applied that series, so we do not yet know if it will find its way into the mainline in that form or not.

Maintaining compatibility over the long term is not an easy task. It is hard enough to ensure that new kernels do not break older applications, but it can often be trickier to avoid breaking newer applications on older kernels. One of the key ways to do that is to provide ways for applications to discover whether a given feature is supported by the kernel or not. The disagreement here is over whether the proposed feature provides that discovery mechanism, with developers like Falcato and Hellwig saying "no", while Brauner feels that it is discoverable on all kernels that actually matter. There is only one chance to make the correct decision; once the new feature is exposed in a kernel release, it will be difficult to change thereafter.

Index entries for this article
KernelDevelopment model/User-space ABI
KernelSystem calls/open()