[cfe-dev] [libc++][RFC] Implementing Directory Entry Caching for Filesystem
Davis Herring via cfe-dev
cfe-dev at lists.llvm.org
Wed Jul 18 10:49:53 PDT 2018
> As I'll explain below, each different approach can cause non-obvious
> behavior, possibly leading to TOCTOU bugs and security
> vulnerabilities!
Anyone who has real TOCTOU concerns (like operating in directories
controlled by other users) has to do special things anyway: in C++17,
they can current_path("foo/bar") to avoid re-resolving foo and bar on
future lookups, while in POSIX the better answer is to use the *at
functions to avoid interference between threads and libraries via the
process-wide shared CWD state.
> Above, a TOCTOU violation may occur if another process acts on the
> file referenced by `ent`, changing the attributes between the point
> the directory_entry was created and the point at which the
> `is_symlink` condition is checked.
But another process can already act between the is_symlink and the call
to remove. Absent a userspace dentry-level interface, it's not clear
what the additional concern is from the is_symlink value being cached.
> However, at least the above example makes it clear *when* the check
> is occuring.
Why does it matter that the location of the check is obvious so long as
(it's at least as obvious that) there's a race window after it?
> Using the `refresh` function, either called directly by the user, or
> implicitly by the constructors or modifying methods (Ex. `assign`).
> `refresh` has the ability to fully populate the cache
It would certainly be nice to cache all the results from [l]stat(2)
(C++17 Late NB comment 20). However, I'm not sure that you can cache
anything more in refresh() than in directory_iterator::operator++. The
normative encouragement in [fs.class.directory_entry]/2 specifically
says "during directory iteration" without suggesting that other
attributes could be cached at other times.
Of course, then I'm not sure that even d_type can be cached:
[fs.dir.entry.obs]/27 says that a file_status is either cached or not,
and it also contains the permissions (from stat(2)).
> if (!ent.is_symlink())
> return false;
>
> // We should have the file_status for the symlink cached, right?
> file_status st = ent.symlink_status();
>
> // Nope! Only when `refresh` has been called are the perms cached.
> assert(is_symlink(st)); // May fail!
The assertion failure is surprising only if the author assumed that the
implementation was caching anything. Caching of any particular
attribute can only increase consistency with regard to it, in that there
are fewer system calls that might observe updates. So code that is
correct without caching will not become more racy when it is introduced.
> bool is_empty_regular_file(path p) {
> directory_entry ent(p); // Calls refresh()
> return ent.is_regular_file() && ent.file_size() == 0;
> }
It's fine to write code this way in an attempt to capitalize on caching
as an optimization, but any coherence it adds is non-portable. There is
simply no way in C++17 to make this function reliable (thus the NB
comment). Of course, there's only so much a race-free implementation
could get you, given the race window before any subsequent operation.
> Worse yet, the latter case, which fully populates the cache
> non-atomically, potentially commits a TOCTOU violation itself, and
> this violation is hidden from the user, preventing them from knowing
> it exists or being able to do anything about it.
First, note that both this caching and is_empty_regular_file (without
any caching) can produce "impossible" results:
In a directory that never contains any empty regular files,
is_empty_regular_file can return true because a regular file is replaced
with an empty non-regular file (e.g., a device file).
In a directory that never contains a symlink to a directory, a
non-atomic caching directory_entry can have is_symlink and is_directory
true simultaneously because a symlink was replaced with a directory (or
vice versa).
The latter case concerns me a bit more: because the order of the system
calls might not match the order of the queries, they can return results
inconsistent with what is known to be a single possibly-concurrent
change to a file. The motivated user can use refresh to recover from
such an impossibility, while in the case of an unbounded number of
concurrent changes nothing can be guaranteed anyway.
> Afterwards, a better and safer proposal can be put forward which to
> provides efficient and atomic access to multiple attributes for the
> same entity can be proposed.
It's hard to standardize atomic access to properties that are not
atomically accessible everywhere. But one could more easily entertain
separate accessors for cached values (which return "unknown" as
appropriate), so that the decision to use them (where available) falls
to the user.
Davis
--
This product is sold by volume, not by mass. If it appears too dense or
too sparse, it is because mass-energy conversion has occurred during
shipping.
More information about the cfe-dev
mailing list