llvm / llvm/llvm-project

[TSAN] ThreadSanitizer Signal Processing Fails in select() Event Loops While poll()/epoll_wait() Work Correctly

Open
#192,401 1 comment 0 reactions 0 assignees View on GitHub
compiler-rt:tsan
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary
ThreadSanitizer does not intercept the select() system call, unlike poll() and epoll_wait(). This omission prevents signal processing in select()-based event loops, as TSan's signal delivery depends on interception contexts that select()never enters.

### Environment
Compiler: clang 22.1.3
TSan: compiler-rt main
OS: Linux 5.10.134-14.el8.x86_64
Architecture: x86_64

### Analysis
1.poll()- Fully intercepted​ (in sanitizer_common/sanitizer_common_interceptors.inc):

```c
INTERCEPTOR(int, poll, __sanitizer_pollfd *fds, __sanitizer_nfds_t nfds, int timeout) {
void *ctx;
COMMON_INTERCEPTOR_ENTER(ctx, poll, fds, nfds, timeout);
if (fds && nfds) read_pollfd(ctx, fds, nfds);
int res = COMMON_INTERCEPTOR_BLOCK_REAL(poll)(fds, nfds, timeout); // Sets in_blocking_func
if (fds && nfds) write_pollfd(ctx, fds, nfds);
return res;
}
```

2.epoll_wait()- Fully intercepted​ (in tsan/rtl/tsan_interceptors_posix.cpp):

```c
TSAN_INTERCEPTOR(int, epoll_wait, int epfd, void *ev, int cnt, int timeout) {
SCOPED_TSAN_INTERCEPTOR(epoll_wait, epfd, ev, cnt, timeout);
if (epfd >= 0)
FdAccess(thr, pc, epfd);
int res = BLOCK_REAL(epoll_wait)(epfd, ev, cnt, timeout); // Sets in_blocking_func
if (res > 0 && epfd >= 0)
FdAcquire(thr, pc, epfd);
return res;
}
```

3.select()- NOT INTERCEPTED:
- No INTERCEPTOR macro for select in TSan codebase
- No COMMON_INTERCEPTOR_ENTER/EXIT calls
- Direct call to libc select() bypasses TSan instrumentation

### TSan Signal Processing Model
TSan processes signals through three mechanisms based on execution context:

1. Immediate Processing​ (in sighandler()):
Synchronous signals (SIGSEGV, SIGILL, SIGFPE, SIGBUS, SIGTRAP, SIGSYS), Execution within BLOCKING_CALL scope

2. Deferred Processing​ (at interception boundaries):
Within Scoped Interceptoror, Scoped Syscall contexts, User handlers called when leaving the interception scope

3. Unprocessed Queuing:
Outside any interception context, Signals remain in pending queue indefinitely

select() has no interception → no boundary → signals never processed.

### Workarounds
Users can:
- Replace select()with poll()(already intercepted)
- Add some intercepted functions like sleep、nanosleep

### Suggested Fix
Add select()interception to match poll()behavior:

```c
TSAN_INTERCEPTOR(int, select, int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout) {
SCOPED_TSAN_INTERCEPTOR(select, nfds, readfds, writefds, exceptfds, timeout);
// TODO Track file descriptor access for race detection

int res = BLOCK_REAL(select)(nfds, readfds, writefds, exceptfds, timeout);

// BLOCK_REAL enables signal processing

return res;
}
```

Contributor guide

Open the contributing guide

Research direction

Compare the poll interceptor in sanitizer_common/sanitizer_common_interceptors.inc with the epoll_wait interceptor in tsan/rtl/tsan_interceptors_posix.cpp, then trace the select() entry point and TSan signal-processing boundaries. Done means select()-based event loops enter the appropriate interception context and signal processing behaves like the existing poll() and epoll_wait() paths, with the relevant Linux TSan behavior verified.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, linux
Domain
compilers, operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.