`bcc_elf_is_exe` should use `stat` instead of `access` to check execute permissions
- Dominant language
- C
- Stars
- 22.7k
- Forks
- 4.1k
- Avg merge
- 10d 4h
- Merged PRs (30d)
- 3
Description
I think the implementation of `bcc_elf_is_exe`
https://github.com/iovisor/bcc/blob/d9eec93cfe46f1133aa8769f13b2550ca7edd08d/src/cc/bcc_elf.c#L1054-L1056
is subtly wrong, and should be using `stat` instead of `access` here.
For the single usage in `bcc` itself, the difference ~doesn't matter, but the current logic broke some usages downstream in `bpftrace`, until the `bpftrace` devs (accidentally) happened to remove the offending call in https://github.com/bpftrace/bpftrace/commit/19df2b1bf8fc018f7a0b4695473c36a5d8752980.
I did spend two hours tracking down what I think is a kernel bug (on top of a `bcc` bug) here, even if it's not super consequential, so I'm going to document it below to help the next person who Googles this.
Absent of the kernel bug, I think `access` might not semantically be what's desired here. From `man 2 access`:
> This allows set-user-ID programs and capability-endowed programs
> to easily determine the invoking user's authority. In other
> words, access() does not answer the "can I read/write/execute
> this file?" question. It answers a slightly different question:
> "(assuming I'm a setuid binary) can the user who invoked me
> read/write/execute this file?", which gives set-user-ID programs
> the possibility to prevent malicious users from causing them to
> read files which users shouldn't be able to read.
---
Now the kernel bug(?): I'm pretty sure that `access` on procfs is broken. Or at least, if working as intended, has undocumented and confusing semantics.
Assuming uid 5 is an unprivileged user, who is running a regular process with pid `223674`, the following is true on my 6.1 kernel:
```python
# Succeeds as root
>>> os.access('/proc/223674/exe', os.X_OK), os.getuid()
(True, 0)
# Fails as a regular user
>>> os.access('/proc/223674/exe', os.X_OK), os.getuid()
(False, 5)
```
There's nothing special about this file, and `stat` behaves as you'd expect it to:
```bash
$ stat /proc/245043/exe
File: /proc/245043/exe -> /tmp/demos.exe
Size: 0 Blocks: 0 IO Block: 1024 symbolic link
Device: 15h/21d Inode: 2052196 Links: 1
Access: (0777/lrwxrwxrwx) Uid: (5/user) Gid: (7/ tech)
Access: 2024-11-12 09:06:22.300926738 -0500
Modify: 2024-11-12 09:06:22.279926617 -0500
Change: 2024-11-12 09:06:22.279926617 -0500
Birth: -
```
A trace of what `access` ends up executing before failing looks like:

In particular, in
https://github.com/torvalds/linux/blob/2d5404caa8c7bb5c4e0435f94b28834ae5456623/security/commoncap.c#L151-L153
`ns_capable(..., CAP_SYS_PTRACE)` falls through, so we `EPERM`. Except this is all pretty obviously wrong: we don't need `CAP_SYS_PTRACE` to dereference `/proc/$pid/exe` and execute it.
(This is where I stopped looking, because I discovered upstream `bpftrace` had already removed the call to `bcc_elf_is_exe` that was tickling this bug.)
Contributor guide
No contributing guide indexed for this repository
Research direction
Start in src/cc/bcc_elf.c at bcc_elf_is_exe around lines 1054-1056 and review how the current access check handles executable paths, especially /proc/$pid/exe. Compare the intended stat-based behavior with the downstream bpftrace case described in the issue, then verify that executable ELF detection works for those paths without the access-related failure.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- c, linux
- Domain
- operating-systems, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100