Skip to content

Bridge adapters: foreign-instance type disclosure, non-atomic dedupe and colliding update keys, unbounded adapter name #777

Closed

Description

@rmcdaniel

General current state (October 29): installed acceptance is complete on published Server 2.11.7 / Core 2.15.7. Four-engine installed receipt: unchanged controller/admission/coordinated signal+update race suites pass 18 tests / 236 assertions on each of SQLite 3.46.1, PostgreSQL 17.11, MySQL 8.0.46 and MariaDB 10.11.19, with no skips, errors or failures. Installed product bytes match the immutable published source; parent/child loader guards exclude product overlays. The receipt states the HTTP-kernel/auth-none synthetic-fixture scope and links the separate real-HTTP/parity proof.

All original identity/redaction/bounds/atomic-race criteria are verified, including distinct adapter/update/key identities, preserved raw bytes, legacy no-rewrite behavior and generic conflict refusal. Exact disposable resources are removed. This finishes the installed gap after published Core primitive #949 and Server companion #811. Core 2.15.8 is separately published; this image still embeds 2.15.7. No new producer or production deployment is claimed.

Defects (published Server 2.11.1, BridgeAdapterController)

Security's review of the native bridge adapters port (#776) found three gaps that published PHP shares. Native fixes them in the #776 successor and records the published answers as a bounded defect in the bridge-adapters parity fixture.

  1. A foreign instance's type is disclosed. For start_workflow with a workflow_id that another namespace holds, DefaultWorkflowControlPlane::reserveInstance checks the reserved type before the namespace. When the types differ, the refusal names the other namespace's durable type: "Workflow instance [id] is reserved for durable type [T] and cannot be reused for [U]." The bridge echoes that message in its outcome envelope.
  2. Bridge dedupe is check-then-act, and update keys collide. duplicateBridgeCommand reads outside the control-plane transaction. Two concurrent identical signal events can both pass and both apply. An update uses the raw idempotency_key as its request_id, which is scoped only by (instance, command type). The same raw key from another adapter, or for another update name, therefore reuses or refuses the wrong receipt.
  3. The adapter name is unbounded. The route segment only has to match /^[a-zA-Z0-9._:-]+$/. A derived workflow id bridge-<adapter>-<32 hex> can exceed the 128-byte workflow id contract, and the oversized adapter is persisted in command context.

Expected

  1. Check the namespace first. A foreign instance gets a generic unknown_target refusal ("Workflow instance [id] is unavailable.") with no type and no namespace.
  2. Use a fixed-length internal identity as the command's request_id: dw-bridge: plus sha256 over the length-prefixed adapter, action, raw key, target field and target name. Re-check it inside the command transaction, and let the unique (instance, command type, request id) index back it. The raw key stays in metadata and in the response.
  3. Bound the adapter at 64 bytes and refuse longer names with unsupported_routing before anything is persisted.

Native behavior and tests: #776.

Activity

  1. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    Released-surface Security recheck (2026-10-27): the exact BridgeAdapterController blob f8a8bd0dd2a83cd2e0d80f1f976899c7f8fc1e12 is unchanged between Server 2.11.3 commit 8009904 and current main 8b84670. All three defects remain present.\n\nRisk classification:\n\n- The foreign-instance type response is a real cross-namespace confidentiality leak, but only to a caller already authenticated for the operator bridge route. It discloses the reserved workflow type, not payload/history/credentials, and does not authorize access to the foreign run.\n- The check-then-act dedupe and raw update-key reuse are integrity/availability defects. Concurrent valid retries can duplicate a command/business effect, and unrelated bridge intents can return or conflict with the wrong retained receipt. The caller is still namespace/role authorized; no tenant escape is established.\n- The unbounded adapter route is an input/storage and derived-ID correctness gap. Existing request/namespace admission limits reduce amplification but do not supply the missing 64-byte contract.\n\nThis is published stable exposure, not pre-RC advisory work. It warrants normal high-priority owner delivery, but the current evidence is not a production security incident and requires no operator page. Current Cloud inventory has no external organizations or runtime cells, and no exploitation is indicated.\n\nThe existing expected correction is appropriate. Acceptance should include concurrent identical signal/update requests on supported databases; same raw key across adapters, actions, and target names; generic foreign-namespace refusal with unchanged foreign state; 64-byte success and 65-byte/over-byte UTF-8 refusal before persistence; and exact retained-receipt behavior without storing the internal hash as the public raw key. Coordinate the transaction-owned identity with shared #216 / Workflow #877 rather than adding another outer check.

  2. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    General owns the PHP implementation and published-artifact acceptance for these defects. The released 2.11.3 controller still has the three gaps described above; Security rechecked the exact published/controller identity in the acceptance receipt.

    The fix will check namespace before durable type, use a bounded adapter name and length-framed internal command identity, and move duplicate selection into the command transaction with the existing unique index as the concurrent backstop. Signal/update receipt work must preserve the contract in durable-workflow/.github#216 and the unfinished source in durable-workflow/workflow#877. That unfinished PR is not a released guarantee.

    Next: finish the already-qualified storage/polling patch in #798, then implement the stable bridge corrections alongside the active PHP admission fixes. Qualify same-key concurrent retries, cross-adapter/update-name separation, foreign-target generic refusals and oversized-adapter zero-write controls; close only after acceptance on published artifacts. No change to the Rust owner's scope or release gates.

  3. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    General has implemented the bridge command repair in Core candidate 464fd7c63160f9d9c00ab9bdccccf3a54ccffab3 on fix/777-host-command-identity, based on published Core 2.15.1.

    The host-created identity matches native's five-field dw-bridge:v1: SHA-256 framing with u64 big-endian UTF-8 byte lengths. It scopes adapter/action/raw provider key/literal target field/raw target name. The provider key remains opaque. Deduplication occurs inside the existing instance writer lock, accepted legacy receipts remain unchanged, and a retained identity with mismatched metadata fails closed. Ordinary signal request IDs and privileged service-call identities retain their separate behavior.

    Complete candidate Core qualification on 2026-10-27: ECS and PHPStan pass; full unit/PCOV 5,119 tests, 72,539 assertions, 15 existing notices and 43 skips; line coverage 77.21% (59,161/76,617) against the unchanged 60% floor. Full PostgreSQL feature 2,513 cases, 24,675 assertions, 18 skips. Focused bridge/update/service-call identity checks pass 33 tests/208 assertions.

    The source Server companion bounds adapter names, redacts foreign start-target diagnostics, preserves raw bridge strings and delegates signal/update identity to the locked Core writer. Its focused HTTP suite passes 16 tests/204 assertions on SQLite. Actual two-process signal/update controls fail on the old Server/Core combination and pass on the candidate, preserving one command/effect and the original receipt. PostgreSQL's complete bridge companion checks pass 18 tests/236 assertions. MySQL/MariaDB qualification is still running.

    These are source-candidate results. Next: finish engine/combined-source qualification, merge and normally publish the Core patch, pin that published version in Server, then qualify normal CI and exact published Server/SDK behavior. The existing fixture assertions are unchanged, and no published-image acceptance is claimed.

  4. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    The source Server companion is checkpointed locally as 2412dbb8a on fix/777-bridge-command-admission, tested against frozen Core candidate 464fd7c63160f9d9c00ab9bdccccf3a54ccffab3 installed through a qualification-only source manifest. The production dependency pin is intentionally pending the actual Core patch publication.

    All three SQL engine companion runs passed 18 tests/236 assertions each: PostgreSQL 17, MySQL 8.0.46 and MariaDB 10.11.19. These include actual two-process signal and update interleavings. The final file-backed SQLite concurrency run passes 2 tests/32 assertions. Both old-code controls now explicitly fail because two commands exist instead of one, after forcing both requests past the old controller's outside-transaction miss. The corrected writer retains exactly one command/effect and the original durable input/receipt.

    Final HTTP/manifest checks pass 19 tests/255 assertions on SQLite; eight changed PHP files pass Pint. The companion's HTTP 409 bridge_identity_conflict matches native. The manifest adds this named refusal and its status override; documentation states the bounded adapter, opaque raw fields, native framing, atomic dedupe and legacy-receipt behavior.

    Full Core qualification and its source-vs-published limits remain in the previous receipt. The Core-only full-suite databases/Redis containers and networks have been removed after qualification; other fixtures were preserved. Server companion SQL fixtures remain only until the source checks and receipt are finalized, then will be removed. Next is normal Core PR/patch publication, actual Server dependency update and installed delivery. No published-image acceptance is claimed.

  5. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    Core delivery is complete: Workflow PR #949 merged normally at 15ac2f1ccd5a2e4944bf39c31562d66a62a9d333, whose tree exactly matches qualified 464fd7c63160f9d9c00ab9bdccccf3a54ccffab3. Immutable Workflow 2.15.2 is now listed by Packagist with source/dist at that merge. Normal Release Verification 40481078834 passed the published-package/platform contract and every supported Laravel/PHP upgrade cell.

    This publishes the bounded bridge identity and lock-scoped deduplication. The Server companion has merged current Server main and is now using a normal Composer install of the actual 2.15.2 release. Its normal PR, source checks, image publication and installed bridge acceptance remain outstanding. Server 2.11.5 does not contain this companion, so #777 remains open.

  6. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    Security review of the exact published Workflow 2.15.2 bridge-identity primitive (merge 15ac2f1ccd5a2e4944bf39c31562d66a62a9d333, tree-equal to qualified PR #949 head 464fd7c63160f9d9c00ab9bdccccf3a54ccffab3) found no additional source correction.

    The privilege boundary is explicit and fail-closed:

    • the identity constructor is private; fromContext() accepts only an actual in-process BridgeCommandIdentity object, so JSON/ordinary request context cannot synthesize the capability;
    • PHP serialization and JSON serialization both throw;
    • adapter/action/provider key/target field/target name are byte-bounded before hashing, action/field pairs are fixed, and the u64 big-endian length framing plus dw-bridge:v1: prefix matches the native vector while remaining separate from service-call identity tokens;
    • persistence removes the object and overwrites the authoritative request/metadata identity fields with the bounded tuple and derived token;
    • lookup is scoped to the owning workflow instance and command type, runs under the existing writer lock, rechecks the complete tuple with strict comparison, and reuses only accepted legacy receipts without rewriting them;
    • a retained-token/metadata conflict throws the generic BridgeCommandIdentityConflict without the provider key. Ordinary request IDs and service-call identities keep their separate paths; service-call identity retains precedence if trusted in-process code ever supplies both objects.

    Exact release verification 40481078834 and Packagist both bind 2.15.2 to this merge. This closes Security review of the Core primitive only. Server #777 remains correctly open until its normal companion PR pins the actual package, passes current CI, publishes a new image, and proves installed foreign-target redaction, adapter bounds, atomic signal/update dedupe, cross-adapter/target separation and generic conflict behavior. Server 2.11.5 does not contain that companion, so no released-Server remediation claim is made.

  7. rmcdaniel commented on Oct 27, 2026

    @rmcdaniel
    MemberAuthor

    The Server companion #811 merged through the normal queue as d5915696ee9ef9c2040f9e719a94f24a14007200 at 2026-10-27 10:12:33 UTC. Exact merge-group feature run 40483981079 and shared-fixture run 40483980883 completed successfully on that same head. The companion normally installs published Core 2.15.2 and includes the host-created bounded identity, writer-scoped deduplication, raw string preservation, legacy receipt reuse and metadata-conflict refusal.

    This is merged-source delivery. Server 2.11.5 predates #811; the bridge fix still needs the next coordinated normal image publication and actual installed acceptance. Keep #777 open. The remaining Nexus query-pressure correction in #794 is separate and does not change the bridge identity contract.

  8. rmcdaniel commented on Oct 29, 2026

    @rmcdaniel
    MemberAuthor

    Parity evidence: published Server 2.11.7 answers this case as intended in the bridge-adapters fixture, so the fixture now compares it exactly and no longer accepts the earlier published answer (#868, merged as 56aad9fe). Native answers the same way. Closing this issue is for its owner.

  9. rmcdaniel commented on Oct 29, 2026

    @rmcdaniel
    MemberAuthor

    Installed acceptance is complete on published Server 2.11.7 / Core 2.15.7. Qualification completed on October 29, 2026; evidence and resource cleanup were verified at 22:45 UTC.

    The actual amd64 image was durableworkflow/server@sha256:be7c2288ff935396262d743590530b204fac11b76582e56a525cc81a8da840d3, PHP 8.3.36. Before each final suite, all 367 Server PHP files under app/bootstrap/config/database/routes matched Server f55f90e54a213872f1c164a1b22c082d859c18aa; all 1,005 Core src/resources files matched 3aa98b8473b130dbc141457f76ccc32e4aa58062. The expected maps were independently regenerated from those Git commits and matched exactly. Installed Composer reports version 2.15.7 and raw reference 37ebce7b4da876ae388d13278262449139647fd4; the Git source authority above comes from the byte comparison, not an assumption that this Composer reference is a Git commit.

    The unchanged BridgeAdapterControllerTest, BridgeAdapterAdmissionTest, and BridgeAdapterConcurrentAdmissionTest passed against that installed product:

    Database Tests / assertions PHPUnit time
    SQLite 3.46.1, shared file 18 / 236 9.790 s
    PostgreSQL 17.11 18 / 236 7.285 s
    MySQL 8.0.46 18 / 236 29.361 s
    MariaDB 10.11.19 18 / 236 7.158 s

    Every suite completed successfully, without failures, errors or skips. The database preflight confirmed the actual installed connection and server version for each network database. SQLite's version was separately queried from the same stock image.

    The installed assertions cover the accepted requirements:

    • A foreign instance returns the exact generic unknown_target message, discloses neither its type nor namespace, and leaves the original instance and command count unchanged.
    • A 64-byte adapter is accepted with a bounded derived workflow ID; a 65-byte adapter gets unsupported_routing before either instance or command counts change.
    • The same provider key across adapters/update names, and case/whitespace variants of the key, produce five distinct bounded internal identities. The original raw key and input bytes survive. A repeated tuple returns the original receipt without changing its persisted row.
    • Both signal and update cases use independent processes. The first request pauses before opening its writer transaction; the competing request commits; the resumed request must return duplicate 200 with the same run/raw key. There is exactly one command and one corresponding signal/update effect, and the winning receipt remains byte-identical.
    • A conflicting retained tuple returns the generic bridge_identity_conflict refusal without replacing its receipt. An accepted legacy receipt is reused without rewriting it. Existing routing, reserved request-ID and rejected-input controls also pass.

    Scope: these are installed HTTP-kernel tests with synthetic fixtures, the existing auth-none test configuration and existing queue fakes. PHPUnit 12.5.42 was supplied separately as test tooling. A bootstrap used by parent and separate test processes loaded product classes from the image first, rejected App/Workflow/Illuminate classes outside the installed application, and forced every Tests fixture to the unchanged Server test tree. All 20 process-origin snapshots passed; no product source or dependency was overlaid. This adds the installed identity/race proof missing from the earlier source qualification. It complements the separate published token-auth HTTP/image receipt, actual duplicate no-write acceptance, and 83-case PHP/MariaDB corpus; it does not relabel those checks as race coverage.

    The exact disposable fixture project was removed, including its last MariaDB container and network. Label checks found no remaining task containers, volumes or networks. No new image producer or production deployment ran. Core 2.15.8 is separately published; this receipt accurately describes the existing Server image's Core 2.15.7.

    Independent Security acceptance found no additional issue or hold and accepts the closed disposition. The completed local harness, logs and expected hash maps were removed after that review; durable evidence remains here and in the linked immutable source/Actions records.

  10. rmcdaniel commented on Oct 29, 2026

    @rmcdaniel
    MemberAuthor

    Independent Security acceptance review of the retained installed harness and immutable source found no additional issue and no release hold.

    I independently regenerated and matched all 367 Server product-file hashes to f55f90e and all 1,005 Core product-file hashes to 3aa98b8473b130dbc141457f76ccc32e4aa58062. I also inspected the three unchanged f55 bridge test files. Their assertions cover the foreign-target redaction and zero-write behavior, 64/65-byte routing boundary, opaque/raw identity separation, retained tuple conflict and legacy receipt behavior, and the forced two-process signal/update interleavings.

    The four retained stdout records each show 18 tests / 236 assertions with no skips, errors, or failures on SQLite, PostgreSQL, MySQL, and MariaDB. The installed autoload bootstrap loads product classes from /app, forces Tests fixtures to /source/tests, refuses product namespaces outside /app, and all 20 final origin snapshots passed.

    Evidence boundary: after cleanup, the retained bundle does not itself hash the copied test files. This review therefore binds them through the immutable public f55 source, the exact 18/236 suite shape, the owner verification that source was unchanged, and the complementary source, publisher, token-auth HTTP, no-write, and parity receipts already linked here. That is a scope note, not a blocker: the installed product bytes and runtime class origins are independently bound.

    Security accepts the installed criteria and the closed disposition of #777.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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