masaori335 commented on PR #13409:
URL: https://github.com/apache/trafficserver/pull/13409#issuecomment-5100594795
I'm just forwarding message, but P0 and P1 seems blocker. P4 is pretty
interesting, this fixed a leak silently, which I introduced, thank you :)
----
# Review: `Add live HostDB cache clearing` (aa9fabc9c6)
The design is sound: `probe()` returns a `Ptr<HostDBRecord>` and
`ResolveInfo::record`
holds it, so in-flight transactions genuinely survive a clear. The
status-handler
rewrite also silently fixes a real refcount leak. But the new `clear()`
exposes a
pre-existing lock-discipline hole in a way that is now operator-triggerable.
Findings are ranked P0 (blocking, memory-unsafe) through P4 (nit).
---
## P0 — `clear()` races with unlocked `erase()`: data race / use-after-free
`RefCountCache::clear()` now takes the partition's exclusive lock
(`src/iocore/hostdb/P_RefCountCache.h:513`), which correctly excludes
`probe()`
(shared lock, `src/iocore/hostdb/HostDB.cc:482`) and `put()` (unique lock,
`src/iocore/hostdb/HostDB.cc:1000`).
It does **not** exclude the two `erase()` call sites that take no partition
lock at all:
- `src/iocore/hostdb/HostDB.cc:887` — `dnsEvent()` NXDOMAIN path,
`hostDB.refcountcache->erase(old_r->key)`. This is a common path.
- `src/iocore/hostdb/HostDB.cc:405` — `reply_to_cont()`, reached unlocked
from
`dnsEvent` (`:1030`) and `probeEvent` (`:1109`).
`erase()` mutates the intrusive hash map, mutates the `PriorityQueue
expiry_queue`,
and frees the `RefCountCacheHashEntry` back to the ClassAllocator.
Concurrently the
RPC thread walks and destroys that same map under a lock the ET_NET thread
never
takes. Result: corrupted bucket/expiry lists, double-free, or UAF.
Before this commit `clear()` was only ever called from unit tests, so the
unlocked
`erase()` sites raced only against each other and against `put()` on the same
partition — a much narrower window. This commit turns it into "operator runs
`traffic_ctl hostdb clear` during an NXDOMAIN storm."
**This is not a one-line fix.** `getby()` holds a *shared* lock at
`src/iocore/hostdb/HostDB.cc:580` and calls `reply_to_cont()` at `:600`,
which can
call `erase()`. `ts::shared_mutex` wraps a non-recursive, writer-preferring
`pthread_rwlock_t`, so naively adding a `unique_lock` inside `erase()`
self-deadlocks
that path. Either hoist the erase out of `reply_to_cont()`, or restructure
so `getby()`
releases the shared lock before calling it.
**Strongly recommended:** annotate the partition state so the compiler
enumerates the
violations instead of relying on review —
```cpp
hash_type item_map TS_GUARDED_BY(lock);
PriorityQueue<RefCountCacheHashEntry *> expiry_queue TS_GUARDED_BY(lock);
void erase(uint64_t key, ink_time_t expiry_time = -1) TS_REQUIRES(lock);
```
Note this will only flag on macOS/libc++ — on Fedora CI the std lock types
are not
capabilities, so `-Wthread-safety` will stay quiet even with `AS_ERROR=ON`.
## P1 — Null deref if `clear` is called during startup
`initialize_jsonrpc_server()` runs at
`src/traffic_server/traffic_server.cc:2143`;
`hostDBProcessor.start()` at `:2365`. Between them:
`eventProcessor.start()`, thread
spawn, `Log::init()`. `hostDB.refcountcache` is `nullptr` until
`HostDBCache::start()`
(`src/iocore/hostdb/HostDB.cc:289`). A `traffic_ctl hostdb clear` in that
window
segfaults traffic_server.
`get_hostdb_status` has the same pre-existing hole (its `try`/`catch` does
not catch
SIGSEGV), so one shared guard fixes both:
```cpp
if (hostDB.refcountcache == nullptr) {
return {errata: "HostDB is not initialized"};
}
```
## P2 — The headline behavior is untested
`tests/gold_tests/dns/hostdb_clear.test.py` covers only the happy path.
Nothing
verifies that an in-flight transaction survives a clear — the one behavior
the commit
message and both doc changes promise, and the one most likely to silently
regress.
## P3 — Emptiness assertion is a weak negative
The test asserts `ExcludesExpression(hostname)` on `traffic_ctl hostdb
status` output.
That passes for the wrong reasons too (RPC error, empty response, changed
formatting).
Assert on the metric instead: `proxy.process.hostdb.cache.current_items ==
0`.
## P3 — No audit log for a destructive operation
Wiping the entire DNS cache emits nothing to `diags.log`. Add a
`Note("HostDB cleared via JSONRPC")` in `clear_hostdb()`
(`src/mgmt/rpc/handlers/hostdb/HostDB.cc:212`).
## P3 — `RefCountCachePartition::copy()` is now dead code
The status handler was its only caller. Delete the declaration
(`src/iocore/hostdb/P_RefCountCache.h:176`) and definition (`:346-355`).
## P3 — Prefer `ts::write_guard` over `std::unique_lock`
`include/tsutil/TsSharedMutex.h:188` explicitly directs new code to the
capability-carrying guards. The surrounding `std::unique_lock` uses in
`HostDB.cc` are
legacy, not convention. Relevant to the P0 annotation work above.
## P3 — Writer-lock stall on ET_NET
Freeing up to `max_count / partitions` entries per partition under an
exclusive lock
blocks ET_NET threads in `probe()`, and Linux's writer-preferring rwlock
queues new
readers behind it. Bounded per partition, so likely fine — but consider
scheduling the
per-partition clears on ET_TASK rather than running them inline on the RPC
thread.
## P4 — Leak fix is real but unmentioned
The old status handler called `partition.copy()`, which `alloc()`s a
`RefCountCacheHashEntry` per record and `make_ptr()`s the item (refcount++),
then
dropped the raw pointers on the floor. Every `traffic_ctl hostdb status`
leaked one
hash entry and one permanent refcount per record — meaning those records
could never
be freed. The new `Ptr`-based collection fixes it. Worth a line in the
commit message
so it is not lost.
## P4 — Docs imply host-file entries are cleared
Host-file entries are correctly left alone (they are config, not cache), but
"Remove
all DNS resolution records from HostDB" reads like they would be affected. A
clarifying clause in both `doc/appendices/command-line/traffic_ctl.en.rst`
and
`doc/developer-guide/jsonrpc/jsonrpc-api.en.rst` would help.
## P4 — Argument-count rejection untested
`add_command("clear", ..., 0, ...)` declares arity 0, but no test exercises
`traffic_ctl hostdb clear extra_arg`.
## P4 — Inconsistent `StillRunningAfter` in the last test run
The final test run sets only `tr.StillRunningAfter = ts`, dropping `dns` and
`server`
unlike every preceding run. Harmless, but inconsistent.
---
**Merge gate:** P0 and P1 should block. P2 is worth doing before merge,
since the
in-flight guarantee is the feature's main safety claim. P3/P4 are cleanups.
--
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]