bneradt opened a new issue, #13524:
URL: https://github.com/apache/trafficserver/issues/13524

   ## Summary
   
   Debug builds intermittently SIGSEGV in the debug-only lock-contention 
tracking:
   a thread that fails a try-lock reads the *current holder's* `SourceLocation` 
out
   of the `ProxyMutex` without holding anything, and `SourceLocation::str()` 
then
   faults dereferencing a stale `file` pointer.
   
   The cache unit tests are the only thing in the tree that enables the `locks`
   debug tag, so this surfaces as a rare `test_cache_*` segfault on the Fedora 
CI
   job. It is not a cache bug; the cache tests are just the trigger.
   
   ## How we saw it in CI
   
   The Fedora job failed on an unrelated PR (#13523, an `HttpSM` change) with:
   
   ```
   26/186 Test  #29: test_cache_Update_L_to_S ...............***Exception: 
SegFault  1.01 sec
   ...
   99% tests passed, 1 tests failed out of 186
   
   The following tests FAILED:
         29 - test_cache_Update_L_to_S (SEGFAULT)
   ```
   
   https://ci.trafficserver.apache.org/job/Github_Builds/job/fedora/9141/
   
   The test's own output ends like this — note the two lock-tracking warnings
   immediately before the crash:
   
   ```
   [...] DIAG: <CacheRead.cc:224 (openReadFromWriter)> (cache_read_agg) 
0x2236840: key: B7088DC0 In openReadFromWriter
   [...] DIAG: <CacheVC.cc:510 (handleRead)> (cache_ram) all memory cache miss
   WARNING: holding lock CacheRead.cc:789 (openReadStartEarliest) too long for 
UNKNOWN
   [...] DIAG: <StripeSM.cc:742 (aggWriteDone)> (cache_agg) ...
   WARNING: holding lock UnixEThread.cc:156 (process_event) too long for UNKNOWN
   
   DummyLocation:18446744073709551615: FAILED:
     {Unknown expression after the reported line}
   due to a fatal error condition:
     SIGSEGV - Segmentation violation signal
   ```
   
   The PR under test cannot be responsible: `add_cache_test` builds each cache 
test
   from `unit_tests/main.cc`, `unit_tests/stub.cc`, 
`unit_tests/CacheTestHandler.cc`
   and the test source, linked against `ts::inkcache` and Catch2 only. Nothing 
from
   `libhttp` is in the binary.
   
   ## Reproduction
   
   On `master` at b9b9109864 with no modifications, using the CI configuration
   (clang, `CMAKE_BUILD_TYPE=Debug`):
   
   ```bash
   cmake -B build --preset ci-fedora-cxx20 -DBUILD_TESTING=ON
   cmake --build build
   cd build
   # Loop this; it fails roughly 1 in 12-20 iterations.
   for i in $(seq 1 30); do ctest -R "^test_cache_" -j 8 --output-on-failure; 
done
   ```
   
   Observed:
   
   | What | Result |
   | --- | --- |
   | `test_cache_Update_L_to_S` alone, 25 runs | 25 pass |
   | `ctest -R "^test_cache_" -j 8`, 12 iterations | 1 crash (`Update_S_to_L`) |
   | `ctest -R "^test_cache_" -j 8`, 30 iterations | 1 crash 
(`Alternate_S_to_L`) |
   
   It never fails standalone, and it lands on a different test in the
   Update/Alternate family each time, which is what pointed at shared code 
rather
   than any one test.
   
   An ASan build did not reproduce it in 15 iterations of the same loop, and 
runs
   supervised by gdb did not reproduce it in 120 attempts — both perturb the 
timing
   window. A concurrently running unsupervised `Alternate_L_to_S` did crash and
   dump a core, which is where the backtrace below comes from.
   
   ## Backtrace
   
   ```
   #0  0x0000ffffb0137e50 in strrchr () from /lib64/libc.so.6
   #1  0x00000000008e0708 in SourceLocation::str (this=0x2cc09858, buf=..., 
buflen=128)
         at src/tsutil/SourceLocation.cc:48
   #2  0x00000000008669e4 in lock_waiting (srcloc=..., handler=0x0)
         at src/iocore/eventsystem/Lock.cc:47
   #3  0x0000000000472dd0 in Mutex_trylock (location=..., ahandler=0x0, 
m=0x2cc09800, t=...)
         at include/iocore/eventsystem/Lock.h:264
   #4  0x0000000000472d04 in Mutex_trylock (...) at 
include/iocore/eventsystem/Lock.h:303
   #5  0x00000000004707f0 in MutexTryLock::MutexTryLock (...) at 
include/iocore/eventsystem/Lock.h:590
   #6  0x000000000049fbb8 in CacheVC::openReadStartHead (this=..., event=2, 
e=...)
         at src/iocore/cache/CacheRead.cc:1014
   #7  0x0000000000468064 in Continuation::handleEvent (...)
   #8  0x0000000000868464 in EThread::process_event (...) at 
src/iocore/eventsystem/UnixEThread.cc:171
   #9  0x0000000000868b48 in EThread::execute_regular (...)
   ```
   
   `m = 0x2cc09800` is the `ProxyMutex`; `this = 0x2cc09858` in frame #1 is
   `m->srcloc` inside it.
   
   ## Analysis
   
   `Mutex_trylock`, `include/iocore/eventsystem/Lock.h:261`:
   
   ```cpp
     if (m->thread_holding != t) {
       if (!ink_mutex_try_acquire(&m->the_mutex)) {
   #ifdef DEBUG
         lock_waiting(m->srcloc, m->handler);   // line 264
   #endif
         return false;
       }
   ```
   
   The thread reaching line 264 just *failed* to acquire the mutex, so it holds
   nothing, yet it reads `m->srcloc` and `m->handler`. Those fields are owned by
   whoever holds the mutex and are written non-atomically:
   
   - `Lock.h:276-277` (`Mutex_trylock`, after acquiring)
   - `Lock.h:323-324` (`Mutex_lock`, after acquiring)
   - `Lock.h:370-371` (`Mutex_unlock`, cleared *before* `ink_mutex_release`)
   
   `SourceLocation::operator=` copies `file`, `func` and `line` as three 
separate
   stores, so a racing reader can observe a mixture of old and new fields.
   `SourceLocation::valid()` only checks `file && line`
   (`include/tsutil/SourceLocation.h:49`), so a stale `file` with a non-zero 
`line`
   passes validation and `strrchr(file, '/')`
   (`src/tsutil/SourceLocation.cc:48`) faults.
   
   Three conditions have to coincide, which is why this is so rare and why it 
only
   ever shows up in these tests:
   
   1. **A Debug build.** The whole block is `#ifdef DEBUG`, defined by
      `CMakeLists.txt:63` when `CMAKE_BUILD_TYPE` is `Debug` — that is the `ci`
      preset family, including `ci-fedora-cxx20`.
   2. **The `locks` debug tag enabled.** `lock_waiting()` only calls
      `SourceLocation::str()` inside `if (dbg_ctl_locks.on())`
      (`src/iocore/eventsystem/Lock.cc:45-49`). The cache unit tests are the 
only
      place that turns it on:
      `src/iocore/cache/unit_tests/main.cc:132` does
      `diags()->activate_taglist("cache.*|agg.*|locks", DiagsTagType_Debug)`.
   3. **Lock contention.** Line 264 is reached only when `ink_mutex_try_acquire`
      fails, which is why parallelism matters and a solo run never trips it.
   
   The two `WARNING: holding lock ... too long for UNKNOWN` lines in the CI 
output
   come from `lock_holding()` in `Mutex_unlock`, which *is* called while 
holding the
   lock and is safe. They are the tell that the `locks` tag was active and that
   contention was occurring — the precondition for the crashing `lock_waiting` 
path.
   
   ## Possible fix
   
   `lock_waiting()` is the only one of these that reads holder state without
   holding the lock. Reporting the *waiter's* `location` — already passed in, 
always
   a `__FILE__` literal, never raced — instead of `m->srcloc` removes the race
   outright. That does drop "who is holding it" from the message, so if that
   information is worth keeping the alternative is to make the debug fields
   atomically publishable rather than three plain stores. Happy to put up a PR
   either way once there is a preference.
   
   ## Environment
   
   - `master` at b9b9109864, unmodified.
   - `cmake --preset ci-fedora-cxx20 -DBUILD_TESTING=ON`, clang, 
`CMAKE_BUILD_TYPE=Debug`.
   - Reproduced in a Fedora 44 container. Note this was **aarch64**, whereas 
the CI
     job that failed is x86_64; same source and same preset, different 
architecture.
     The CI log's evidence is consistent with this mechanism but we have not 
proven
     the CI crash is the identical one.
   - The code involved is long-standing, so 10.2.x and earlier are very likely
     affected too.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to