Skip to content

DeviceLab Android: nil-pointer panic in webViewManager.refreshPage kills the whole run #149

Description

@laiskajoonas

Summary

On the DeviceLab Android driver, webViewManager.refreshPage can hit a nil-pointer dereference inside go-rod and take down the entire runner process — every in-flight flow plus every flow not yet scheduled. On a 19-flow --parallel 4 suite this turns one flaky WebView into a total run loss: 4 flows die mid-execution and 9 never start.

It reproduced 3 times in a single day on our CI with an identical stack.

Environment

  • maestro-runner 1.1.24 (linux-amd64), --driver devicelab --parallel 4
  • Android emulators, SDK 35, x86_64, headless CI (4 emulators on one host)
  • App under test is React Native / Expo; the WebView involved is expo-print's offscreen WebView, created transiently to render a PDF

I haven't confirmed on 1.1.25, but its release notes carry no WebView or go-rod change, so I assume it's still present.

Stack

panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x10 pc=0x8b7bbc]

goroutine 310 [running]:
github.com/ysmood/goob.(*Observable).Subscribe(0x0, {0x12f8840?, 0x247ee47e8a00?})
	/Users/omnarayan/work/go/pkg/mod/github.com/ysmood/goob@v0.4.0/goob.go:37 +0x3c
github.com/go-rod/rod.(*Browser).Event(0x247ee3e145a0)
	/Users/omnarayan/work/go/pkg/mod/github.com/go-rod/rod@v0.116.2/browser.go:415 +0x2e
github.com/go-rod/rod.(*Page).initEvents(0x247ee4770bb0)
	/Users/omnarayan/work/go/pkg/mod/github.com/go-rod/rod@v0.116.2/page.go:1041 +0x1ab
github.com/go-rod/rod.(*Browser).PageFromTarget(0x247ee3e14480, {0x247ee3ffd680, 0x20})
	/Users/omnarayan/work/go/pkg/mod/github.com/go-rod/rod@v0.116.2/browser.go:309 +0x615
github.com/go-rod/rod.(*Browser).Pages(0x247ee3e14480)
	/Users/omnarayan/work/go/pkg/mod/github.com/go-rod/rod@v0.116.2/browser.go:229 +0x11f
github.com/devicelab-dev/maestro-runner/pkg/driver/devicelab.(*webViewManager).refreshPage(0x247ee4048360)
	/Users/omnarayan/work/go/src/maestro-runner/pkg/driver/devicelab/webview.go:447 +0x8e
github.com/devicelab-dev/maestro-runner/pkg/driver/devicelab.(*webViewManager).findWebOnce(_, {{0x0, 0x0}, {0x247ee4202288, 0x18}, 0x0, 0x0, 0x0, 0x0, 0x0, ...})
	/Users/omnarayan/work/go/src/maestro-runner/pkg/driver/devicelab/webview.go:513 +0xea
github.com/devicelab-dev/maestro-runner/pkg/driver/devicelab.(*Driver).findElementOnce(_, {{0x0, 0x0}, {0x247ee4202288, 0x18}, 0x0, 0x0, 0x0, 0x0, 0x0, ...})
	/Users/omnarayan/work/go/src/maestro-runner/pkg/driver/devicelab/driver.go:1222 +0x13c
github.com/devicelab-dev/maestro-runner/pkg/driver/devicelab.(*Driver).waitUntil(0x247ee4462308, 0x247ee401c2d0)
	/Users/omnarayan/work/go/src/maestro-runner/pkg/driver/devicelab/commands.go:2047 +0x519
github.com/devicelab-dev/maestro-runner/pkg/driver/devicelab.(*Driver).Execute(0x247ee4462308, {0x12fcf80, 0x247ee401c2d0})
	/Users/omnarayan/work/go/src/maestro-runner/pkg/driver/devicelab/driver.go:567 +0x15a
github.com/devicelab-dev/maestro-runner/pkg/flutter.(*FlutterDriver).Execute(0x247ee3de0000, {0x12fcf80, 0x247ee401c2d0})
	/Users/omnarayan/work/go/src/maestro-runner/pkg/flutter/wrapper.go:92 +0x4b4
github.com/devicelab-dev/maestro-runner/pkg/executor.(*FlowRunner).executeStep(0x247ee4781618, 0x11, {0x12fcf80, 0x247ee401c2d0})
	/Users/omnarayan/work/go/src/maestro-runner/pkg/executor/flow_runner.go:538 +0x1837
github.com/devicelab-dev/maestro-runner/pkg/executor.(*FlowRunner).Run(0x247ee4781618)
	/Users/omnarayan/work/go/src/maestro-runner/pkg/executor/flow_runner.go:237 +0xbeb
github.com/devicelab-dev/maestro-runner/pkg/executor.(*Runner).executeFlow(_, {_, _}, {{0x247ee40aa150, 0x18}, {{0x247ee3e90780, 0x9}, {0x0, 0x0}, {0x0, ...}, ...}, ...}, ...)
	/Users/omnarayan/work/go/src/maestro-runner/pkg/executor/runner.go:233 +0x18b

goob.(*Observable).Subscribe(0x0, …) — the receiver is nil, i.e. Browser.event is nil at the point Pages()PageFromTarget()initEvents()Browser.Event() runs. That reads like a rod.Browser that was torn down (or never fully connected) being used concurrently by refreshPage.

What triggers it

A selector wait that spans the lifetime of a short-lived, offscreen WebView.

The flow taps a button that starts PDF generation, then waits for the screen to change:

- tapOn:
    id: "account-statement-button"
- extendedWaitUntil:
    notVisible:
      id: "account-statement-button"
    timeout: 60000

While expo-print's offscreen WebView is alive, every poll iteration falls through the native lookup into findWebOnce. Two things make this a wide window:

  1. The miss path is cheap and unthrottled. 5,664 of the 5,665 WebView probes in the crashed run logged — webview not visible (WebView attached, not on screen). Sustained rate was ~400 probes/sec for 14s, with no backoff. A full 60s budget is ~24,000 attempts at the race.
  2. It's not specific to id: selectors. A text: selector reaches the same call — in a passing run I see [webview] findWebOnce failed: JS findByText failed: … , refreshing page… for a plain text wait. So any wait for a not-yet-present element during WebView teardown is exposed.

For contrast, in the same suite every other selector combined accounted for 9 WebView probes. All the exposure is concentrated in waits that overlap a WebView's lifetime.

Impact

A panic in a worker goroutine is process-fatal in Go, so there's no per-flow containment: one racing WebView loses the whole suite, including flows that never ran. Our CI works around it by classifying the unexecuted flows as infrastructure loss and re-running them, but that's a workaround for a crash, not a fix.

Suggestions

  1. Guard refreshPage against a browser whose event Observable is gone — a nil check on the rod.Browser state before Pages(), and/or recover() around the WebView find path so a CDP-side failure fails the step rather than the process.
  2. Back off the webview not visible path the way ensureWebViewConnection already backs off a stalled devtools socket (1.1.22). ~400 probes/sec against a WebView that was just reported invisible is mostly wasted work, and it's what widens the race window.
  3. An opt-out would help as a stopgap — something like MAESTRO_NO_WEBVIEW=1 to skip CDP element finding entirely for suites with no WebView assertions. I couldn't find an existing switch for this.

Happy to supply more log context if useful — the runner logs are ~18k lines, most of it the probe loop.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions