I am root. I would like to know whether a non-root user has write access to some files - thousands of them. How to do it efficiently while avoiding process creation?
6 Answers
TL;DR
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'
You need to ask the system if the user has write permission. The only reliable way is to switch the effective uid, effective gid and supplementation gids to that of the user and use the access(W_OK)
system call (even that has some limitations on some systems/configurations).
And bear in mind that not having write permission to a file does not necessarily guarantee that you can't modify the content of the file at that path.
The longer story
Let's consider what it takes for instance for a $user to have write access to /foo/file.txt
(assuming none of /foo
and /foo/file.txt
are symlinks)?
He needs:
- search access to
/
(no need forread
) - search access to
/foo
(no need forread
) - write access to
/foo/file.txt
You can see already that approaches (like @lcd047's or @apaul's) that check only the permission of file.txt
won't work because they could say file.txt
is writable even if the user doesn't have search permission to /
or /foo
.
And an approach like:
sudo -u "$user" find / -writeble
Won't work either because it won't report the files in directories the user doesn't have read access (as find
running as $user
can't list their content) even if he can write to them.
If we forget about ACLs, read-only file systems, FS flags (like immutable), other security measures (apparmor, SELinux, which can even distinguish between different types of writing) and only focus on traditional permission and ownership attributes, to get a given (search or write) permission, that's already quite complicated and hard to express with find
.
You need:
- if the file is owned by you, you need that permission for the owner (or have uid 0)
- if the file is not owned by you, but the group is one of yours, then you need that permission for the group (or have uid 0).
- if it's not owned by you, and not in any of your groups, then the other permissions apply (unless your uid is 0).
In find
syntax, here as an example with a user of uid 1 and gids 1 and 2, that would be:
find / -type d \
\( \
-user 1 \( -perm -u=x -o -prune \) -o \
\( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) -o -type l -o \
-user 1 \( ! -perm -u=w -o -print \) -o \
\( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
That one prunes the directories that user doesn't have search right for and for other types of files (symlinks excluded as they're not relevant), checks for write access.
If you also want to consider write access to directories:
find / -type d \
\( \
-user 1 \( -perm -u=x -o -prune \) -o \
\( -group 1 -o -group 2 \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user 1 \( ! -perm -u=w -o -print \) -o \
\( -group 1 -o -group 2 \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
Or for an arbitrary $user
and its group membership retrieved from the user database:
groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
\( \
-user "$user" \( -perm -u=x -o -prune \) -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( ! -perm -u=w -o -print \) -o \
\( -group $groups \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
(that's 3 processes in total: id
, sed
and find
)
The best here would be to descend the tree as root and check the permissions as the user for each file.
find / ! -type l -exec sudo -u "$user" sh -c '
for file do
[ -w "$file" ] && printf "%s\n" "$file"
done' sh {} +
(that's one find
process plus one sudo
and sh
process every few thousand files, [
and printf
are usually built in the shell).
Or with perl
:
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -l -0ne 'print if -w'
(3 processes in total: find
, sudo
and perl
).
Or with zsh
:
files=(/**/*(D^@))
USERNAME=$user
for f ($files) {
[ -w $f ] && print -r -- $f
}
(0 process in total, but stores the whole file list in memory)
Those solutions rely on the access(2)
system call. That is instead of reproducing the algorithm the system uses to check for access permission, we're asking the system to do that check with the same algorithm (which takes into account permissions, ACLs, immutable flags, read-only file systems...) it would use would you try to open the file for writing, so is the closest you're going to get to a reliable solution.
To test the solutions given here, with the various combinations of user, group and permissions, you could do:
perl -e '
for $u (1,2) {
for $g (1,2,3) {
$d1="u${u}g$g"; mkdir$d1;
for $m (0..511) {
$d2=$d1.sprintf"/%03o",$m; mkdir $d2; chown $u, $g, $d2; chmod $m,$d2;
for $uu (1,2) {
for $gg (1,2,3) {
$d3="$d2/u${uu}g$gg"; mkdir $d3;
for $mm (0..511) {
$f=$d3.sprintf"/%03o",$mm;
open F, ">","$f"; close F;
chown $uu, $gg, $f; chmod $mm, $f
}
}
}
}
}
}'
Varying user between 1 and 2 and group betweem 1, 2, and 3 and limiting ourselves to the lower 9 bits of the permissions as that's already 9458694 files created. That for directories and then again for files.
That creates all possible combinations of u<x>g<y>/<mode1>/u<z>g<w>/<mode2>
. The user with uid 1 and gids 1 and 2 would have write access to u2g1/010/u2g3/777
but not u1g2/677/u1g1/777
for instance.
Now, all those solutions try to identify the paths of files that the user may open for writing, that's different from the paths where the user may be able to modify the content. To answer that more generic question, there are several things to take into account:
- $user may not have write access to
/a/b/file
but if he ownsfile
(and has search access to/a/b
, and the file system is not read-only, and the file doesn't have the immutable flag, and he's got shell access to the system), then he would be able to change the permissions of thefile
and grant himself access. - Same thing if he owns
/a/b
but doesn't have search access to it. - $user may not have access to
/a/b/file
because he doesn't have search access to/a
or/a/b
, but that file may have a hard link at/b/c/file
for instance, in which case he may be able to modify the content of/a/b/file
by opening it via its/b/c/file
path. - Same thing with bind-mounts. He may not have search access to
/a
, but/a/b
may be bind-mounted in/c
, so he could openfile
for writing via its/c/file
other path. - He may not have write permissions to
/a/b/file
, but if he has write access to/a/b
he can remove or renamefile
in there and replace it with his own version. He would change the content of the file at/a/b/file
even if that would be a different file. - Same thing if he's got write access to
/a
(he could rename/a/b
to/a/c
, create a new/a/b
directory and a newfile
in it.
To find the paths that $user
would be able to modify. To address 1 or 2, we can't rely on the access(2)
system call anymore. We could adjust our find -perm
approach to assume search access to directories, or write access to files as soon as you're the owner:
groups=$(id -G "$user" | sed 's/ / -o -group /g'); IFS=" "
find / -type d \
\( \
-user "$user" -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" -print -o \
\( -group $groups \) \( ! -perm -g=w -o -print \) -o \
! -perm -o=w -o -print
We could address 3 and 4, by recording the device and inode numbers or all the files $user has write permission to and report all the file paths that have those dev+inode numbers. This time, we can use the more reliable access(2)
-based approaches:
Something like:
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
perl -l -0ne '
($w,$p) = /(.)(.*)/;
($dev,$ino) = stat$p or next;
$writable{"$dev,$ino"} = 1 if $w;
push @{$p{"$dev,$ino"}}, $p;
END {
for $i (keys %writable) {
for $p (@{$p{$i}}) {
print $p;
}
}
}'
5 and 6 are at first glance complicated by the t
bit of the permissions. When applied on directories, that's the restricted deletion bit which prevents users (others than the owner of the directory) from removing or renaming the files they don't own (even though they have write access to the directory).
For instance, if we go back to our earlier example, if you have write access to /a
, then you should be able to rename /a/b
to /a/c
, and then recreate a /a/b
directory and a new file
in there. But if the t
bit is set on /a
and you don't own /a
, then you can only do it if you own /a/b
. That gives:
- If you own a directory, as per 1, you can grant yourself write access, and the t bit doesn't apply (and you could remove it anyway), so you can delete/rename/recreate any file or dirs in there, so all file paths under there are yours to rewrite with any content.
- If you don't own it but have write access, then:
- Either the
t
bit is not set, and you're in the same case as above (all file paths are yours). - or it's set and then you can't modify the files you don't own or don't have write access to, so for our purpose of finding the file paths you can modify, that's the same as not having write permission at all.
- Either the
So we can address all of 1, 2, 5 and 6 with:
find / -type d \
\( \
-user "$user" -prune -exec find {} + -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( -type d -o -print \) -o \
\( -group $groups \) \( ! -perm -g=w -o \
-type d ! -perm -1000 -exec find {} + -o -print \) -o \
! -perm -o=w -o \
-type d ! -perm -1000 -exec find {} + -o \
-print
That and the solution for 3 and 4 are independent, you can merge their output to get a complete list:
{
find / ! -type l -print0 |
sudo -u "$user" perl -Mfiletest=access -0lne 'print 0+-w,$_' |
perl -0lne '
($w,$p) = /(.)(.*)/;
($dev,$ino) = stat$p or next;
$writable{"$dev,$ino"} = 1 if $w;
push @{$p{"$dev,$ino"}}, $p;
END {
for $i (keys %writable) {
for $p (@{$p{$i}}) {
print $p;
}
}
}'
find / -type d \
\( \
-user "$user" -prune -exec sh -c 'exec find "$@" -print0' sh {} + -o \
\( -group $groups \) \( -perm -g=x -o -prune \) -o \
-perm -o=x -o -prune \
\) ! -type d -o -type l -o \
-user "$user" \( -type d -o -print0 \) -o \
\( -group $groups \) \( ! -perm -g=w -o \
-type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o -print0 \) -o \
! -perm -o=w -o \
-type d ! -perm -1000 -exec sh -c 'exec find "$@" -print0' sh {} + -o \
-print0
} | perl -l -0ne 'print unless $seen{$_}++'
As should be clear if you've read everything thus far, part of it at least only deals with permissions and ownership, not the other features that may grant or restrict write access (read-only FS, ACLs, immutable flag, other security features...). And as we process it in several stages, some of that information may be wrong if the files/directories are being created/deleted/renamed or their permissions/ownership modified while that script is running, like on a busy file server with millions of files.
Portability notes
All that code is standard (POSIX, Unix for t
bit) except:
-print0
is a GNU extension now also supported by a few other implementations. Withfind
implementations that lack support for it, you can use-exec printf '%s\0' {} +
instead, and replace-exec sh -c 'exec find "$@" -print0' sh {} +
with-exec sh -c 'exec find "$@" -exec printf "%s\0" {\} +' sh {} +
.perl
is not a POSIX-specified command but is widely available. You needperl-5.6.0
or above for-Mfiletest=access
.zsh
is not a POSIX-specified command. Thatzsh
code above should work with zsh-3 (1995) and above.sudo
is not a POSIX-specified command. The code should work with any version as long as the system configuration allows runningperl
as the given user.

- 544,893
-
What is a search access ? I have never heard of it in the traditional permissions : read, write, execute. – bela83 May 15 '15 at 19:35
-
2@bela83, execute permission on a directory (you don't execute directories) translates to search. That's the ability to access files in it. You can list a directory's content if you have read permission, but you can't do anything with the files in it unless you also have search (
x
bit) permission on the directory. You can also have search permissions but not read, meaning files in there are hidden to you, but if you know their name, you can access them. A typical example is the php session file directory (something like /var/lib/php). – Stéphane Chazelas May 15 '15 at 19:53
You can combine options with the find
command, so it will find out the files with specified mode and owner. For instance:
$ find / \( -group staff -o -group users \) -and -perm -g+w
The above command will list all entries which belong to the group "staff" or "users" and have write permission for that group.
You should also check for entries which are owned by your user, and files wich are world writable, so:
$ find / \( -user yourusername -or \
\( \( -group staff -o -group users \) -and -perm -g+w \
\) -or \
-perm -o+w \
\)
However, this command won't match entries with extended ACL. So you may su
then find out all writable entries:
# su - yourusername
$ find / -writable

- 3,378
-
That would say that a file with
r-xrwxrwx yourusername:anygroup
orr-xr-xrwx anyone:staff
is writeable. – Stéphane Chazelas May 15 '15 at 16:18 -
It would also report as writeable files that are in directories
yourusername
doesn't have access to. – Stéphane Chazelas May 15 '15 at 16:21
Perhaps like this:
#! /bin/bash
writable()
{
local uid="$1"
local gids="$2"
local ids
local perms
ids=($( stat -L -c '%u %g %a' -- "$3" ))
perms="0${ids[2]}"
if [[ ${ids[0]} -eq $uid ]]; then
return $(( ( perms & 0200 ) == 0 ))
elif [[ $gids =~ (^|[[:space:]])"${ids[1]}"($|[[:space:]]) ]]; then
return $(( ( perms & 020 ) == 0 ))
else
return $(( ( perms & 2 ) == 0 ))
fi
}
user=foo
uid="$( id -u "$user" )"
gids="$( id -G "$user" )"
while IFS= read -r f; do
writable "$uid" "$gids" "$f" && printf '%s writable\n' "$f"
done
The above runs a single external program for each file, namely stat(1)
.
Note: This assumes bash(1)
, and the Linux incarnation of stat(1)
.
Note 2: Please read comments from Stéphane Chazelas below for past, present, future, and potential dangers and limitations of this approach.

- 7,238
-
That could say a file is writable even though the user has no access to the directory where it is. – Stéphane Chazelas May 16 '15 at 08:22
-
That assumes file names don't contain newline character and file paths passed on stdin don't start with
-
. You could modify it to accept a NUL delimited list instead withread -d ''
– Stéphane Chazelas May 16 '15 at 08:23 -
Note that there's no such thing as a Linux stat. Linux is the kernel found in some GNU and non-GNU systems. While there are commands (like from
util-linux
) that are specifically written for Linux, thestat
you're referring to is not, it's a GNU command which has been ported to most systems, not only Linux. Also note that you had astat
command on Linux long before GNUstat
was written (thestat
zsh builtin). – Stéphane Chazelas May 16 '15 at 08:26 -
2@Stéphane Chazelas: Note that there's no such thing as a Linux stat. - I believe I wrote "the Linux incarnation of
stat(1)
". I'm referring to astat(1)
that accepts the-c <format>
syntax, as opposed to, say, BSD syntax-f <format>
. I also believe it was pretty clear I wasn't referring tostat(2)
. I'm sure a wiki page about the history of the common commands would be quite interesting though. – lcd047 May 16 '15 at 08:35 -
See there for the various stats. Also, you could use GNU find's
-printf
(predates GNU stat by many years) to avoid having to run one stat per file. – Stéphane Chazelas May 16 '15 at 08:38 -
1@Stéphane Chazelas: That could say a file is writable even though the user has no access to the directory where it is. - True, and probably a reasonable limitation. That assumes file names don't contain newline character - True, and probably a reasonable limitation. and file paths passed on stdin don't start with - - Edited, thank you. – lcd047 May 16 '15 at 08:40
-
IMO, limititions are fine as long as they are clearly stated. Then the OP (or anyone coming here with the same or similar question) can decide if they are reasonable for their specific use case. For instance, on a system with private home directories, that solution will report a lot of false positives (all the symlinks in all home directories for instance). It's not uncommon on file servers to have blanket strict access restriction to one top-level directory and then relax all permissions for everything underneath... – Stéphane Chazelas May 17 '15 at 20:17
-
The approach depends upon what you are really testing.
- Do you want to ensure write access is possible?
- Do you want to ensure lack of write access?
This is because there are so many ways to arrive at 2) and Stéphane's answer covers these well (immutable is one to remember), and recall that there are physical means as well, such as unmounting the drive or making it read-only at a hardware level (floppy tabs). I am guessing your thousands of files are in differing directories and you are wanting a report or you are checking against a master list. (Another abuse of Puppet just waiting to happen).
You likely want Stéphane's perl tree-traversal and to "join" the output with a list if required (su will also catch missing execute on parent directories?). If surrogacy is a performance issue are you doing this for a "large number" of users ? Or is it an online query ? If this is a permanent ongoing requirement it may be time to consider a third party product.

- 327
You can do...
find / ! -type d -exec tee -a {} + </dev/null
...for a list of all files to which the user cannot write as written to stderr in the form...
"tee: cannot access %s\n", <pathname>"
...or similar.
See the comments below for notes on the issues this approach might have, and the explanation below for why it might work. More sanely, though, you should probably only find
regular files like:
find / -type f -exec tee -a {} + </dev/null
In short, tee
will print errors when it attempts to open()
a file with either of the two flags...
O_WRONLY
Open for writing only.
O_RDWR
Open for reading and writing. The result is undefined if this flag is applied to a FIFO.
...and encounters...
[EACCES]
Search permission is denied on a component of the path prefix, or the file exists and the permissions specified by oflag are denied, or the file does not exist and write permission is denied for the parent directory of the file to be created, or O_TRUNC is specified and write permission is denied.
...as specified here:
The
tee
utility shall copy standard input to standard output, making a copy in zero or more files. The tee utility shall not buffer output.If the
-a
option is not specified, output files shall be written (see File Read, Write, and Creation)......POSIX.1-2008 requires functionality equivalent to using O_APPEND...
Because it has to check the same way test -w
does...
-w
pathnameTrue if pathname resolves to an existing directory entry for a file for which permission to write to the file will be granted, as defined in File Read, Write, and Creation. False if pathname cannot be resolved, or if pathname resolves to an existing directory entry for a file for which permission to write to the file will not be granted.
They both check for EACCESS.
-
You're likely to run into a limit of number of concurrently open files with that approach (unless the number of files is low). Beware of side effects with devices and named pipes. You'll get a different error message for sockets. – Stéphane Chazelas May 17 '15 at 21:20
-
@StéphaneChazelas - All tue - I think it might be also true that
tee
will hang unless explicitly interrupted once per run. It was the closest thing I could think of to[ -w
... though - it's effects should be close in that it guarantees the user can OAPPEND the file. Much easier than either option would bepax
with-o
format options and/or-t
for checking againstEACCESS
- but every time I suggestpax
people seem to shrug it off. And, anyway, the onlypax
I've found that meets the std there is AST's - in which case you might as well use theirls
. – mikeserv May 17 '15 at 22:13
Would it be possible to use find
's writable
predicate?
find /some/path -writable
From the man page
-writable Matches files which are writable. This takes into account access control lists and other permissions artefacts which the -perm test ignores. This test makes use of the access(2) system call, and so can be fooled by NFS servers which do UID mapping (or root-squashing), since many systems implement access(2) in the client's kernel and so cannot make use of the UID mapping information held on the server.

- 231
access(2)
with an appropriately set real-UID (e.g. viasetresuid(2)
or the portable equivalent)? I mean, I'd be hard-pressed to do that from bash, but I'm sure Perl/Python can handle it. – Kevin May 15 '15 at 17:47[ -w
do generally use access(2) or equivalent. You also need to set the gids in addition to uid (as su or sudo do). bash doesn't have builtin support for that but zsh does. – Stéphane Chazelas May 15 '15 at 21:32chgrp
in any shell. – mikeserv May 18 '15 at 10:06chgrp
doesn't change the gids of a process, my comment above was to complement Kevin's note that you should callsetresuid() + access()
. You also needinitgroups(3)
. – Stéphane Chazelas May 18 '15 at 10:25chgrp(1)
. – Stéphane Chazelas May 18 '15 at 20:01chgrp
is a builtin. Well, that and many other things. In fact, most of the stuff in the answers here become builtin when also built with the rest of the AST library. – mikeserv May 18 '15 at 23:20login
as well] - then run id and see what you get. with ksh you don't even need sudo. – mikeserv May 18 '15 at 23:26