linux-fsdevel.vger.kernel.org archive mirror
 help / color / mirror / Atom feed
* futimens use of utimensat does not support O_PATH fds
@ 2025-08-07 21:01 Josh Triplett
  2025-08-07 21:51 ` Aleksa Sarai
  2025-08-08 13:22 ` Christian Brauner
  0 siblings, 2 replies; 6+ messages in thread
From: Josh Triplett @ 2025-08-07 21:01 UTC (permalink / raw)
  To: linux-fsdevel

I just discovered that opening a file with O_PATH gives an fd that works
with

utimensat(fd, "", times, O_EMPTY_PATH)

but does *not* work with what futimens calls, which is:

utimensat(fd, NULL, times, 0)

The former will go through do_utimes_fd, while the latter goes through
do_utimes_path. I would have expected these two cases to end up in the
same codepath once they'd discovered they were operating on a file
descriptor, and I would have expected both to support O_PATH file
descriptors if either does.

This is true for both symlinks (with O_NOFOLLOW | O_PATH) and regular
files (with just O_PATH). This is on 6.12, in case it matters.

Quick and dirty test program (in Rust, using rustix to make syscalls):

```
use rustix::fs::{AtFlags, OFlags, Timespec, Timestamps, UTIME_OMIT};

fn main() -> std::io::Result<()> {
    let f = rustix::fs::open("oldfile", OFlags::PATH | OFlags::CLOEXEC, 0o666.into())?;
    let times = Timestamps {
        last_access: Timespec { tv_sec: 0, tv_nsec: UTIME_OMIT },
        last_modification: Timespec { tv_sec: 0, tv_nsec: 0 },
    };
    let ret = rustix::fs::utimensat(&f, "", &times, AtFlags::EMPTY_PATH);
    println!("utimensat: {ret:?}");
    let ret = rustix::fs::futimens(&f, &times);
    println!("futimens: {ret:?}");
    Ok(())
}
```

Is this something that would be reasonable to fix? Would a patch be
welcome that makes both cases work identically and support O_PATH file
descriptors?

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: futimens use of utimensat does not support O_PATH fds
  2025-08-07 21:01 futimens use of utimensat does not support O_PATH fds Josh Triplett
@ 2025-08-07 21:51 ` Aleksa Sarai
  2025-08-08  2:16   ` Josh Triplett
  2025-08-08 13:22 ` Christian Brauner
  1 sibling, 1 reply; 6+ messages in thread
From: Aleksa Sarai @ 2025-08-07 21:51 UTC (permalink / raw)
  To: Josh Triplett; +Cc: linux-fsdevel

[-- Attachment #1: Type: text/plain, Size: 3024 bytes --]

On 2025-08-07, Josh Triplett <josh@joshtriplett.org> wrote:
> I just discovered that opening a file with O_PATH gives an fd that works
> with
> 
> utimensat(fd, "", times, O_EMPTY_PATH)

I guess you mean AT_EMPTY_PATH? We don't have O_EMPTY_PATH on Linux
(yet, at least...).

> but does *not* work with what futimens calls, which is:
> 
> utimensat(fd, NULL, times, 0)
> 
> The former will go through do_utimes_fd, while the latter goes through
> do_utimes_path. I would have expected these two cases to end up in the
> same codepath once they'd discovered they were operating on a file
> descriptor, and I would have expected both to support O_PATH file
> descriptors if either does.
> 
> This is true for both symlinks (with O_NOFOLLOW | O_PATH) and regular
> files (with just O_PATH). This is on 6.12, in case it matters.
> 
> Quick and dirty test program (in Rust, using rustix to make syscalls):
> 
> ```
> use rustix::fs::{AtFlags, OFlags, Timespec, Timestamps, UTIME_OMIT};
> 
> fn main() -> std::io::Result<()> {
>     let f = rustix::fs::open("oldfile", OFlags::PATH | OFlags::CLOEXEC, 0o666.into())?;
>     let times = Timestamps {
>         last_access: Timespec { tv_sec: 0, tv_nsec: UTIME_OMIT },
>         last_modification: Timespec { tv_sec: 0, tv_nsec: 0 },
>     };
>     let ret = rustix::fs::utimensat(&f, "", &times, AtFlags::EMPTY_PATH);
>     println!("utimensat: {ret:?}");
>     let ret = rustix::fs::futimens(&f, &times);
>     println!("futimens: {ret:?}");
>     Ok(())
> }
> ```
> 
> Is this something that would be reasonable to fix? Would a patch be
> welcome that makes both cases work identically and support O_PATH file
> descriptors?

The set of things that are and are not allowed on O_PATH file
descriptors is a bit of a hodge-podge these days. Originally the
intention was for all of these things to be blocked by O_PATH (kind of
like O_SEARCH on other *nix systems) but the existence of AT_EMPTY_PATH
(and /proc/self/fd/... hackery) slowly led more and more things to be
allowed.

The current stalemate is that stuff which operates on the fd directly
(fchmod/fchown) tends to not allow operations on O_PATH file
descriptors, while stuff that operates on paths with AT_EMPTY_PATH
(fchmodat/fchownat) does. Why? My impression is that this is mainly
because the man-page (and the original descriptions by Al) says that the
former set of functions don't work on O_PATH. (Though Al probably has a
different viewpoint.)

I was working on an attempted solution for this mess (as part of work to
add O_EMPTY_PATH support while also plugging some holes in the
re-opening semantics on Linux), but it's on the backburner for now.
The core issue is that O_PATH is a very minimalistic capability system,
and different users expect different things from O_PATH, and there isn't
a nice way to make everyone happy with just one bit.

-- 
Aleksa Sarai
Senior Software Engineer (Containers)
SUSE Linux GmbH
https://www.cyphar.com/

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: futimens use of utimensat does not support O_PATH fds
  2025-08-07 21:51 ` Aleksa Sarai
@ 2025-08-08  2:16   ` Josh Triplett
  0 siblings, 0 replies; 6+ messages in thread
From: Josh Triplett @ 2025-08-08  2:16 UTC (permalink / raw)
  To: Aleksa Sarai; +Cc: linux-fsdevel

On Fri, Aug 08, 2025 at 07:51:26AM +1000, Aleksa Sarai wrote:
> On 2025-08-07, Josh Triplett <josh@joshtriplett.org> wrote:
> > I just discovered that opening a file with O_PATH gives an fd that works
> > with
> > 
> > utimensat(fd, "", times, O_EMPTY_PATH)
> 
> I guess you mean AT_EMPTY_PATH? We don't have O_EMPTY_PATH on Linux
> (yet, at least...).

Yes, that was a typo.

> The set of things that are and are not allowed on O_PATH file
> descriptors is a bit of a hodge-podge these days. Originally the
> intention was for all of these things to be blocked by O_PATH (kind of
> like O_SEARCH on other *nix systems) but the existence of AT_EMPTY_PATH
> (and /proc/self/fd/... hackery) slowly led more and more things to be
> allowed.

I do appreciate that. In large part, I'm trying to figure out if it
would be reasonable for this specific case to work, based on the premise
that it doesn't add any new capability, just makes an existing
capability available more consistently.

Having that would make it easier to write portable code with *fewer*
branches for Linux. It'd still be necessary to *open* files with the
Linux-specific O_PATH, but generic library routines that operate on open
files wouldn't have to change.

- Josh Triplett

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: futimens use of utimensat does not support O_PATH fds
  2025-08-07 21:01 futimens use of utimensat does not support O_PATH fds Josh Triplett
  2025-08-07 21:51 ` Aleksa Sarai
@ 2025-08-08 13:22 ` Christian Brauner
  2025-08-08 19:01   ` Josh Triplett
  1 sibling, 1 reply; 6+ messages in thread
From: Christian Brauner @ 2025-08-08 13:22 UTC (permalink / raw)
  To: Josh Triplett; +Cc: linux-fsdevel

On Thu, Aug 07, 2025 at 02:01:15PM -0700, Josh Triplett wrote:
> I just discovered that opening a file with O_PATH gives an fd that works
> with
> 
> utimensat(fd, "", times, O_EMPTY_PATH)
> 
> but does *not* work with what futimens calls, which is:
> 
> utimensat(fd, NULL, times, 0)

It's in line with what we do for fchownat() and fchmodat2() iirc.
O_PATH as today is a broken concept imho. O_PATH file descriptors
should've never have gained the ability to meaningfully alter state. I
think it's broken that they can be used to change ownership or mode and
similar.

O_PATH file descriptors get file->f_op set to empty_fops when they're
opened. So anything that uses file operations is off-limits (read,
write, poll, mmap, sync etc.) but anything that uses inode_operations
(e.g., acl, setattr) or super operations isn't per se. So that scheme
isn't great. It shouldn't matter whether a write-like operation is
implemented on the file or inode level.

People do keep poking holes into O_PATH semantics at random. We're
constantly fighting off such attempts because they would break the few
useful promises that O_PATH file descriptors still give.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: futimens use of utimensat does not support O_PATH fds
  2025-08-08 13:22 ` Christian Brauner
@ 2025-08-08 19:01   ` Josh Triplett
  2025-08-10  4:44     ` Aleksa Sarai
  0 siblings, 1 reply; 6+ messages in thread
From: Josh Triplett @ 2025-08-08 19:01 UTC (permalink / raw)
  To: Christian Brauner; +Cc: linux-fsdevel

On Fri, Aug 08, 2025 at 03:22:58PM +0200, Christian Brauner wrote:
> On Thu, Aug 07, 2025 at 02:01:15PM -0700, Josh Triplett wrote:
> > I just discovered that opening a file with O_PATH gives an fd that works
> > with
> > 
> > utimensat(fd, "", times, O_EMPTY_PATH)
> > 
> > but does *not* work with what futimens calls, which is:
> > 
> > utimensat(fd, NULL, times, 0)
> 
> It's in line with what we do for fchownat() and fchmodat2() iirc.
> O_PATH as today is a broken concept imho. O_PATH file descriptors
> should've never have gained the ability to meaningfully alter state. I
> think it's broken that they can be used to change ownership or mode and
> similar.

In the absence of having O_PATH file descriptors, what would be the way
to modify the properties of a symlink using race-free
file-descriptor-based calls rather than filenames? AFAICT, there's no
way to get a file descriptor corresponding to a symbolic link without
using `O_PATH | O_NOFOLLOW`.

It makes sense that a file descriptor for a symbolic link would be able
to do inode operations but not file operations.

^ permalink raw reply	[flat|nested] 6+ messages in thread

* Re: futimens use of utimensat does not support O_PATH fds
  2025-08-08 19:01   ` Josh Triplett
@ 2025-08-10  4:44     ` Aleksa Sarai
  0 siblings, 0 replies; 6+ messages in thread
From: Aleksa Sarai @ 2025-08-10  4:44 UTC (permalink / raw)
  To: Josh Triplett; +Cc: Christian Brauner, linux-fsdevel

[-- Attachment #1: Type: text/plain, Size: 3190 bytes --]

On 2025-08-08, Josh Triplett <josh@joshtriplett.org> wrote:
> On Fri, Aug 08, 2025 at 03:22:58PM +0200, Christian Brauner wrote:
> > On Thu, Aug 07, 2025 at 02:01:15PM -0700, Josh Triplett wrote:
> > > I just discovered that opening a file with O_PATH gives an fd that works
> > > with
> > > 
> > > utimensat(fd, "", times, O_EMPTY_PATH)
> > > 
> > > but does *not* work with what futimens calls, which is:
> > > 
> > > utimensat(fd, NULL, times, 0)
> > 
> > It's in line with what we do for fchownat() and fchmodat2() iirc.
> > O_PATH as today is a broken concept imho. O_PATH file descriptors
> > should've never have gained the ability to meaningfully alter state. I
> > think it's broken that they can be used to change ownership or mode and
> > similar.
> 
> In the absence of having O_PATH file descriptors, what would be the way
> to modify the properties of a symlink using race-free
> file-descriptor-based calls rather than filenames? AFAICT, there's no
> way to get a file descriptor corresponding to a symbolic link without
> using `O_PATH | O_NOFOLLOW`.

Yes, O_PATH|O_NOFOLLOW is the only way to get a file descriptor
referencing a symlink. However, depending on what property you were
talking about, doing

  fooat(parent_dirfd, "terminal-pathname-without-slashes", AT_SYMLINK_NOFOLLOW);

is probably sufficient for most programs, and I believe is the pattern
that Solaris was going for when they introduced *at(2) system calls.
Solaris does also have O_SEARCH, but I believe it's more restrictive
than O_PATH.

Yes, if you want to operate on a very specific inode, this approach
doesn't work if an attacker has write access to the parent directory.
But in my experience there are very few cases where you want to operate
on a very specific inode inside an attacker-controlled directory (most
of the time you just want to avoid being tricked to operate on stuff
outside the directory, and any inode inside the directory is fine --
which is what the above gives you).

> It makes sense that a file descriptor for a symbolic link would be able
> to do inode operations but not file operations.

From a kernel developer's perspective, maybe. But what is a file
operation or an inode operation is not immediately obvious to user
space, and the in-kernel distinction really isn't an API that was
intended to be user-visible IMHO.

In general, when it comes to O_PATH some userspace programs would prefer
O_PATH to disallow modifying _any aspect_ of the file descriptor, so
that you can pass them to untrusted programs (like a real
capability-based system). This is no longer achievable on Linux today,
and the fact we keep poking more holes in O_PATH is making the situation
less and less tenable.

I _do_ want a better solution for this, but if we want to keep expanding
O_PATH then we really need to have some way for programs to opt-out of
those expansions. Then we can come up with a default set of allowed
operations on O_PATH that programs can adjust, which will finally break
up the binary nature of O_PATH.

-- 
Aleksa Sarai
Senior Software Engineer (Containers)
SUSE Linux GmbH
https://www.cyphar.com/

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 228 bytes --]

^ permalink raw reply	[flat|nested] 6+ messages in thread

end of thread, other threads:[~2025-08-10  4:45 UTC | newest]

Thread overview: 6+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2025-08-07 21:01 futimens use of utimensat does not support O_PATH fds Josh Triplett
2025-08-07 21:51 ` Aleksa Sarai
2025-08-08  2:16   ` Josh Triplett
2025-08-08 13:22 ` Christian Brauner
2025-08-08 19:01   ` Josh Triplett
2025-08-10  4:44     ` Aleksa Sarai

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).