Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CODE_AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
> | 2.13 / 单测建议 | 部分:两份 `UpdateModule.java`、`buildToolsVersion`;若干纯函数未覆盖 |
> | 3.10 | 部分:构建脚本仍写 pod 源码目录 |
> | 3.13 / 3.14 | 部分 / 未做:`RCTPushy.mm` 拆分、纯决策下沉 C++ + XCTest |
> | 4.4 | 部分:Android 已兜住;鸿蒙 NAPI 与 iOS 入口无 C++ catch |
> | 4.4 | 部分:三端入口均已兜住;`Application.mk` 的 `-fno-unwind-tables` 与 `-fexceptions` 并用待确认 |
> | 4.12 | 部分:`buildCheckFingerprint` 无 C++ 移植 |
> | §4 P4 | 开放:鸿蒙 NAPI 结果对象省略空串字段(Android 设 null) |
> | 5.7 | 部分:RCP 重定向控制(HMS-only)未做 |
Expand Down Expand Up @@ -133,7 +133,8 @@

**4.4(P2)STL 分配抛出的 C++ 异常穿越 JNI/NAPI/ObjC 边界**(部分修复)
Android 已兜住:`Application.mk` 改 `-fexceptions`,每个 JNI 入口经 `jni_util::GuardJni` / catch-all 转成 Java 异常或 null。
剩余:鸿蒙 `harmony/pushy/src/main/cpp/pushy.cpp` 的 NAPI 回调无 C++ `try/catch`,`bad_alloc/length_error` 会 unwind 进 NAPI;iOS `.mm` 只有 `@try/@catch(NSException*)`,抓不到 C++ 异常。4.5 的输入上限压低了可达性,但入口仍应 `catch (...)` 转成干净错误。另:`Application.mk` 仍带 `-fno-unwind-tables -fno-asynchronous-unwind-tables`,宜确认与 `-fexceptions` 同时使用时 unwind 正常。
鸿蒙已兜住(2026-10):`pushy.cpp` 每个导出回调经 `Guarded<>` 把 C++ 异常转成 JS Error,async work 的 execute(工作线程)与 settle 各自 catch,promise 以异常信息 reject。iOS 已兜住(2026-10):所有入口(RN 导出方法、`+bundleURL`、host configure/prepareBundle、下载/解压/清理/摘要的 GCD 块、解压守卫委托、crash hold、轮次调度)改为 `@catch (...)`(ObjC++ 下能接住 C++ 异常),原因经 `PushyCaughtReason` 取 `what()`;`+bundleURL` 解析抛错时回退内置包并照常调度原生轮次;`scripts/test-ios-purge-restore.sh` 的 `cpp_exception_boundary` 用例锁定(含状态锁释放)。
剩余:`Application.mk` 仍带 `-fno-unwind-tables -fno-asynchronous-unwind-tables`,宜确认与 `-fexceptions` 同时使用时 unwind 正常。

**4.12(P3)对等性:`buildCheckFingerprint` 无 C++ 移植**(部分修复)
`IsMirrorRetryableCode` 已移植并进向量,`paths:null` 已有向量。`buildCheckFingerprint`(`src/updateFlowCore.ts`)仍只有 TS 实现,各原生编排器各自重实现了等价的去重逻辑——对等性框架抓不到这类漂移。
Expand Down
97 changes: 73 additions & 24 deletions harmony/pushy/src/main/cpp/pushy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <js_native_api_types.h>

#include <cerrno>
#include <exception>
#include <cstdint>
#include <string>
#include <unistd.h>
Expand Down Expand Up @@ -558,8 +559,25 @@ struct AsyncCall {
W data;
void (*execute)(W&) = nullptr;
void (*settle)(napi_env, napi_deferred, const W&) = nullptr;
// Set when execute threw: the promise rejects with it instead of settling
// from half-written results.
bool threw = false;
std::string failure;
};

// The message of the exception being handled, without throwing again.
void RecordFailure(std::string* failure) noexcept {
try {
throw;
} catch (const std::exception& error) {
try {
*failure = error.what();
} catch (...) {
}
} catch (...) {
}
}

// Runs execute(data) on a libuv worker thread and returns a Promise that
// settle(env, deferred, data) settles back on the JS thread. Every path
// settles the promise exactly once and frees the call: a creation or queue
Expand Down Expand Up @@ -591,15 +609,30 @@ napi_value QueueWork(
nullptr,
resource_name,
[](napi_env, void* raw) {
// A worker thread: an exception escaping here terminates the
// process (CODE_AUDIT 4.4).
auto* c = static_cast<AsyncCall<W>*>(raw);
c->execute(c->data);
try {
c->execute(c->data);
} catch (...) {
c->threw = true;
RecordFailure(&c->failure);
}
},
[](napi_env cb_env, napi_status status, void* raw) {
auto* c = static_cast<AsyncCall<W>*>(raw);
if (status != napi_ok) {
RejectDeferredWithMessage(cb_env, c->deferred, "async work aborted");
} else if (c->threw) {
RejectDeferredWithMessage(
cb_env, c->deferred,
c->failure.empty() ? "native error" : c->failure.c_str());
} else {
c->settle(cb_env, c->deferred, c->data);
try {
c->settle(cb_env, c->deferred, c->data);
} catch (...) {
RejectDeferredWithMessage(cb_env, c->deferred, "native error");
}
}
napi_delete_async_work(cb_env, c->work);
delete c;
Expand Down Expand Up @@ -1144,6 +1177,22 @@ napi_value CheckExtractedDirectory(napi_env env, napi_callback_info info) {
});
}

// Every exported callback runs through Guarded: a C++ exception from the
// shared cores (std::bad_alloc, std::length_error) must not unwind into NAPI,
// so it becomes a JS Error instead (CODE_AUDIT 4.4).
template <napi_value (*Callback)(napi_env, napi_callback_info)>
napi_value Guarded(napi_env env, napi_callback_info info) {
try {
return Callback(env, info);
} catch (...) {
std::string failure;
RecordFailure(&failure);
napi_throw_error(env, nullptr,
failure.empty() ? "native error" : failure.c_str());
}
return nullptr;
}

bool ExportFunction(
napi_env env,
napi_value exports,
Expand Down Expand Up @@ -1381,28 +1430,28 @@ static napi_value FlowNormalizeConfiguration(napi_env env,
}

napi_value Init(napi_env env, napi_value exports) {
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", SyncStateWithBinaryVersion) ||
!ExportFunction(env, exports, "runStateCore", RunStateCore) ||
!ExportFunction(env, exports, "applyArchiveDelta", ApplyArchiveDelta) ||
!ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) ||
!ExportFunction(env, exports, "sha256HexFileAsync", Sha256HexFileAsync) ||
!ExportFunction(env, exports, "sha256HexRawFdAsync", Sha256HexRawFdAsync) ||
!ExportFunction(env, exports, "isInstallComplete", InstallRecordIsComplete) ||
!ExportFunction(env, exports, "recordedBundleSha256", InstallRecordBundleSha256) ||
!ExportFunction(env, exports, "sealInstallRecord", SealInstallRecord) ||
!ExportFunction(env, exports, "promoteStaging", PromoteStaging) ||
!ExportFunction(env, exports, "checkActivation", CheckActivation) ||
!ExportFunction(env, exports, "checkUncompressedSize", CheckUncompressedSize) ||
!ExportFunction(env, exports, "planFreeSpaceProbe", PlanFreeSpaceProbe) ||
!ExportFunction(env, exports, "checkExtractedDirectory", CheckExtractedDirectory) ||
!ExportFunction(env, exports, "crc32", Crc32) ||
!ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) ||
!ExportFunction(env, exports, "prepareRound", FlowPrepareRound) ||
!ExportFunction(env, exports, "nextQueryStep", FlowNextQueryStep) ||
!ExportFunction(env, exports, "handleResponse", FlowHandleResponse) ||
!ExportFunction(env, exports, "configsEquivalent", FlowConfigsEquivalent) ||
!ExportFunction(env, exports, "runnableConfig", FlowRunnableConfig) ||
!ExportFunction(env, exports, "normalizeConfiguration", FlowNormalizeConfiguration)) {
if (!ExportFunction(env, exports, "syncStateWithBinaryVersion", Guarded<SyncStateWithBinaryVersion>) ||
!ExportFunction(env, exports, "runStateCore", Guarded<RunStateCore>) ||
!ExportFunction(env, exports, "applyArchiveDelta", Guarded<ApplyArchiveDelta>) ||
!ExportFunction(env, exports, "cleanupOldEntries", Guarded<CleanupOldEntries>) ||
!ExportFunction(env, exports, "sha256HexFileAsync", Guarded<Sha256HexFileAsync>) ||
!ExportFunction(env, exports, "sha256HexRawFdAsync", Guarded<Sha256HexRawFdAsync>) ||
!ExportFunction(env, exports, "isInstallComplete", Guarded<InstallRecordIsComplete>) ||
!ExportFunction(env, exports, "recordedBundleSha256", Guarded<InstallRecordBundleSha256>) ||
!ExportFunction(env, exports, "sealInstallRecord", Guarded<SealInstallRecord>) ||
!ExportFunction(env, exports, "promoteStaging", Guarded<PromoteStaging>) ||
!ExportFunction(env, exports, "checkActivation", Guarded<CheckActivation>) ||
!ExportFunction(env, exports, "checkUncompressedSize", Guarded<CheckUncompressedSize>) ||
!ExportFunction(env, exports, "planFreeSpaceProbe", Guarded<PlanFreeSpaceProbe>) ||
!ExportFunction(env, exports, "checkExtractedDirectory", Guarded<CheckExtractedDirectory>) ||
!ExportFunction(env, exports, "crc32", Guarded<Crc32>) ||
!ExportFunction(env, exports, "getSupportedDiffVersion", Guarded<GetSupportedDiffVersion>) ||
!ExportFunction(env, exports, "prepareRound", Guarded<FlowPrepareRound>) ||
!ExportFunction(env, exports, "nextQueryStep", Guarded<FlowNextQueryStep>) ||
!ExportFunction(env, exports, "handleResponse", Guarded<FlowHandleResponse>) ||
!ExportFunction(env, exports, "configsEquivalent", Guarded<FlowConfigsEquivalent>) ||
!ExportFunction(env, exports, "runnableConfig", Guarded<FlowRunnableConfig>) ||
!ExportFunction(env, exports, "normalizeConfiguration", Guarded<FlowNormalizeConfiguration>)) {
return nullptr;
}
return exports;
Expand Down
71 changes: 46 additions & 25 deletions ios/RCTPushy/RCTPushy.mm
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,11 @@ static void PushyTriggerReload(NSString *reason) {
});
}

// A promise rejection for an exception caught around a state transition.
static void PushyRejectException(RCTPromiseRejectBlock reject, const char *code,
NSException *exception, NSString *fallback) {
PushyRejectError(reject, PushyErrorWithCode(code, exception.reason ?: fallback));
// A promise rejection for an exception caught (`@catch (...)`) around a
// state transition.
static void PushyRejectCaught(RCTPromiseRejectBlock reject, const char *code,
NSString *fallback) {
PushyRejectError(reject, PushyErrorWithCode(code, PushyCaughtReason(fallback)));
}

// Host preparation runs off the caller's thread; the completion is delivered
Expand All @@ -61,8 +62,9 @@ static void PushyRunHostPreparation(NSDictionary *(^work)(void),
NSDictionary *result;
@try {
result = work();
} @catch (NSException *exception) {
RCTLogWarn(@"RCTPushy -- native host sync failed: %@", exception.reason);
} @catch (...) {
RCTLogWarn(@"RCTPushy -- native host sync failed: %@",
PushyCaughtReason(@"unknown exception"));
result = PushyHostResult(@"failed", @"internal_error");
}
if (completion != nil) {
Expand Down Expand Up @@ -101,11 +103,20 @@ + (NSURL *)bundleURL
}
#endif
return resolvedURL ?: [RCTPushy binaryBundleURL];
} @catch (...) {
// An exception here would end every launch the same way; the
// packaged bundle always loads, and the round scheduled below can
// still repair the state.
RCTLogError(@"RCTPushy -- cannot resolve the launch bundle: %@",
PushyCaughtReason(@"unknown exception"));
return [RCTPushy binaryBundleURL];
} @finally {
// State corruption is exactly when the rescue path matters most. If
// resolution throws before a snapshot exists, nil safely omits only
// this launch's rollback guard instead of disabling the check.
[RCTPushyOrchestrator scheduleFromColdStart:launchRolledBackVersion];
PushyGuard(@"native sync schedule", ^{
[RCTPushyOrchestrator scheduleFromColdStart:launchRolledBackVersion];
});
}
}

Expand All @@ -122,9 +133,9 @@ + (void)configure:(NSDictionary<NSString *, id> *)options
if (config != nil) {
@try {
[RCTPushyOrchestrator persistConfiguration:config];
} @catch (NSException *exception) {
} @catch (...) {
failure = PushyErrorWithCode(pushy::error_codes::kFileOperationFailed,
exception.reason ?: @"Native configuration failed");
PushyCaughtReason(@"Native configuration failed"));
}
}
if (completion != nil) {
Expand Down Expand Up @@ -158,7 +169,9 @@ + (BOOL)requiresMainQueueSetup
- (NSDictionary *)constantsToExport
{
NSMutableDictionary *ret = [NSMutableDictionary new];
PushyWithStateLock(^{
// Whatever was gathered before a failure is still returned: throwing out
// of getConstants takes the app down.
PushyGuard(@"constants", ^{ PushyWithStateLock(^{
NSUserDefaults *defaults = PushyDefaults();

ret[@"downloadRootDir"] = [RCTPushy downloadDir];
Expand Down Expand Up @@ -194,7 +207,7 @@ - (NSDictionary *)constantsToExport
[defaults removeObjectForKey:KeyPackageUpdatedMarked()];
[RCTPushyInstaller clearInvalidFiles];
}
});
}); });

return ret;
}
Expand Down Expand Up @@ -233,9 +246,9 @@ - (NSDictionary *)constantsToExport
@try {
[RCTPushyOrchestrator persistConfiguration:config];
resolve(@true);
} @catch (NSException *exception) {
PushyRejectException(reject, pushy::error_codes::kFileOperationFailed, exception,
@"Native configuration failed");
} @catch (...) {
PushyRejectCaught(reject, pushy::error_codes::kFileOperationFailed,
@"Native configuration failed");
}
}

Expand Down Expand Up @@ -366,7 +379,11 @@ - (NSDictionary *)constantsToExport
resolve(@"");
#else
dispatch_async(PushyFileQueue(), ^{
resolve(PushyBundleHashSync(YES));
__block NSString *hash = @"";
PushyGuard(@"bundle hash", ^{
hash = PushyBundleHashSync(YES);
});
resolve(hash);
});
#endif
}
Expand All @@ -390,9 +407,8 @@ - (NSDictionary *)constantsToExport
}
PushyApplyStateToDefaults(defaults, result.state);
});
} @catch (NSException *exception) {
PushyRejectException(reject, pushy::error_codes::kMarkSuccessFailed, exception,
@"markSuccess failed");
} @catch (...) {
PushyRejectCaught(reject, pushy::error_codes::kMarkSuccessFailed, @"markSuccess failed");
return;
}

Expand Down Expand Up @@ -441,8 +457,8 @@ - (NSDictionary *)constantsToExport
[defaults removeObjectForKey:keyNativeCheckCache];
ignoreRollback = false;
});
} @catch (NSException *exception) {
PushyRejectException(reject, pushy::error_codes::kResetFailed, exception, @"reset failed");
} @catch (...) {
PushyRejectCaught(reject, pushy::error_codes::kResetFailed, @"reset failed");
return;
}

Expand All @@ -453,11 +469,16 @@ - (NSDictionary *)constantsToExport
// promise settles here, after the cleanup, so a failed wipe reaches
// JS as RESET_FAILED (Android parity) instead of a warning nobody
// aggregates.
pushy::delta::Status status = pushy::delta::CleanupOldEntries(
PushyToStdString([RCTPushy downloadDir]),
std::vector<std::string>{PushyToStdString(keepVersion)},
0
);
pushy::delta::Status status = pushy::delta::Status::Ok();
try {
status = pushy::delta::CleanupOldEntries(
PushyToStdString([RCTPushy downloadDir]),
std::vector<std::string>{PushyToStdString(keepVersion)},
0
);
} catch (const std::exception &error) {
status = pushy::delta::Status::Error(error.what());
}
if (!status.ok) {
RCTLogWarn(@"Pushy reset cleanup error: %s", status.message.c_str());
PushyRejectError(reject, PushyErrorWithCode(
Expand Down
9 changes: 8 additions & 1 deletion ios/RCTPushy/RCTPushyConfiguration.mm
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,14 @@
}
std::string config;
std::string message;
if (flowcore::NormalizeConfiguration(optionsJson, &config, &message)) {
bool normalized = false;
try {
normalized = flowcore::NormalizeConfiguration(optionsJson, &config, &message);
} catch (const std::exception &failure) {
// Called on the host's thread (CODE_AUDIT 4.4).
message = std::string("Invalid native configuration: ") + failure.what();
}
if (normalized) {
NSString *result = [[NSString alloc] initWithBytes:config.data()
length:config.size()
encoding:NSUTF8StringEncoding];
Expand Down
Loading
Loading