Skip to content

throttleRecord: take the record lock in delayFuncCallback before valuePut#27

Open
physwkim wants to merge 1 commit into
epics-modules:masterfrom
physwkim:fix/throttle-callback-lock
Open

throttleRecord: take the record lock in delayFuncCallback before valuePut#27
physwkim wants to merge 1 commit into
epics-modules:masterfrom
physwkim:fix/throttle-callback-lock

Conversation

@physwkim

Copy link
Copy Markdown

delayFuncCallback() runs on the general callback thread pool and calls valuePut() with no dbScanLock. valuePut() mutates record fields and calls dbPutLink(&prec->out), recGblFwdLink(), recGblResetAlarms() and db_post_events() — all of which require the record lock; dbPutLink/recGblFwdLink also reach into another record's lockset (memory-unsafe unlocked on SMP).

It races process()enterValue() on delay_flag: the scan thread can read the stale flag while the callback clears it, stranding a value in prec->val with no OUT write, no FLNK and no timer re-arm — a stalled throttle — plus torn sts/sent writes.

The sibling callback checkLinkCallback() in the same file already wraps its body in dbScanLock/dbScanUnlock; this makes delayFuncCallback match it. delayFuncCallback is armed only during processing (never at init), so unlike checkLinkCallback it needs no interruptAccept guard.

delayFuncCallback() runs on the general callback thread pool (armed via
callbackRequestDelayed() from valuePut() and enterValue()) and called
valuePut() with no dbScanLock. valuePut() mutates record fields
(wait_flag, delay_flag, sts, sent, wait), fires the output link with
dbPutLink(&prec->out), and runs recGblFwdLink(prec), recGblResetAlarms()
and db_post_events() -- all of which EPICS requires the record lock to be
held for. dbPutLink/recGblFwdLink additionally reach into another
record's lockset, which is memory-unsafe when performed unlocked on SMP.

valuePut() is reentrant against process(): a scan thread runs process()
holding the record lock and reaches valuePut() through enterValue(),
while the callback thread runs valuePut() with no lock at all. The two
paths race on the same record. Observable failure: process()->enterValue()
reads delay_flag while the callback's valuePut() is concurrently clearing
it (delay_flag = 0); enterValue() sees the stale delay_flag == 1 and
returns without writing, yet the callback has already passed its
wait_flag test -- so the value is left in prec->val with wait_flag set,
no OUT write, no FLNK, and no timer re-armed, and the throttle stalls
until an unrelated later process. The torn writes to sts/sent are the
same race's other face. Taking the record lock around valuePut()
serializes the callback-thread mutation against the scan-thread
process()/enterValue() path, closing the race.

The omission is a slip, proven by this file against itself: the other
callback, checkLinkCallback(), already wraps its body in dbScanLock()/
dbScanUnlock(). delayFuncCallback is armed only during record processing,
never at init, so unlike checkLinkCallback it needs no interruptAccept
guard -- only the lock, matching checkLinkCallback's else branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant