Summary
Binder HTML rendering invokes bind.Formatter, fmt.Formatter, and fmt.Stringer behavior while holding the caller-supplied Binder lock. A normal formatter that locks the same synchronized state self-deadlocks and leaves every related operation blocked.
Class / severity
Concurrency — Medium. One page render can permanently stall all reads and writes sharing the Binder lock.
Contract and valid usage
Binder methods are documented as concurrency-safe when the supplied locker is safe. Explicit Binder hooks clearly document when the lock is held; Formatter.Format and ordinary formatting methods have no no-lock/reentry obligation. It is idiomatic for a formatter over synchronized state to acquire that state's mutex.
Reproduction
type value struct{ mu *sync.Mutex }
func (v value) Format(string) string {
v.mu.Lock()
defer v.mu.Unlock()
return "formatted"
}
func TestBinderFormatterDoesNotDeadlock(t *testing.T) {
var mu sync.Mutex
v := value{mu: &mu}
b := bind.New(&mu, &v).Format("ignored")
done := make(chan struct{})
go func() {
_ = bind.MakeHTMLGetter(b).JawsGetHTML(nil)
close(done)
}()
select {
case <-done:
case <-time.After(100 * time.Millisecond):
t.Fatal("Formatter called while Binder lock was held")
}
}
The focused regression consistently remains blocked.
Root cause
binder.JawsGetHTML holds RWLocker.RLock across jawsGetHTMLLocked. That function invokes Formatter.Format, fmt.Sprintf, or default fmt.Sprint while still under the lock (lib/bind/binder.go, around lines 217-239).
Impact
The render goroutine and all other state operations sharing the lock hang indefinitely.
Suggested fix
For Format/default rendering, copy the bound value under the lock, release it, and only then invoke arbitrary formatting methods. Preserve the explicitly documented locked behavior for GetHTMLHook.
Summary
Binder HTML rendering invokes
bind.Formatter,fmt.Formatter, andfmt.Stringerbehavior while holding the caller-supplied Binder lock. A normal formatter that locks the same synchronized state self-deadlocks and leaves every related operation blocked.Class / severity
Concurrency — Medium. One page render can permanently stall all reads and writes sharing the Binder lock.
Contract and valid usage
Binder methods are documented as concurrency-safe when the supplied locker is safe. Explicit Binder hooks clearly document when the lock is held;
Formatter.Formatand ordinary formatting methods have no no-lock/reentry obligation. It is idiomatic for a formatter over synchronized state to acquire that state's mutex.Reproduction
The focused regression consistently remains blocked.
Root cause
binder.JawsGetHTMLholdsRWLocker.RLockacrossjawsGetHTMLLocked. That function invokesFormatter.Format,fmt.Sprintf, or defaultfmt.Sprintwhile still under the lock (lib/bind/binder.go, around lines 217-239).Impact
The render goroutine and all other state operations sharing the lock hang indefinitely.
Suggested fix
For Format/default rendering, copy the bound value under the lock, release it, and only then invoke arbitrary formatting methods. Preserve the explicitly documented locked behavior for
GetHTMLHook.