Skip to content

[BOUNTY #2851] Reuse remote DroidGuard sessions across snapshots - #3841

Open
woahwhattheheck wants to merge 24 commits into
microg:masterfrom
woahwhattheheck:luna2-dott-session-transport-2851-20261001
Open

woahwhattheheck wants to merge 24 commits into
microg:masterfrom
woahwhattheheck:luna2-dott-session-transport-2851-20261001

Conversation

@woahwhattheheck

@woahwhattheheck woahwhattheheck commented Oct 1, 2026 •

Copy link
Copy Markdown

Summary

Reuse remote DroidGuard state across the existing init / repeated snapshot / close lifecycle, centralize the HTTP transport, and return completed callback results before potentially slow remote cleanup. The protocol now also has an operator-enabled Android provider and authenticated host adapter that retain actual Embedded core handles.

Response transport source: 7a45ad14d102e89c1ba7082887908efa9246737b. Request-bound source: f826c8ed1540ac8969140c340f7c211ad83e68c2. The executed validation pins and their source scope are recorded below.

  • guardWithRequest uses the existing bounded executor, returns its callback result, and closes its handle in a finally boundary, including callback failure.
  • Remote initialization stays pending until setup finishes. A snapshot observes the matching initialization generation, and closing a handle prevents late setup from reopening it.
  • HTTP request encoding, timeouts, non-success responses, and connection cleanup are shared.
  • Legacy single-request fallback remains available when session setup is unavailable.
  • Completed success/fallback results are delivered before waiting for the remote close response; cleanup remains on the same executor worker.

Native server and public client

The operator setup guide documents the new default-disabled provider and Python host adapter. The provider checks DUMP permission and the actual local caller UID before decoding or changing Binder identity. It requires Embedded mode, checks native readiness, retains one core handle for repeated snapshots, and closes failed initialization and late results. The ordinary service broker is unchanged.

Session capacity includes pending initialization and cleanup. Operation deadlines, idle expiry, and bounded queues limit retained work. A duplicate operation for a busy session is rejected before occupying a global worker, preserving progress for another session. The adapter selects an explicit ADB device, authenticates the existing base-URL token query, and carries exact JSON envelopes instead of treating content-command diagnostics as result bytes.

The HTTP action/form protocol is adapted from GautamKumarOffical's PR #3575, source 9d9a8d37e1f4b70997ba1868d2c196261dfd7d0f; its Apache notice and source credit are retained.

The public RemoteDroidGuardHandleClient exposes the existing Task/handle lifecycle. At 02f6c0f0, initial executor rejection becomes a failed Task, and getResults uses one accepted worker for begin/snapshot/result/close. Per-session HTTP serialization from 04fbd911 remains in place.

Executed validation and source pins

HTTP response boundary and current Android execution

At 7a45ad14, RemoteDroidGuardHttpClient reads responses through an 8,192-byte buffer with a 262,144-byte limit. Its maintained regression rejects an oversized response and still verifies connection cleanup.

Run 37229223484 checked out product 2de4887ea7ffd27ec86256f3ccbd48e3e46a8baf / tree f23fccdf238d29c8a515381de40e06f586958565, then applied exactly the two retained response-bound postimages. The scope guard passed. The production HTTP blob is 730ea3d34bb44f25d13070aa2a44fc59dde24ce0, and the maintained test blob is 0e1887b51bc57cfe8224e3f19f5aa1c9ad476355; both are now exact readbacks from the existing PR at 7a45ad14.

The three selected classes produced 12 actual JUnit XML tests, 0 failures, errors or skips: initialization 5, session 4, HTTP client 3. :play-services-droidguard-core:testDebugUnitTest and assembleVtmDefaultDebug completed with Gradle exit 0 in 186.811 seconds. The runner's own JDK jdk.httpserver module supplied an evidence-scoped unit-test classpath JAR through a Gradle init script.

The downloaded VtmDefaultDebug APK is com.google.android.gms-252432000.apk, 107,332,090 bytes, SHA-256 f4981a147e7b05d71669d48e2983c3673fd810e021768b20d71cfcdafa0cc1d6. These results and that APK belong to 2de4887e plus the stated exact two-file response patch. Device Play Integrity and the Dott unlock-and-ride outcome remain pending.

Request admission and adapter concurrency

At f826c8ed, the client rejects an over-limit encoded request target, form body or field count before opening the HTTP connection. The bounds are 8,192 bytes for the request target, 32,768 bytes for the form body, 128 query fields and 256 form fields. The already-landed 262,144-byte response limit and connection cleanup remain in place.

The actual Kotlin/JDK test replay completed 10/10 cases, including the exact 8 KiB target, 32 KiB body, 128/256 field boundaries and an accepted exact 256 KiB response. Controlled runs removing the target, field and response guards failed their intended cases. These are the focused production-client checks for those postimages.

At 0e8369b0, the Python adapter protocol test executes the maintained HTTP adapter and ContentBridge with a disposable subprocess at the Android content-command boundary. Python 3.12.10 completed its one concurrent acceptance scenario; a forced-sequential control failed the assertion that the independent B session progresses while A is blocked. Six content-command calls were retained. The adapter source is unchanged in that test forward.

Native server at 911188d3

The complete changed provider, store, and core handle compiled with Kotlin 2.2.21 for JVM 1.8. Six focused scenarios passed using retained JDK 17 / Robolectric 4.12.2 / Android API 29: failed native setup closes once without an ID; owner isolation and idle expiry; caller/input admission; independent-session progress under a held snapshot; one core handle across two snapshots; and cleanup of initialization completing after its deadline.

Those runs execute the actual core handle, HandleProxy, GuardCallback, and Android Binder/Bundle/JSON behavior with a recording VM/factory and controlled local preferences. The provider was manually attached using retained resources. Installed manifest/component behavior, Google VM/JNI, an Android device, and a new APK were not exercised.

The production Python HTTP/subprocess adapter completed ten requests and seven content-command fixtures covering lifecycle, byte preservation, authentication, invalid input, and provider errors. This verifies the transport implementation at an explicit fixture boundary, not a connected device. The setup guide records the exact source blobs and limits.

Session serialization and client dispatch

At 04fbd911, the actual Kotlin session/HTTP classes against a latched loopback server reproduced snapshot/snapshot and snapshot/close overlap before the change and prevented both afterward; a separate session completed while the first was held. Its maintained four-case session class passed. Source and execution scope.

At 02f6c0f0, the complete public client/session/HTTP code ran with the actual retained microG TaskImpl and existing loopback server. The original three interop cases plus two rejection regressions passed 5/5; the preceding source failed the two new cases. One worker admission and result-before-close ordering were asserted. These focused results are separate from the earlier integrated APK below.

Integrated Android run at 7a648d5f plus the retained test patch

Run 37193312230, job 111409876131, succeeded after checking out product 7a648d5fe5f073c61ef7b202aef4c74dd329f4be / tree 24896870026d0edb04989c9627b4a1dc0586670d.

The checkout applied only the retained session lifecycle test patch v2 (SHA-256 497af7ad875d46645c85eff4f2d02711678dc0513fbf70f4359ccc7636b7fc24; test blob f300e7fad7a6a9649d6c591f36a701b6bf7ee80b). No production file changed in the execution checkout.

The three selected classes produced 12 actual XML tests, 0 failures, errors, or skips:

Class Cases
RemoteDroidGuardInitializationTest 5
RemoteDroidGuardSessionTest with v2 patch 5
RemoteDroidGuardHttpClientTest 2

:play-services-droidguard-core:testDebugUnitTest and assembleVtmDefaultDebug completed with Gradle exit 0. The unit task executed. Toolchain: Gradle 8.13, OpenJDK 17.0.20.1, Android API 35 / build-tools 35.0.0.

That APK belongs to the 7a648d5f production source. The session test extension was transient validation input; the report records it separately.

Later callback-order repair at 4fec1b39

The callback-order report records execution of the complete original/repaired production service and the remote handle, initialization, session, HTTP, and fallback classes against a real loopback HTTP server with disposable Android/AIDL/context/preference collaborators.

Two ordering cases failed before the repair and passed afterward; the two existing callback-failure/rejected-executor controls remained passing. The ordering assertions use latches and still verify one callback, one close, and expected result bytes.

With a deliberately delayed 300 ms close response, the recorded callback-latency median across three samples changed from 391.153 ms to 42.407 ms. These are observations from that controlled loopback scenario. Cleanup still occupies the executor worker; this is callback ordering/latency evidence, not a sustained-throughput measurement.

The current callback change has that focused production-class replay. The integrated APK and 12-case Gradle result above are for its preceding source pin.

Earlier source results retained

The original 3f10ba2b transport/session change passed its four focused offline Gradle tests and compiled the module/AIDL. The 7a648d5f initialization follow-up separately passed five maintained JVM checks and four stubbed production-handle loopback scenarios. Those earlier counts overlap with subsequent selections and are not added together.

Device acceptance

Real remote-server/device Play Integrity behavior and an actual Dott unlock-and-ride video remain pending. The linked software checks do not contain that field result.

Refs #2851.

Original-author conditional bounty / compensation request

I, @woahwhattheheck (GitHub ID 293286387), submit and affirmatively claim the original DroidGuard transport, lifecycle, request-admission, Android provider and client integration work recorded in this PR for any applicable issue/bounty compensation. Please identify the corresponding eligible BountyHub or sponsor program, acceptance criteria, payable amount and payout registration route before treating this original contribution as uncompensated work. Third-party protocol/source credit to GautamKumarOffical PR #3575 is explicitly retained; this request covers my own integration and changes, not someone else's original implementation.

Neither BountyHub listing eligibility nor portal registration for #2851 is verified by this update. The attached unit/Gradle results retain their exact historical source pins; actual device/Play Integrity/Dott unlock-and-ride acceptance remains outstanding. This metadata update changes no source, tests, PR head or evidence.

## Summary

- Implement `guardWithRequest` on the bounded executor and close its handle on every completion path.
- Reuse remote server state through the existing `init` / repeated `snapshot` / `close` lifecycle.
- Centralize HTTP request encoding, timeouts, non-success status handling, and connection cleanup.

## Verification

- `./gradlew.bat :play-services-droidguard-core:testDebugUnitTest --tests org.microg.gms.droidguard.core.RemoteDroidGuardHttpClientTest --tests org.microg.gms.droidguard.core.RemoteDroidGuardSessionTest --offline`
- BUILD SUCCESSFUL; 4 focused tests pass, and the task compiles the module and AIDL.
- `git diff --check` passes.

This verifies local transport/session behavior. A real server/device Play Integrity flow remains unverified. Related issue: microg#2851.
@woahwhattheheck

Copy link
Copy Markdown
Author

To align the remote-session lifecycle with the intended Play Integrity call sequence: this change keeps one remote session alive across repeated snapshot() calls on a retained DroidGuardHandle. In this tree, getResults() and service guardWithRequest() each open a handle, take one snapshot, and close it.

Can you confirm whether the required multi-step sequence reuses one handle for multiple snapshots, or expects state to persist across separate one-shot getResults()/guardWithRequest() calls? This PR covers the retained-handle sequence; it does not demonstrate the real server/device or end-to-end Dott flow.

@D3SOX

D3SOX commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

Did you read the bounty note?

This is probably a general issue with Firebase SMS authentication. For the bounty to be released the Dott app should work to unlock and ride the scooters.

Can you show a video where it works?

@tokenjunkielabs

Copy link
Copy Markdown

Thanks for clarifying the bounty's acceptance criterion. PR #3841 implements the remote session transport path and includes four focused local session/transport tests. It has not been exercised with Dott unlocking or riding against a real server/device, so I cannot provide a video demonstrating that outcome. The PR body records this limit; end-to-end Dott behavior remains unverified.

woahwhattheheck and others added 23 commits October 4, 2026 04:18
… ci]

Keep each initialization pending until session setup finishes, including when a synchronous snapshot overtakes one-way init dispatch. Bind snapshot metadata to the observed generation and close late sessions after handle shutdown.

Validation: five maintained JVM tests passed. A loopback HTTP replay of the actual RemoteHandleImpl source with disposable platform stubs improved from 1/4 to 4/4 scenarios: pending begin, snapshot before init dispatch, close during begin, and completed reinitialization. No full Gradle, Binder, device, Play Integrity, or Dott scooter acceptance claim.
Retain the successful three-class, twelve-case Gradle run and VtmDefaultDebug APK receipt for product 7a648d5 plus the original lifecycle test patch. Keep the tested source separate from later PR revisions; no production file is changed.
Move result delivery inside the existing task's cleanup finally boundary. A slow remote close no longer delays the completed callback; fallback, rejection, logging and cleanup on callback failure are preserved. Record the focused production-class loopback replay and controlled callback-latency results, preserving the concurrent integrated Android report.
Add a DroidGuardClient that returns Task<DroidGuardHandle> and then uses
snapshot, isOpened, and close against a begin/snapshot/close session backend.
Loopback interop covers retained-handle reuse and rejects a single-attest
server that has no session. This does not mint Play Integrity or device results.

Co-authored-by: woahwhattheheck <woahwhattheheck@users.noreply.github.com>
Copy the public String map into a session map so handle.snapshot compiles
and still reuses the retained remote session.

Co-authored-by: woahwhattheheck <woahwhattheheck@users.noreply.github.com>
Keep begin, snapshot, and close ordered within one retained session while
allowing other sessions to progress independently. Prevent close from
retiring remote state during an in-flight snapshot.

Direct production-class loopback execution reproduces both overlaps on the
parent and removes both with this change. The maintained session class
passes four focused JUnit checks. JVM evidence and Android/device limits
are recorded in SESSION_SERIALIZATION.md. Preserve the latest retained
handle client and snapshot map repair.
Return failed Tasks when the supplied executor rejects initial admission. Keep begin, snapshot, result publication and close in one accepted getResults worker, so a second dispatch cannot strand the result Task or the opened remote session. Use trySetException after task publication to avoid duplicate completion when a listener throws. Preserve result-before-close ordering and existing per-session serialization.

Extend RemoteDroidGuardHandleInteropTest with rejected-admission and accept-once executor cases. The three existing interop cases remain unchanged. With the actual microG TaskImpl, full current client/session/HTTP classes and the existing loopback server, parent 04fbd91 passes 3/5 and fails both new cases (RejectedExecutionException and DuplicateTaskCompletionException); this change passes 5/5. The accepted-worker case observes result completion before close and exactly one executor submission.

Focused JVM validation used retained OpenJDK 17, Kotlin 2.2.21 and JUnit 4.13.2. Executed source blob 2836391; test blob dc2b839. Android packaging and device acceptance remain separate.
Add a default-disabled provider and authenticated host adapter for the existing begin/snapshot/close protocol. Retain actual core handles, check readiness, preserve initialization metadata, and return native result bytes through an explicit response envelope. Runtime caller checks restrict the bridge to authorized local operators; the ordinary service broker is unchanged.

Bound active and closing sessions, work queues, operation waits, and idle lifetime. Reject overlapping work for one session before occupying a global worker, so another session can progress. Close failed native setup and late initialization results.

Compile the complete changed Kotlin production classes for JVM 1.8 and execute six focused scenarios with retained Robolectric API29 and a recording VM boundary: 6 passed. Exercise the production Python HTTP/subprocess adapter with ten requests and seven content-command fixtures. The setup guide records source blobs, outcomes, donor credit, and execution limits. No APK, installed-provider, Google VM, Play Integrity, or Dott ride result is asserted.
The native runtime can return encoded error bytes through a successful snapshot response. Clarify that an explicit provider error status becomes an HTTP error; Base64 validity alone does not establish attestation success.
Generate a test-only public API JAR from the active JDK module and resolve jdk.httpserver in unit-test JVMs. The ordinary module task passes all 17 maintained cases on validation tree 4866ca6 in run37234495146, without an init script, filters, or APK assembly. No production dependencies or API behavior change.
Preflight request-target bytes, form-body bytes, and adapter field counts before opening a connection. Preserve the existing response cap and cover accepted and rejected protocol boundaries.
@woahwhattheheck

Copy link
Copy Markdown
Author

/claim #2851

I claim the applicable allocation from the advertised $85 USD bounty for my contribution in this PR, payable to the original contributor @woahwhattheheck. The contribution and remaining acceptance requirements are documented in the existing PR; this claim preserves the original contributors' attribution. Please confirm eligibility and the award upon acceptance, payment method, and payout date.

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.

4 participants