fix: harden and fix bundle hot-update across all platforms#10340
fix: harden and fix bundle hot-update across all platforms#10340huhuanming wants to merge 46 commits intoxfrom
Conversation
- Fix swapped event subscription variable names in native JS bridge - Fix iOS currentMetadataJson double path concatenation causing metadata lookup failure - Fix iOS nil signature crash in installBundle fallback data - Fix iOS dead partial-file resume code that never worked with NSURLSessionDownloadTask - Fix iOS validateAllFilesInDir duplicate recursive validation - Fix Android downloadBundleASC not saving signature to SharedPreferences - Fix Android SharedPreferences race condition with two separate apply() calls - Fix Android version string split using lastIndexOf for dash-containing versions - Fix Desktop 416 status Promise leak (return in callback instead of resolve) - Fix Desktop verifyAndResolve executing resolve after reject - Fix Desktop writeStream race condition (rename before stream fully flushed) - Add Desktop HTTP redirect following (301/302/307/308) - Reduce unreasonable cache-hit delays from 10s/5s to 1s - Remove stray console.log in production code - Fix method name typo valiateAllFilesInDir -> validateAllFilesInDir - Specify UTF-8 encoding in Android readFileContent
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
- Enforce HTTPS for bundle download URLs (desktop/iOS/Android) - Add path traversal protection for zip extraction (desktop) - Verify all extracted files against metadata SHA256 (all platforms) - Validate all bundle files on startup, not just main.jsbundle.hbc (iOS/Android) - Prevent bundle version downgrade attacks (all platforms) - Require signature and sha256 before seamless install (SplashProvider/hooks) - Validate server config response schema and jsBundle URL scheme
- Add try-catch around cached file verification to prevent Promise hang and isDownloading getting stuck when SHA256 check fails - Enforce HTTPS on redirect URLs to prevent downgrade via Location header
…load Add OkHttp network interceptor to reject any request redirected to a non-HTTPS URL, matching the desktop redirect protection. Android has usesCleartextTraffic=true in manifest, so OkHttp would otherwise silently follow HTTPS→HTTP redirects.
- Reject symbolic links during extracted file verification (desktop) - Cleanup extracted directory on verification failure (all platforms) - Check SSZipArchive return value before proceeding (iOS) - Add writeStream error handler to prevent download hang (desktop) - Validate HTTPS in updateDownloadUrl (ServiceAppUpdate) - Add 30-minute download timeout for native platforms (iOS/Android) - Cleanup GPG verification temp file in finally block (Android)
Add 126 unit tests and ~30 harness tests covering bundle update logic: - Shared: version comparison, update detection, HTTPS validation, error mapping - Desktop: SHA256/SHA512 verification, GPG signature, path traversal, downgrade prevention - Native (harness): SHA256, parameter validation, HTTPS enforcement, test helpers
Two-layer UI in dev settings: version list → bundle list with download progress tracking and bundle switching. GPG verification can be skipped in dev mode. Uses mock API data (to be replaced with real endpoints).
Add isBundleExists API across all platforms (Desktop/iOS/Android) to check if a bundle directory already exists. BundleList UI now checks on mount and shows "Switch to this bundle" directly for bundles that were previously downloaded and extracted.
… mode isBundleExists now reads metadata.json and validates all extracted files against their SHA256 hashes before reporting a bundle as existing. installBundle now accepts skipGPGVerification to bypass the version downgrade check in dev bundle switcher mode.
…le download limit Revert isBundleExists to simple directory existence check on all platforms. Replace Download button with a small download icon. Only one bundle can be downloaded at a time - other download icons are disabled while a download is in progress.
Add verifyExtractedBundle(appVersion, bundleVersion) across all platforms. Reads metadata.json and validates every file's SHA256. Called only when user clicks "Switch to this bundle" for previously downloaded bundles, avoiding slow checks on page load.
Shows all bundles on device (current + fallback list). Each bundle can be switched to directly without downloading. Verifies file integrity before switching via verifyExtractedBundle, then calls switchBundle to update and restart.
Replace getFallbackBundles with listLocalBundles in LocalBundleList page. The new API scans the bundle directory on all platforms (Desktop/iOS/Android) to accurately list all extracted bundles on device for offline switching.
Post-extraction security check validates all extracted files are within the destination directory and rejects symbolic links to prevent attacks. Aligns iOS with Desktop and Android which already have this protection.
- Remove duplicate promise.resolve in Android verifyASC (double resolve crash) - Add missing return after promise.reject in Android verifyAPK (double reject crash) - Remove redundant else reject in verifyAPK since checkFilePackage already rejects - Add bundle directory existence check before updating store in Desktop installBundle - Add bundle directory existence check before updating UserDefaults in iOS installBundle
Android installAPK: - Add file existence check before checkFilePackage - Remove double promise.reject when checkFilePackage fails - Move promise.resolve after startActivity so failures reject properly - Reject with INVALID_PACKAGE when APK cannot be parsed (info==null) - Early return with correct error when ASC signature file is missing - Add FLAG_ACTIVITY_NEW_TASK for all Android versions - Add getCurrentActivity() null check before startActivity Desktop installPackage: - Throw error instead of silent return when verification fails - Validate downloadedEvent before spreading to main process JS layer: - Call onFail for all install errors, not just NOT_FOUND_PACKAGE - Add downloadedEvent existence check in SplashProvider for all file types
- Fix relative redirect URL handling in Desktop bundle download - Store bundle signature in iOS installBundle to match Android behavior - Remove premature promise.resolve in Android downloadASC - Fix TypeScript errors in e2e harness tests
- Split "Dev App Update Settings" into separate "App Update Test" and "JS Bundle Manager" pages - Add new "App Update" accordion section in dev settings grouping app update test, JS bundle manager, and firmware update - Redesign all bundle manager pages with card-based sections, icon badges, status indicators, and proper visual hierarchy - Pin current app version to top in Remote Bundles version list - Add 6.1.0 mock data for bundle version testing
Previously the desktop implementation silently skipped file integrity verification when metadata.json was absent from the extracted zip. iOS and Android both explicitly reject this case. Now desktop throws an error to match, preventing unverified bundles from being installed.
GPG signature and SHA256 verification are already covered by unit tests (desktop Jest + RN harness), making the UI button redundant.
…e registration Remove unnecessary 1200ms setTimeout in installBundle and setCurrentUpdateBundleData. Destroy the BrowserWindow before app.relaunch() to ensure the renderer process is fully terminated, preventing the "webview" custom element from being registered twice which causes NotSupportedError on all desktop platforms (MAS/WinMS/Snap). Fixes DESKTOP-MAS-1, DESKTOP-WINMS-PY, DESKTOP-SNAP-8R
Cover the state machine that drives bundle/app updates: - Happy path (notify → download → ASC → verify → ready) - Error paths with error message mapping - Download timeout (30 min) - Recovery and reset flows - Security validations (HTTPS enforcement, updateStrategy) - fetchAppUpdateInfo integration and caching
There was a problem hiding this comment.
🔴 Android downloadBundle: promise.reject called after promise already resolved causes crash
In BundleUpdateModule.java, downloadBundle calls promise.resolve(result) at line 824 immediately after enqueuing the asynchronous OkHttp callback. If the download subsequently fails, the callback invokes promise.reject() at lines 774, 784, or 815 — but the promise was already resolved. On React Native, calling reject on an already-resolved promise throws an IllegalStateException and crashes the app.
Root Cause and Impact
The method follows a pattern where the promise resolves eagerly with the file-path result, and download completion/error is communicated via events (update/error, update/complete). This is the correct pattern used by the iOS side — iOS's downloadBundle resolves the promise at BundleUpdateModule.m:811 and then only uses delegate callbacks to send events, never touching the promise again.
However, the Android implementation also calls promise.reject(...) inside the Callback.onFailure (line 774), Callback.onResponse for non-success (line 784), and after SHA256 verification failure (line 815). Since the promise was already resolved at line 824, these reject calls will crash the app.
For example, if the server returns an HTTP error:
// Line 824 - promise already resolved
promise.resolve(result);
// Later, async callback fires:
// Line 784 - CRASH: promise already settled
promise.reject("DOWNLOAD_ERROR", "HTTP " + response.code());Impact: Any download failure (network error, HTTP error, SHA256 mismatch) will crash the Android app with an unhandled native exception.
(Refers to lines 774-824)
Prompt for agents
In apps/mobile/android/app/src/main/java/so/onekey/app/wallet/BundleUpdateModule.java, the downloadBundle method (around line 708) calls promise.resolve(result) at line 824 after enqueuing the async OkHttp callback. The async callback also calls promise.reject() on failure at lines 774, 784, and 815. Since the promise is already resolved, these reject calls will crash the app.
Fix: Remove all promise.reject() calls inside the enqueued Callback (onFailure and onResponse error paths). The error handling is already done correctly via sendEvent("update/error", ...) which the JS side listens for. Similarly, remove the promise.reject at line 815 for verification failure. The callback should only use events (sendEvent) to communicate results, not the promise, since the promise is resolved immediately.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed. Moved promise.resolve(result) from after enqueue() into the async callback's success path (after sendEvent("update/complete")). Now the promise is settled exactly once: resolve on success, reject on failure.
…KeyHQ/app-monorepo into analyze/bundle-update-issues
Pull request was converted to draft
…pdate flow - Add state transition guards on all intermediate methods (downloadASC, verifyASC, verifyPackage, readyToInstall) to prevent state regression - Add guards on failed methods (downloadASCFailed, verifyASCFailed, verifyPackageFailed) to only accept transitions from correct states - Allow current-state re-entry for breakpoint recovery on app restart - Add failed state auto-recovery timer (2h) resetting to notify - Fix fetchAppUpdateInfo to clear downloadedEvent for verify failures when a newer version resets the state to notify - Fix refreshUpdateStatus to reset failed states on app launch - Add 147 ServiceAppUpdate unit tests covering all state transitions - Add 52 hooks.test.ts tests for useDownloadPackage, useAppUpdateInfo useEffect startup logic, and onUpdateAction routing
Cover all branches of the seamless update install-at-launch logic: - Non-seamless strategies (manual/force/silent) show splash immediately - Seamless + first launch after update → refreshUpdateStatus + splash - Seamless + ready + missing downloadedEvent → reset + splash - Seamless + ready + jsBundle missing signature/sha256 → reset + splash - Seamless + ready + jsBundle valid → BundleUpdate.installBundle - Seamless + ready + appShell → AppUpdate.installPackage - Install failure → reset + splash - Seamless + non-ready status → splash without install - Idempotency: useLayoutEffect fires only once - Web/extension platform → always returns true
The useDisplaySplash hook had several paths where displaySplash could stay false forever, leaving the user stuck on the native splash screen: 1. getUpdateInfo() throws — no outer catch, async rejection swallowed 2. getUpdateInfo() hangs — async never resolves, no timeout fallback 3. refreshUpdateStatus() throws — setDisplaySplash called after await 4. reset() throws in guard paths — setDisplaySplash called after await 5. Install succeeds but app doesn't restart — no fallback Fixes: - Wrap entire launchCallback body in try/catch with setDisplaySplash(true) - Move setDisplaySplash(true) before all await calls in guard paths - Add 10s safety timeout as last-resort fallback Added 6 error resilience tests covering all identified stuck paths.
Add debug logging across all bundle update code paths to aid troubleshooting: download guards, HTTPS rejections, redirect errors, HTTP status errors, timeouts, verification failures, state guard rejections, recovery timer, splash safety timeout, and install parameter validation. Also add missing test coverage for new code branches and fix test mocks.
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
Remove all legacy RCTBridgeModule (iOS) and ReactContextBaseJavaModule (Android) implementations, replacing them with Nitro Module imports from the @onekeyfe scoped packages in app-modules. Deleted legacy native files: - iOS: BundleUpdateModule, Verification, LaunchOptionsManager, PerfMemoryModule - Android: BundleUpdateModule, AutoUpdateModule, Verification, PerfMemoryModule, WebViewCheckerModule, ExitModule, LaunchOptionModule, SplashScreen (full dir), all associated Package files, and flavor-specific AutoUpdateModulePackage Updated native entry points: - AppDelegate.swift: BundleUpdateStore + DeviceUtilsStore (Nitro) - CustomReactNativeHost.java: BundleUpdateStoreAndroid (Nitro) - MainApplication.java: removed all legacy package registrations - Bridging-Header: removed legacy ObjC imports - project.pbxproj: removed deleted file references Updated JS/TS wrappers to use Nitro imports: - auto-update/index.native.ts: NativeEventEmitter → callback listeners - useJsBundle.native.ts, androidNativeEnv.android.ts - LaunchOptionsManager.native.ts → ReactNativeDeviceUtils - SplashView.native.tsx → ReactNativeSplashScreen - webview-checker/index.android.tsx → ReactNativeWebviewChecker - memoryCollector.native.ts → ReactNativePerfMemory - rnNativeModules.ts: removed migrated type definitions Added new dependencies to apps/mobile/package.json: - @onekeyfe/react-native-app-update - @onekeyfe/react-native-bundle-update - @onekeyfe/react-native-perf-memory - @onekeyfe/react-native-splash-screen - @onekeyfe/react-native-webview-checker
Update .oxlintrc.json with airbnb/wesbos rules, import rules, and React rules to match eslint configuration. Add eslint-disable comments alongside existing oxlint-disable comments across 117 source files to ensure both linters produce consistent suppression results. Fix comment ordering so eslint-disable-next-line appears before oxlint-disable-next-line.
Automatically fixed 75 violations across 24 files including prettier formatting, no-var to const/let conversions, and prefer-const fixes.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Summary
Comprehensive bundle hot-update overhaul: fix critical bugs, harden security, add dev tooling and tests across iOS, Android, and Desktop.
Bug Fixes
Critical
currentMetadataJsondouble path concatenationdownloadBundleASCnever saves signature to SharedPreferencesverifyAndResolvecalls bothreject()andresolve()currentSignatureinserted into NSDictionaryfs.renameSyncbeforewriteStreamfinishes flushingpromise.resolveinverifyASCpromise.rejectinverifyAPKMedium
1.0.0-betaformat (lastIndexOf("-"))SharedPreferences.Editor.apply()into one atomic operationNSURLSessionDownloadTask)validateAllFilesInDirduplicate recursive validationinstallBundleto match Android behaviorpromise.resolveindownloadASCLow
valiateAllFilesInDir→validateAllFilesInDirconsole.login production codereadFileContentSecurity Hardening
downloadedEventbefore usefinallyblock (Android)New Features
verifyExtractedBundlebefore switchinglistLocalBundlesAPI: Scans bundle directory on all platforms to list extracted bundlesverifyExtractedBundleAPI: On-demand SHA256 integrity check for previously downloaded bundlesisBundleExistsAPI: Check if a bundle directory exists on deviceTests
Test Plan
clearAllJSBundleDataon all platforms