Skip to content

bookmarks: App's fetch-timer control test segfaults intermittently under CI load #391

Description

@Yaraslaut

ladder_bookmarks_tests segfaulted once in CI on the control half of the
stopBackgroundJobs() pair. It has not reproduced since, so this is filed with
the evidence that exists rather than a recipe.

What happened

Linux / all optional features (clang), ctest:

1524/2475 Test #1524: App's fetch timer really does fire under a pumping loop (the control for stopBackgroundJobs) ...***Exception: SegFault  0.22 sec

The following tests FAILED:
	1524 - App's fetch timer really does fire under a pumping loop (the control for stopBackgroundJobs) (SEGFAULT) ladder-bookmarks
Errors while running CTest

99% tests passed, 1 tests failed out of 1524 — the segfault was the only failure
in the run.

Verification status

Reproduced: once, in CI, not on demand.

  • Revision fb3f4c9a (branch ci/self-hosted-linux-runner), run
    33603372999 attempt 1,
    self-hosted runner morph-docker-1, started 2026-09-02T07:40:22Z.
  • The runner is a Docker container pinned to 2 processors (--cpuset-cpus) with
    a 6 GiB cap, one of five such containers compiling concurrently on a
    12-processor host. The machine was heavily loaded, which is the one
    environmental difference worth noting against every green run of this test.

Not reproduced, four ways:

  • 40/40 passes running that single test in a loop locally, same revision, on a
    binary built through the compiler cache.
  • The Application ladder / ASan+UBSan leg of the same CI run executed the
    same test and passed: 208/976 Test #1512: ... Passed 0.17 sec, and the leg
    finished 100% tests passed, 0 tests failed out of 976. So neither ASan nor
    UBSan flagged anything on the path that crashed.
  • The rerun of the failed job (attempt 2) was green, 26/26.
  • Three earlier full runs on adjacent revisions were green.

Not verified: no core dump or backtrace was captured — ctest reports only
SegFault, and the container is gone. Everything below the next heading is read
from the source, not observed.

Mechanism — inferred from reading, NOT reproduced

examples/bookmarks/tests/test_app.cpp:370 pumps until a timer-driven pass has
dispatched, pumps until fetchInFlight() is false, then lets App destruct at
scope exit.

The parts that are already defensive, and so are probably not it:

  • App::~App() (examples/bookmarks/src/app/app.cpp:167) calls
    stopBackgroundJobs() first, with a comment saying exactly why.
  • The in-flight counter is a shared_ptr<atomic> captured by value into
    .then()/.onError(), explicitly so a late completion outliving the App
    can still decrement it safely.

The part that does not have that protection: the per-pass handler at
examples/bookmarks/src/app/app.cpp:216 is built over _fetchBridge and
&_fetchExecutor, both App members:

auto handler = std::make_shared<::morph::bridge::BridgeHandler<BookmarkModel>>(_fetchBridge, &_fetchExecutor);

The shared_ptr keeps the handler alive past ~App, but not the executor it
points at. If any queued delivery reaches that handler after ~App has run,
it touches a destroyed member. fetchInFlight() == false says every dispatch
settled its counter; it is not obviously the same statement as "no queued event
referencing _fetchExecutor remains", and the test destroys the App
immediately on that signal.

This is a hypothesis that fits the crash site and the load-sensitivity. It is
not evidence, and it may be wrong.

What would change the verdict

  • Close if the mechanism above is disproved (e.g. the executor provably
    cannot be reached after ~App) and the crash is traced elsewhere, or if it is
    fixed and a stress loop under artificial load stays green.
  • Reopen / raise severity if it recurs, especially on a GitHub-hosted runner
    or an unloaded machine — that would make it a plain lifetime bug rather than
    one that needs contention to surface.
  • Worth trying to force before doing either: run this test under heavy CPU
    contention in a loop, and/or shorten kFastFetchInterval, to see whether it
    becomes reproducible on demand. A version of it that reproduces is worth more
    than this report.

The self-hosted runners make this class of failure more likely to be seen,
because a 2-processor container under load stretches exactly the windows a
same-thread event-loop test relies on staying closed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ladderSubsystem: ladderarea: qtSubsystem: qtbugSomething isn't workingtriage: rescopeReal problem, wrong framing; rewrite before building

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions