Skip to content

nest: nil-pointer panic in StartExtendStreamTimer (race with StopExtendStreamTimer) crash-loops Nest WEB_RTC sources #2319

Description

@tofu713

Describe the bug

The nest: source crash-loops with a nil-pointer dereference panic in StartExtendStreamTimer. It became reproducible after Google migrated Nest accounts to the Google Home app: StreamExpiresAt now reports a very short remaining lifetime, so the extend-stream timer churns rapidly and reliably loses a race between the timer goroutine and StopExtendStreamTimer.

Version

1.9.14 — also present on current master (pkg/nest/api.go is unchanged).

Panic

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation]
goroutine N [running]:
github.com/AlexxIT/go2rtc/pkg/nest.(*API).StartExtendStreamTimer.func1()
        .../pkg/nest/api.go:474

Root cause

func (a *API) StartExtendStreamTimer() {
    if a.extendTimer != nil {
        return
    }
    a.extendTimer = time.NewTimer(time.Until(a.StreamExpiresAt) - time.Minute)
    go func() {
        <-a.extendTimer.C   // (api.go:474) reads a.extendTimer when the goroutine runs
        ...
    }()
}

func (a *API) StopExtendStreamTimer() {
    if a.extendTimer != nil {
        a.extendTimer.Stop()
        a.extendTimer = nil  // niled concurrently with the goroutine above
    }
}

The goroutine evaluates a.extendTimer.C when it runs. If StopExtendStreamTimer has already set a.extendTimer = nil (stream teardown / re-arm), the expression dereferences a nil *time.Timer and panics. With the shortened post-migration expiry, start/stop cycles happen constantly, so the race fires almost every time and the source never stays up.

Steps to reproduce

  1. Add a Nest source over WebRTC: nest:?client_id=...&client_secret=...&refresh_token=...&project_id=...&device_id=...&protocols=WEB_RTC
  2. Consume it (e.g. via RTSP/WebRTC). The producer starts, then panics within a few extend cycles and crash-loops.

Suggested fix

Capture the timer channel locally so the goroutine never dereferences a.extendTimer:

a.extendTimer = time.NewTimer(time.Until(a.StreamExpiresAt) - time.Minute)
ch := a.extendTimer.C
go func() {
    <-ch
    ...
}()

A done-channel variant additionally avoids a goroutine leak when the timer is stopped before it fires (the goroutine would otherwise block on the channel forever). Happy to open a PR.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions