[TSAN] ThreadSanitizer Signal Processing Fails in select() Event Loops While poll()/epoll_wait() Work Correctly
- 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
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