[<prev] [next>] [<thread-prev] [thread-next>] [day] [month] [year] [list]
Message-ID: <2026-01-27-awake-stony-flair-patrol-g4abX8@cyphar.com>
Date: Wed, 28 Jan 2026 00:23:45 +0100
From: Aleksa Sarai <cyphar@...har.com>
To: Dorjoy Chowdhury <dorjoychy111@...il.com>
Cc: linux-fsdevel@...r.kernel.org, linux-kernel@...r.kernel.org,
viro@...iv.linux.org.uk, brauner@...nel.org, jack@...e.cz, jlayton@...nel.org,
chuck.lever@...cle.com, alex.aring@...il.com, arnd@...db.de, adilger@...ger.ca
Subject: Re: [PATCH v3 1/4] open: new O_REGULAR flag support
On 2026-01-27, Dorjoy Chowdhury <dorjoychy111@...il.com> wrote:
> This flag indicates the path should be opened if it's a regular file.
> This is useful to write secure programs that want to avoid being tricked
> into opening device nodes with special semantics while thinking they
> operate on regular files.
>
> A corresponding error code ENOTREG has been introduced. For example, if
> open is called on path /dev/null with O_REGULAR in the flag param, it
> will return -ENOTREG.
>
> When used in combination with O_CREAT, either the regular file is
> created, or if the path already exists, it is opened if it's a regular
> file. Otherwise, -ENOTREG is returned.
>
> -EINVAL is returned when O_REGULAR is combined with O_DIRECTORY (not
> part of O_TMPFILE) because it doesn't make sense to open a path that
> is both a directory and a regular file.
As you mention in your cover letter, this is something that the UAPI
group has asked for in the past[1] and was even discussed at a recent
LPC (maybe LPC 2024?) -- thanks for the patch!
In the next posting of this patchset, I would suggest including this
information in the *commit message* with a link (commit messages end up
in the git history, cover letters are a little harder to search for when
doing "git blame").
[1]: https://uapi-group.org/kernel-features/#ability-to-only-open-regular-files
> #define WILL_CREATE(flags) (flags & (O_CREAT | __O_TMPFILE))
> -#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC)
> +#define O_PATH_FLAGS (O_DIRECTORY | O_NOFOLLOW | O_PATH | O_CLOEXEC | O_REGULAR)
It doesn't really make sense to use this flag with O_PATH -- O_PATH file
descriptors do not actually open the target inode and so there is no
risk to doing this.
In fact the method of safely opening files while avoiding device inodes
on Linux today is to open an O_PATH, then use fstat(2) to check whether
it is a regular file, and then re-open the file descriptor through
/proc/self/fd/$n. (This is totally race-safe.)
My main reason for pushing back against this it's really quite
preferable to avoid expanding the set of O_* flags which work with
O_PATH if they don't add much -- O_PATH has really unfortunate behaviour
with ignoring other flags and openat2(2) finally fixed that by blocking
ignored flag combinations.
> inline struct open_how build_open_how(int flags, umode_t mode)
> {
> @@ -1250,6 +1250,8 @@ inline int build_open_flags(const struct open_how *how, struct open_flags *op)
> return -EINVAL;
> if (!(acc_mode & MAY_WRITE))
> return -EINVAL;
> + } else if ((flags & O_DIRECTORY) && (flags & O_REGULAR)) {
> + return -EINVAL;
> }
> if (flags & O_PATH) {
> /* O_PATH only permits certain other flags to be set. */
> diff --git a/include/linux/fcntl.h b/include/linux/fcntl.h
> index a332e79b3207..4fd07b0e0a17 100644
> --- a/include/linux/fcntl.h
> +++ b/include/linux/fcntl.h
> @@ -10,7 +10,7 @@
> (O_RDONLY | O_WRONLY | O_RDWR | O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC | \
> O_APPEND | O_NDELAY | O_NONBLOCK | __O_SYNC | O_DSYNC | \
> FASYNC | O_DIRECT | O_LARGEFILE | O_DIRECTORY | O_NOFOLLOW | \
> - O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE)
> + O_NOATIME | O_CLOEXEC | O_PATH | __O_TMPFILE | O_REGULAR)
Legacy open(2)/openat(2) do not reject invalid flag arguments, which
means that you cannot trivially add a new security-critical flag to them
for two reasons:
* You cannot easily rely on them because old kernels will not return
-EINVAL, meaning you cannot be sure that the flag is supported. You
can try to test-run it, but the operation needs to be a non-dangerous
operation to try (and caching this has its own issues, such as with
programs that apply seccomp filters later).
To be fair, since you reject O_DIRECTORY|O_REGULAR there is a
relatively easy way to detect this, but the caveats about problems
with caching still apply.
* Old programs might pass garbage bits that have been ignored thus far,
which means that making them have meaning can break userspace. Given
the age of open(2) this is a very hard thing to guarantee and is one
of many reasons I wrote openat2(2) and finally added proper flag
checking.
This is something your patch doesn't deal with and I don't think can
be done in a satisfactory way (because the behaviour relies on more
than just the arguments).
For reference, this is why O_TMPFILE includes O_DIRECTORY and requires
an O_ACCMODE with write bits -- this combination will fail on old
kernels, which allows you to rely on it and also guarantees that no
existing older programs passed that flag combination already and
happened to work on older kernels. This kind of trick won't work for
O_REGULAR, unfortunately.
In my view, this should be an openat2(2)-only API. In addition, I would
propose that (instead of burning another O_* flag bit for this as a
special-purpose API just for regular files) you could have a mask of
which S_IFMT bits should be rejected as a new field in "struct
open_how". This would let you reject sockets or device inodes but permit
FIFOs and regular files or directories, for instance. This could even be
done without a new O_* flag at all (the zero-value how->sfmt_mask would
allow everything and so would work well with extensible structs), but we
could add an O2_* flag anyway.
> +#define ENOTREG 134 /* Not a regular file */
> +
We are probably a little too reticent to add new errnos, but in this
case I think that there should be some description in the commit or
cover letter about why a new errno is needed. ENXIO or
EPROTONOSUPPORT/EPROTOTYPE is what you would typically use (yes, they
aren't a _perfect_ match but one of the common occurrences in syscall
design is to read through errno(7) and figure out what errnos kind of
fit what you need to express).
Then to be fair, the existence of ENOTBLK, ENOTDIR, ENOTSOCK, etc. kind
of justify the existence of ENOTREG too. Unfortunately, you won't be
able to use ENOTREG if you go with my idea of having mask bits in
open_how... (And what errno should we use then...? Hm.)
--
Aleksa Sarai
https://www.cyphar.com/
Download attachment "signature.asc" of type "application/pgp-signature" (266 bytes)
Powered by blists - more mailing lists