Skip to content

PIMETextService crashes TSF host processes when activation RPC fails (nlohmann::json::type_error.306) #888

Description

@Campione01

Summary

PIMETextService.dll terminates its TSF host process when PIME is activated while the launcher/backend RPC endpoint is unavailable.

The activation path ignores a failed callRpcMethod(). The response remains a default-null nlohmann::json, but handleRpcResponse() calls value("success", false), which throws nlohmann::json::type_error.306. The exception escapes the TSF/COM callback and terminates the host.

This was observed independently in Explorer, Task Manager, conhost, and SearchHost.

Affected build and environment

  • PIME 1.3.0-beta2
  • Commit 9f6a1e9161b7f609eb1fadf282048c2907da04c9
  • Official CI x64 PIMETextService.dll
  • PE timestamp 0x6A653E3C, checksum 0x8EC54, image size 0x82000
  • Windows 11 Pro for Workstations 25H2, build 26200.8039, x64

Safe reproduction

Please use a disposable VM or isolated TSF test host rather than Explorer.

  1. Register the affected x64 build in the disposable environment.
  2. Keep PIMELauncher or its backend unavailable so the RPC connection cannot be established.
  3. Activate PIME in an x64 sacrificial TSF host.
  4. The host terminates during Client::onActivate().

A deterministic source-level test can make callRpcMethod() return false while leaving ret default-null, then invoke Client::onActivate().

Crash evidence

Nine independently captured LocalDumps contain the same PIME frame:

KERNELBASE!RaiseException
PIMETextService+0x360d9

Exception metadata and return addresses from the exact official CI binary resolve to:

nlohmann::json::type_error, id 306
PIMETextService+0x1bcb1  json::value() type-error throw
PIMETextService+0x2006c  Client::handleRpcResponse(), reading "success"
PIMETextService+0x21b7d  Client::onActivate()

Observed terminal signatures include the original C++ exception 0xe06d7363, followed in several hosts by ucrtbase.dll 0xc0000409, fast-fail subcode 7 (FAST_FAIL_FATAL_APP_EXIT).

The source path is:

  1. Client::onActivate() creates default-null json ret.
  2. It calls callRpcMethod(req, ret) but ignores the returned false.
  3. callRpcMethod() can return false on connection, pipe, parsing, or sequence failures without producing an object response.
  4. handleRpcResponse() calls msg.value("success", false).
  5. nlohmann value() throws type error 306 when the value is not an object.
  6. No exception barrier prevents it from escaping the TSF/COM activation callback.

This appears to be a regression from e5cf1916, which migrated from JsonCpp:

msg.get("success", false).asBool()

to nlohmann:

msg.value("success", false)

The old expression tolerated an empty/default Json::Value; the new expression throws for a null JSON value.

The exact initial RPC failure reason cannot be recovered because no launcher log survived. Connection refusal, timeout, malformed reply, sequence mismatch, and broken pipe are all possible triggers. The fatal defect is independent of that trigger: a normal RPC failure must not escape a TSF callback or terminate its host.

Expected behavior

If the launcher, backend, pipe, or RPC response is unavailable or invalid, activation should fail cleanly. PIME should remain inactive and must not terminate the TSF host process.

Suggested fix

At minimum, honor the RPC result before inspecting the response:

json ret;
if (!callRpcMethod(req, ret) || !handleRpcResponse(ret)) {
    isActivated_ = false;
    return;
}
isActivated_ = true;

For defense in depth:

  • Make handleRpcResponse() reject non-object JSON before calling value().
  • Validate the type of success.
  • Apply the same RPC-result check to deactivation and other callers that ignore callRpcMethod().
  • Make teardown paths non-throwing.
  • Catch JSON and other C++ exceptions before they can cross any TSF/COM callback boundary.

Regression tests

  • A failed callRpcMethod() with a default-null response does not throw and leaves PIME inactive.
  • Null, malformed, non-object, missing-success, and wrong-type-success responses do not throw.
  • Activating with the launcher/backend absent does not terminate an isolated TSF host.
  • Losing the connection during deactivation does not let an exception escape.
  • No C++ exception can cross a TSF/COM callback boundary.

The dumps are not attached because they contain host-process memory. The evidence above was extracted without including personal data.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions