4

(I think) I have a relatively good understanding of how Linux permissions work - traditional Unix DAC permissions, and how they're represented, security context in relation to processes (i.e. cred struct, capabilities, etc.), hooks for LSMs, etc., and the link they have to (E)UID/E(GID), etc. What I don't understand, and am having difficulty finding in kernel code, and from using ftrace (via trace-cmd), is when these checks are actually done (presumably) in kernel space, and at which point the access decision is actually made.

To elaborate:

I have a root-owned directory /dir chmod-ed to r-x------ (500). As an unprivileged user, I run /bin/ls /dir and receive a permission denied error, as expected.

If I run strace -s 10000 /bin/ls /dir I can see the open syscall, which returns a -1 back to user space, and sets errno to EACCES (i.e. "permission denied"). There's then a subsequent write syscall to print the error message to STDERR, all as expected. At no point here do I ever see getdents or getdents64 syscalls being called.

If I run the same test using trace-cmd, as trace-cmd record -p function_graph -F /bin/ls with the same unprivileged user doing /bin/ls /dir in a separate terminal tab (where trace-cmd will trigger on the execution of the ls binary), I can basically map most of the syscalls I'm seeing, one-to-one, between strace and trace-cmd outputs.

However, the discrepancy is that, in the trace-cmd output after the open syscall, rather than seeing the write outputs and it exiting, I can see getdents calls being made (and the full call tree of iterating over items in the directory, etc.). Because of limitations with ftrace, I guess (and more likely limitations in my understanding), I'm not seeing syscall arguments or returns in the output, so I can't see when, if at all, I'm getting permission errors, but from my limited understanding, the kernel appears to be actually doing the full directory listing, but just not returning the output back to user space.

So can anyone explain what's actually happening - why trace-cmd shows the kernel actually doing the getdents syscalls, while strace exits after the open call fails? Most of the forum posts (on here and elsewhere) with questions pertaining to permissions suggest that checks are done on opening the file, but provide little more detail than that, though this aligns with what I can see with strace (but not ftrace/trace-cmd).

Looking at the kernel source, and following that through manually from the open/openat syscalls, I can see functions call sequences such as inode_permission -> do_inode_permission -> generic_permission -> acl_permission_check with the latter actually doing comparisons on the classic Unix permissions, and returning from this. I can also see the hooks elsewhere for the LSMs, like SELinux, Apparmor, SMACK, Tomoyo, etc., which I guess will do their bits.

My assumption would be that the directory is attempted to be opened, permission checks would happen, kernel says no, and returns the error back to user space, and it would never get to the point of actually doing the directory listing. However, based on the trace-cmd output appearing to actually show the directory listing happening, I'm not sure any more.

Any information - in as much technical depth as possible - would be greatly appreciated. Also, I understand that using Systemtap may be able to offer more information to me, but my knowledge of that is very limited!

For awareness, my testing has been on Kernel versions 2.6.35 and 6.5.0 on Debian-based distros, with comparatively similar results.

6
  • As I understand it, the -F flag on the trace-cmd is the binary to trace. In a separate terminal tab, I run the ls command, which is captured by trace-cmd. I'll add that into the original question for clarity. In terms of trace-cmd output, all you get (from how I was running it) is a function call chain - so you can essentially see a formatted code block sequence like: foo() -> bar() -> baz() But not args passed to functions, or what's returned. So, I can't be sure what the gendents syscall is actually being run on, only that it is being run (but not in the strace output) Commented Mar 26 at 13:25
  • 1
    ok, I'll admit I don't know anything about trace-cmd. But, if I run trace-cmd record -p function_graph -F /bin/ls, it prints me the listing of the current directory, and the same with -l / at the end prints a long listing of the root directory, so it seems to me, it'd just run the program with the given options then and there.
    – ilkkachu
    Commented Mar 26 at 15:44
  • 1
    Everything I know says that permissions checks are mostly done just at the time of open(), not at the time of read() (or getdents()), and it would seem odd for one tracer to show a different result than another. Accidentally running it against a wrong directory would be a rather simple explanation (if almost a bit too convenient), and not at all as uncommon a mistake to make than what one might hope!
    – ilkkachu
    Commented Mar 26 at 15:45
  • @ilkkachu Embarrassingly, user error is the culprit. Both my original understanding, and your understanding with regards to permission checks being done at the time of the open call were correct. The issue, I think, seems to be my lack of understanding of trace-cmd, and specifically the "-F" flag. I assumed I tell it to watch that binary in one terminal, and then run the binary in the other, and it'll trace that. But the minute I run the command, it had already populated the trace - populated with what, I don't know, but it wasn't actually the data from me running the "ls" myself! Commented Mar 26 at 17:48
  • @ilkkachu Worse still, "-F" was indeed just causing the /bin/ls command to run itself, as opposed to waiting until it saw it running - so I was seeing the output from just running it as root (given trace-cmd was running as root). Now that is really embarrassing, especially since I missed your comment saying exactly this! Commented Mar 26 at 17:55

1 Answer 1

3

User error, and lack of understanding of trace-cmd (and specifically the -F flag that I was using) was the problem.

My assumption was that in running trace-cmd record -p function_graph -F /bin/ls, trace-cmd would wait until it saw the "ls" process being executed, then hook that. It turns out that -F just causes the binary to run there and then (as root, given trace-cmd would be running as root). Hacky workaround was to ensure I was attaching to a process (some ugly C to do a sleep for a few seconds while I grab the PID of the process, then have that process do the ls as the user). In doing it this was, everything was as expected. I was able to confirm my original understanding was correct.

For reference, permission checks are done at the time of file opening, following the function call chain from an open syscall. There some code initially allocate an empty file description, and begin populating bits of that structure. The actual permission checking seems to happen closer to the end of the call chain. If it's a directory object, there's some dedicated code for checking execute permission, which makes sense, at that bit has a different behaviour. There's then a call to a may_open function, which in turn calls inode_permission, which calls generic_permission that does the basic DAC checks. There's also the LSM checks around here, too. On failing these, permission errors are returned as one may expect.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .