Skip to content

bind: Formatter and Stringer callbacks run under Binder lock and can deadlock #136

Description

@linkdata

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinggoPull requests that update go code

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions