Skip to content

[DO NOT MERGE] PaddleOCR crash diagnostics on top of sockets-server-posix - #1812

Closed
pa-devbot wants to merge 15 commits into
sockets-server-posixfrom
mac-paddleocr-crash-diagnostics
Closed

pa-devbot wants to merge 15 commits into
sockets-server-posixfrom
mac-paddleocr-crash-diagnostics

Conversation

@pa-devbot

Copy link
Copy Markdown
Collaborator

What this is

A [DO NOT MERGE] experiment branch for the handoff in #1811 (PaddleOCR/ONNX Runtime segfault in the C++ CI Serial Programs Mac Intel job, exit 139 during the first PaddleOCR ONNX session creation).

Based on sockets-server-posix @ 77e2f4f because that is the tree where the crash has been observed (runs 41266441051 / 41275162708). The diff against that base is diagnostics only - no behavior changes.

What it does

  1. Temporary [ORT-DIAG] stderr markers (revert when done):

    • ML/Models/ML_OrtEnv.cpp: prints the ONNX Runtime version, hardware_concurrency() and the chosen intra/inter-op thread counts (the file logger output is not in the CI log), and runs the global Ort::Env at ORT_LOGGING_LEVEL_INFO so ONNX Runtime's own session-creation log (Session Options trace, thread pool params, graph transforms, BFCArena, ...) lands in the CI log.
    • ML/Models/ML_ONNXRuntimeHelpers.cpp: markers around clean_up_old_model_cache / the Ort::Session constructor in create_session().
    • ML/Inference/ML_PaddleOCRPipeline.cpp: markers at the end of the PaddleOCRPipeline ctor and before the first session->Run() calls.
  2. (Pending - see below) CI workflow changes that, on a Mac test failure:

    • dump the macOS crash reports ($HOME/Library/Logs/DiagnosticReports/*.ips - symbol names + imageOffset + image UUIDs, matchable against 3rdPartyBinaries/onnxruntime-osx-x86_64-1.23.2.tgz),
    • upload them as an artifact,
    • rerun the suite under lldb with 'thread backtrace all'.

Status / next steps

  • The workflow part of this PR is not pushed yet: the bot's token is missing the 'workflow' scope, so pushes that modify .github/workflows are rejected by GitHub. @kuroneko could add the 'workflow' scope to the pa-devbot PAT (or push the workflow commit himself - it is commit 37133db on this branch's history, also attached as a patch in the comments).
  • Each push re-runs Mac Intel; if the run passes, I push a trivial commit to get another attempt (the crash is probabilistic, ~3/5 runs on this tree).
  • Once the crash reproduces with this instrumentation, the log should show the exact stage (ORT INFO lines + [ORT-DIAG] markers), and the .ips / lldb backtrace the faulting thread.
  • No code here changes PaddleOCR behavior; the only functional difference vs the base is the ORT env logging level and extra stderr lines.

Implements ServerSocket_Qt (a QTcpServer on the Qt event thread pool,
mirroring ClientSocket_Qt; the implementation lives in
ServerSocket_Qt.cpp, following the split of the client backends) and
adds loopback tests that combine a ClientSocket and a ServerSocket
against each other on 127.0.0.1.

The test logic is written against the abstract
AbstractClientSocket/AbstractServerSocket classes and is instantiated
per backend; this PR instantiates the Qt backend on all platforms. The
POSIX and WinSocket backends (and their test instantiations) land in
separate PRs on top of this one, so that every platform runs 2 of the
3 backends and CI as a whole covers all 3.

Scenarios: echo round trip, zero-length sends, a 1 MiB transfer with
integrity check, rapid reconnect cycles, a send/close race, rejection
of a second client, and a port-in-use start failure.

Also documents that ServerSocket::close_connection() is safe to call
from within a listener callback (close() is not: it joins the worker
threads).
@pa-devbot

Copy link
Copy Markdown
Collaborator Author

Pending: the crash-report / lldb workflow commit

The token used here lacks the `workflow\ scope, so I could not push the CI-instrumentation commit. It is commit \37133db\ (against this branch). Once the pa-devbot PAT has the \workflow\` scope (or this commit is pushed by someone else), it can be applied with:

git fetch origin mac-paddleocr-crash-diagnostics  # or apply the patch below
git am /path/to/0001-Mac-CI-capture-crash-reports.patch
git push

Patch (commit 37133db):

From 37133db7a3f157c97eb34f5ac199a4318986c18c Mon Sep 17 00:00:00 2001
From: PA Dev Bot <344001113+pa-devbot@users.noreply.github.com>
Date: Sun, 1 Nov 2026 06:47:46 +0000
Subject: [PATCH] Mac CI: capture crash reports and lldb backtrace when the
 test run dies

On a segfault the macOS crash reporter writes a .ips report to
~/Library/Logs/DiagnosticReports/, which is not in the Actions log and
the runner is wiped afterwards. Dump the SerialPrograms crash reports
to the log (and upload them as an artifact) on test failure, then rerun
the suite under lldb and print a backtrace of every thread.

Used to investigate the intermittent PaddleOCR/ONNX Runtime segfault in
the Mac Intel CI; see the PR description.
---
 .../workflows/cpp-ci-serial-programs-base.yml | 54 +++++++++++++++++++
 1 file changed, 54 insertions(+)

diff --git a/.github/workflows/cpp-ci-serial-programs-base.yml b/.github/workflows/cpp-ci-serial-programs-base.yml
index 58f1a9d89..b71425319 100644
--- a/.github/workflows/cpp-ci-serial-programs-base.yml
+++ b/.github/workflows/cpp-ci-serial-programs-base.yml
@@ -293,7 +293,61 @@ jobs:
         if: startsWith(inputs.os, 'mac') && inputs.run-tests
         run: |
           cd Arduino-Source/SerialPrograms/bin
+          set +e
           ./SerialPrograms.app/Contents/MacOS/SerialPrograms --command-line-test-mode
+          TEST_EXIT=$?
+          set -e
+          echo "Tests exited with code $TEST_EXIT (139 = segfault, 134 = abort)"
+
+      # The macOS crash reporter (ReportCrash) writes .ips reports for crashed processes to
+      # ~/Library/Logs/DiagnosticReports/. They are not part of the Actions log and the runner
+      # is wiped, so dump them here when the test run dies. Each .ips holds the faulting
+      # thread's stack (symbol names + imageOffset) and the image UUIDs, which can be matched
+      # against 3rdPartyBinaries/onnxruntime-osx-x86_64-*.tgz.
+      - name: Dump macOS crash reports (on test failure)
+        if: startsWith(inputs.os, 'mac') && inputs.run-tests && failure()
+        run: |
+          #  ReportCrash writes the report shortly after the crash; give it a moment.
+          sleep 10
+          mkdir -p crash-reports
+          echo "=== $HOME/Library/Logs/DiagnosticReports ==="
+          ls -la "$HOME/Library/Logs/DiagnosticReports/" || true
+          FOUND=0
+          for f in "$HOME/Library/Logs/DiagnosticReports/"*.ips; do
+            [ -e "$f" ] || continue
+            case "$(basename "$f")" in
+              SerialPrograms*|onnxruntime*) ;;
+              *) continue ;;
+            esac
+            FOUND=1
+            cp "$f" crash-reports/
+            echo "=== $f ==="
+            cat "$f"
+            echo
+          done
+          if [ "$FOUND" -eq 0 ]; then
+            echo "No SerialPrograms crash reports found in DiagnosticReports."
+          fi
+
+      - name: Upload crash reports
+        if: startsWith(inputs.os, 'mac') && inputs.run-tests && failure()
+        uses: actions/upload-artifact@v7
+        with:
+          name: Mac crash reports
+          path: crash-reports/
+          if-no-files-found: ignore
+
+      # Rerun the test suite under lldb and print a backtrace of every thread when it dies.
+      # This step is expected to fail again; the value is the "thread backtrace all" output.
+      - name: Rerun tests under lldb for a backtrace (on test failure)
+        if: startsWith(inputs.os, 'mac') && inputs.run-tests && failure()
+        run: |
+          cd Arduino-Source/SerialPrograms/bin
+          lldb -b \
+            -o run \
+            -k "thread backtrace all" \
+            -k quit \
+            ./SerialPrograms.app/Contents/MacOS/SerialPrograms --command-line-test-mode
 
       # The list of files that passed clang-query, from the latest run on this branch or on main.
       # See .github/scripts/clang-query-cached.py. Unlike ccache, every run saves it: it is only
-- 
2.43.0

AGENTS.md forbids using std::mutex/std::condition_variable directly;
use the PokemonAutomation::Mutex/ConditionVariable wrappers from
Common/Cpp/Concurrency instead. Migrate the Qt server's completion
structs and the test harness's Waiter/rng lock.
The method bodies kept their in-class 8-space indentation from the
monolithic header; un-dedent them to file scope (and drop the stray
blank lines the split left behind).
@pa-devbot
pa-devbot force-pushed the sockets-server-posix branch from 77e2f4f to 427b871 Compare November 1, 2026 07:24
The 5 s completion waits in send()/close_connection() were only safe
while the queued call ran within the window: the completion objects
were stack-local, so if the wait expired and the event thread
delivered the call later, on_send/on_close_connection wrote through
a dead stack pointer (stack-use-after-return; reproduced under
ASAN by stalling the event thread in on_receive_data for 6 s).

The signals now pass QSharedPointer<SendData>/QSharedPointer<CloseCompletion>
by value: the queued call's own copy keeps the payload alive for as
long as the call is pending or running (no UAF after a timed-out
caller), and if close() destroys the QTcpServer first, Qt discards
the queued call and the caller's copy is the last reference (no
leak). The 5 s bounds remain as plain safety valves.

Also add the regression test 'Sockets (Qt) - Completion lifetime
under event-thread stall', which stalls the event thread inside
on_receive_data, verifies close_connection() times out at ~5 s
instead of hanging, and that the late handler still restores
LISTENING. Takes ~12 s to run.
Implements ServerSocket_POSIX (an accept-loop thread plus one
connection thread at a time, both from the given thread pool via plain
dispatch(), with non-blocking sockets; the implementation lives in
ServerSocket_POSIX.cpp, following the split of the client backends)
and enables the POSIX instantiation of the loopback tests on
non-Windows.

close()/close_connection() only flip the state: the connection thread
notices within ~1 ms and closes the socket itself, so
close_connection() is safe to call from within a listener callback.
The accept loop keeps running while a connection is active and rejects
new clients via on_connect_rejected.

The connection task is started and published without holding the main
lock: assigning to the task slot destroys the previous connection
task's AsyncTask, whose destructor waits for that task to finish - and
the task may be blocked on the main lock.
send() with MSG_DONTWAIT can return after writing only part of the
buffer when the kernel's send buffer is full. macOS does this much
more readily than Linux, so the 1 MiB loopback transfer test failed
on the Mac CI: client.send() returned early with fewer bytes.

Loop until the whole buffer has been written, matching the Qt
backend's send() behavior.
The method bodies kept their in-class 8-space indentation from the
monolithic header; un-dedent them to file scope.
@pa-devbot
pa-devbot force-pushed the sockets-server-posix branch from 427b871 to ca15f0a Compare November 1, 2026 09:17
While investigating the intermittent Mac Intel PaddleOCR segfault:

- Print the ONNX Runtime version, hardware_concurrency() and the chosen
  intra/inter-op thread counts to stderr (the file logger is not in the
  CI log).
- Run the global Ort::Env at ORT_LOGGING_LEVEL_INFO so ONNX Runtime's
  own session-creation progress (Session Options, thread pool params,
  graph transforms, BFCArena, ...) lands in the CI log.
- Add [ORT-DIAG] stderr markers around create_session() and the first
  PaddleOCRPipeline::recognize_line() Run() calls, to pin down which
  stage the crash is in.

All of this is temporary: revert when the crash is understood.
…t mode

The CI log lost the output right up to the segfault because stdout is
fully buffered when piped: in the #1812 Mac Intel run the crash happened
~2.5 s after 'Running parallel unit tests...' and not a single line after
that was captured.

Two temporary diagnostics:

- A SIGSEGV/SIGBUS/SIGABRT/SIGILL/SIGFPE handler (command-line test mode
  only) that writes a native backtrace of the crashing thread straight to
  fd 2 with backtrace_symbols_fd(), then restores the default disposition
  and re-raises so the OS crash report and exit code are unchanged.
- setvbuf(stdout, _IOLBF) in test mode, so the test 'Starting:'/'Passed:'
  lines reach the log as they happen and the log shows the last thing the
  crashing process was doing.
Apple's execinfo.h declares backtrace_symbols_fd() as void (glibc's
returns int), so ignore its result on both. Also restore the CRLF line
endings the previous commit accidentally rewrote to LF.
setvbuf(stdout, nullptr, _IOLBF, 0) crashed the Windows CI runner with
0xC0000409 (stack-buffer-overrun) ~0.7 s into the test run, on the first
stdout write after the call: with size 0, MSVC's ucrt appears to install
a zero-size stdio buffer for stdout. 8192 is the conventional default.
The Mac Intel crash is probabilistic (~60% of runs) and lands in the
PaddleOCR ONNX session creation, which happens only once per process
because the sessions are cached. Run the full suite three times and
clear the PaddleOCR cache between runs so the sessions are re-created
each time, raising the per-CI-run reproduction odds to ~93%. Stop and
fail on the first reported test failure.
@pa-devbot
pa-devbot force-pushed the mac-paddleocr-crash-diagnostics branch from b3e20f5 to 48dc961 Compare November 1, 2026 09:42
@pa-devbot

Copy link
Copy Markdown
Collaborator Author

Reopening to re-trigger CI

@pa-devbot

Copy link
Copy Markdown
Collaborator Author

Investigation complete — the "PaddleOCR crash" was a stack-use-after-return in the Qt socket tests

Root cause. The probabilistic exit-139 on macos-15-intel was not in PaddleOCR/ONNX Runtime. It was a stack-use-after-return in the new Qt server-socket close-completion path (the PR #1793 test harness era): the connection-closed completion handler wrote to a stack-local CloseCompletion after the close_connection() frame that owned it had returned. The corrupt write landed on pool-thread stacks and exploded where that memory was next used — in the handoff runs that looked like PaddleOCR session creation; in my A/B run it crashed directly inside the socket test.

Proof (A/B).

  • This branch's diagnostics (stderr markers, fatal-signal backtrace handler, 3× suite loop with PaddleOCR cache cleared between runs so the ONNX sessions are re-created every time) were cherry-picked onto the old base 77e2f4f10 (PR [DO NOT MERGE] PaddleOCR crash A/B: same 3x diagnostics on the pre-fix sockets base (77e2f4f10) #1817): Mac Intel crashed in suite run 2, ~2.4 s into "Sockets (Qt) - Rapid reconnect":
    Test_Sockets_RapidReconnect<ClientSocket_Qt, ServerSocket_Qt>::run() → Waiter::wait_for → ~std::function<void()> (SIGSEGV).
  • On the current sockets-server-posix tip (which now contains 6f1eadf6d "ServerSocket_Qt: give signal completions shared ownership" plus the regression test "Sockets (Qt) - Completion lifetime under event-thread stall"), this branch's 3× loop passed 3 full suites (~26k tests each, 6 fresh PaddleOCR session creations) with zero failures. The base branch's own CI is green at its current tip too.

Also fixed along the way (in this branch's commits, in case you want to cherry-pick any):

  • setvbuf(stdout, nullptr, _IOLBF, 0) crashed the Windows CI runner with 0xC00000409 → 0xC0000409 (stack-buffer-overrun): with size 0, MSVC's ucrt ends up with a zero-size stdout buffer and the first write corrupts the stack. Use an explicit size (8192).
  • Apple's execinfo.h declares backtrace_symbols_fd() as void (glibc's returns int) — don't use its return value portably.

Recommendation. Nothing here needs merging: the crash is already fixed on the sockets-server-posix tip. Please close this PR and #1817. If you'd like, the fatal-signal backtrace handler (commit eee9c0d, ~40 lines in CommandLineTests.cpp) is a useful permanent addition for the test runner — say the word and I'll split it out as a clean PR against main.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants