Skip to content

Fix BGTask SIGSEGV from re-running php_embed_init under a live runtime#154

Merged
shanerbaner82 merged 1 commit into
mainfrom
fix/bgtask-tsrm-crash
Jun 5, 2026
Merged

Fix BGTask SIGSEGV from re-running php_embed_init under a live runtime#154
shanerbaner82 merged 1 commit into
mainfrom
fix/bgtask-tsrm-crash

Conversation

@shanerbaner82

Copy link
Copy Markdown
Contributor

Problem

A WorkManager scheduler job (PHPSchedulerWorker) firing while the app process is alive SIGSEGVs in php_tsrm_startup_ex. Tombstone:

PHPSchedulerWorker.doWork → LaravelEnvironment.initializeForBackground → runBaseArtisanCommands
  → native_run_artisan_command → php_embed_init → php_tsrm_startup_ex → ts_resource_ex
  → zend_ini_refresh_caches → OnUpdateBool → null-pointer deref

The worker's initializeForBackground() runs runBaseArtisanCommands() (optimize:clear, storage:unlink, storage:link, migrate --force) whenever extractLaravelBundle() reports a change. Those route through native_run_artisan_command, which called php_embed_init() unconditionally. When the persistent runtime and/or the database queue worker already hold a process-wide TSRM context, the second php_embed_init() re-runs php_tsrm_startup_ex → zend_ini_refresh_caches → OnUpdateBool against live globals and null-derefs. WorkManager then retry-loops it (~2 min apart). It only stops if both the persistent runtime and the queue worker are disabled.

In DEBUG builds (NATIVEPHP_APP_VERSION="DEBUG") every extraction reports a change, so this fires on every background tick.

Fix

  1. native_run_artisan_command is now TSRM-aware (php_bridge.c), mirroring ephemeral_embed_init:

    • wait_for_persistent_boot_settled() to avoid racing a mid-boot persistent thread;
    • hot path (a runtime is live): ts_resource(0) + php_embed_module.startup() + php_request_startup(), teardown via php_request_shutdown(NULL) + ts_free_thread() — leaving the global TSRM and php_initialized intact;
    • cold path (no live runtime — install-time setup or a WorkManager cold start): unchanged full php_embed_init() / safe_php_embed_shutdown().

    This is the root-cause fix and makes the classic artisan path safe for every caller.

  2. Warm-path guard — new nativeIsPersistentRuntimeLive() JNI accessor (reads the process-wide persistent_initialized) lets initializeForBackground() skip the install-time artisan commands when a persistent runtime is already live — those were already run by MainActivity at app boot. The C fix remains the backstop if the guard ever races a mid-boot runtime.

Files

  • resources/androidstudio/app/src/main/cpp/php_bridge.c — hot/cold TSRM split + JNI accessor + registration
  • resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/PHPBridge.ktnativeIsPersistentRuntimeLive binding
  • resources/androidstudio/app/src/main/java/com/nativephp/mobile/bridge/LaravelEnvironment.kt — guard in initializeForBackground()

Testing

Verified on an Android emulator with a DEBUG build, runtime_mode: persistent, and QUEUE_CONNECTION=database: firing a scheduled background task while the app is open no longer SIGSEGVs; logcat shows ⏭️ Skipping base artisan commands — persistent runtime already live. Reverting the fix reproduces Fatal signal 11 (SIGSEGV) in php_tsrm_startup.

iOS is unaffected — its scheduler already routes background work through ephemeral_php_boot / ephemeral_embed_init, which attaches to the existing TSRM.

🤖 Generated with Claude Code

A WorkManager scheduler job (PHPSchedulerWorker) firing while the app
process is alive crashed in php_tsrm_startup_ex. The worker's
initializeForBackground() runs runBaseArtisanCommands() on extraction,
which routes through native_run_artisan_command — and that path called
php_embed_init() unconditionally. With the persistent runtime / queue
worker already holding a process-wide TSRM, the second php_embed_init()
re-ran php_tsrm_startup_ex → zend_ini_refresh_caches → OnUpdateBool
against live globals and null-deref'd. WorkManager then retry-looped it.

- native_run_artisan_command now mirrors ephemeral_embed_init: wait for
  persistent boot to settle, then take a hot path (ts_resource(0) +
  module/request startup, teardown via php_request_shutdown + ts_free_thread)
  when a runtime is live, or the unchanged full php_embed_init cold path
  otherwise. Root-cause fix for every caller.
- Add nativeIsPersistentRuntimeLive() JNI accessor (reads the process-wide
  persistent_initialized) so the background path can skip the install-time
  artisan commands the live app already ran at boot — those are
  MainActivity's responsibility. The C fix remains the backstop.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shanerbaner82 shanerbaner82 requested a review from simonhamp June 4, 2026 12:37
@shanerbaner82 shanerbaner82 merged commit 045bebc into main Jun 5, 2026
4 of 5 checks passed
@shanerbaner82 shanerbaner82 deleted the fix/bgtask-tsrm-crash branch June 5, 2026 17:27
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.

1 participant