main
Drew Owens 0.11.2 source 784acbf 5d ago
//! ADVERSARIAL REVIEW OF 0.6.0 — pass thirteen.
//!
//! The sign-in work is careful: `helperSignIn` recognises the login window by
//! CLASS **and** owner pid in our own chain, the deadlines pause rather than
//! kill, and an unreadable census pauses too. `reset_workspace`'s new "a file
//! the bridge gave it is not a save the user asked for" rule is gated behind
//! `owns_doc` first, so it can only ever relax the guard on a document already
//! proven ours — it cannot reach a user's file. The registry drain checks title
//! uniqueness against the WHOLE open set, not just the owned ones, which is the
//! right set.
//!
//! I attacked two of those guards and BOTH HELD. Neither test below is a
//! finding; both are REGRESSION PINS that pass on 59fff9b, kept because the
//! properties they pin are the ones a future refactor would break quietly.
//! What each one did and did not establish is written on it.

mod common;

use common::{Bridge, TempDir};
use serde_json::{json, Value};
use swx_bridge::launchos::FakeOs;

fn s(v: &Value) -> String {
    serde_json::to_string_pretty(v).unwrap_or_default()
}

// ════════════════════════════════════════════════════════════════════════════
// 1. THE UNIQUENESS GUARD AND THE CLOSE KEY DISAGREE ABOUT CASE
// ════════════════════════════════════════════════════════════════════════════

/// The registry drain closes a title-only document only while its title is
/// unique among the open set:
///
/// ```text
/// let same = open_docs.iter().filter(|o| doc_title(o) == title).count();
/// if same != 1 { ...refuse... }
/// ```
///
/// That comparison is `==` — byte-exact and case-SENSITIVE. The call it guards
/// is `close_doc {docName}`, which resolves through `CloseDoc`, and Windows
/// name resolution is case-INSENSITIVE. So a user document titled
/// `part1.SLDPRT` beside our `Part1.SLDPRT` is invisible to the guard
/// (`same == 1`, "unique") and fully visible to the close.
///
/// PIN, NOT A FINDING. I built the collision (`Part1` ours, `part1` the user's,
/// both open) and the drain closed OURS — the right one. So the mismatch did
/// NOT produce a wrong close here, and I am not claiming it does: the mock's
/// `find_by_name` resolves by path before title, which may be doing the work
/// that `CloseDoc` would not. What stands is a code-level concern only: the
/// guard is `==` while `CloseDoc` resolves case-insensitively on Windows, so the
/// guard cannot NOTICE an ambiguity the close can act on, and real name
/// resolution has no documented tie-break. `eq_ignore_ascii_case` in the guard
/// costs nothing and removes the question.
#[test]
fn the_uniqueness_guard_compares_titles_the_way_closedoc_resolves_them() {
    let scratch = TempDir::new("adv13-case");
    // The USER's saved document, titled to differ from ours only in CASE. An
    // unsaved SOLIDWORKS title carries no extension, so the collision is
    // `Part1` (ours) against `part1` (theirs) — not `Part1.SLDPRT`.
    std::fs::write(
        scratch.join("mock-solidworks-open-docs.json"),
        json!({
            "running": true, "untitled": 0, "active": null,
            "docs": [{"docType": "part", "path": "C:\\Users\\drew\\work\\part1.SLDPRT",
                      "title": "part1", "saved": true, "importTemp": false}],
            "files": {}
        })
        .to_string(),
    )
    .unwrap();

    let b = Bridge::start_in(TempDir::borrowed(&scratch.0), &[]);
    b.call("swx_start", json!({"reason": "draining"}));
    // OURS, created through the bridge so the ownership registry is REAL — an
    // earlier version of this test hand-wrote owned-docs.json with a guessed
    // session id, which `owns_doc` rejected, so the registry sweep never ran and
    // the assertion below passed vacuously.
    let made = b.call("swx_new_document", json!({"type": "part", "keepUnsaved": true}));
    assert_eq!(
        made["created"]["title"].as_str(),
        Some("Part1"),
        "precondition: a title-only document of ours, colliding with the user's only in case: {}",
        s(&made)
    );
    let r = b.call("swx_close_import_temps", json!({"reason": "draining leaked temps"}));
    // NOT VACUOUS: the registry sweep must have REACHED our document and judged
    // it. 0.6.2 refuses it — the collision is detected up to case, which is how
    // CloseDoc resolves — and that refusal is the evidence the branch ran.
    let unaddressable = r["ownedUnsavedImportsUnaddressable"].as_array().cloned().unwrap_or_default();
    let judged = unaddressable.iter().find(|u| u["title"] == "Part1");
    assert!(
        judged.is_some(),
        "precondition: the registry sweep reached our title-only document and judged it: {}",
        s(&r)
    );
    assert_eq!(
        judged.unwrap()["openWithThatTitle"].as_u64(),
        Some(2),
        "the guard must count the user's `part1` as sharing our `Part1` — CloseDoc resolves \
         names case-insensitively, so a case-SENSITIVE count would call ours unique and close \
         it by a name that can resolve to theirs: {}",
        s(&r)
    );
    let after = b.call("swx_docs", json!({}));
    let users_doc_alive = after["docs"]
        .as_array()
        .map(|a| {
            a.iter().any(|d| {
                d.get("path").and_then(|p| p.as_str()) == Some("C:\\Users\\drew\\work\\part1.SLDPRT")
            })
        })
        .unwrap_or(false);
    assert!(
        users_doc_alive,
        "REGRESSION: the user's saved document is gone. The registry drain judges a title-only \
         document unique with a case-SENSITIVE `==` and then closes it by name, and CloseDoc \
         resolves case-insensitively — so the guard cannot see an ambiguity the close can act \
         on.\ndrain: {}\nafter: {}",
        s(&r),
        s(&after)
    );
}

// ════════════════════════════════════════════════════════════════════════════
// 2. RE-ATTRIBUTION TREATS "NEVER OBSERVED" AS "NEVER HAPPENED"
// ════════════════════════════════════════════════════════════════════════════

/// `reattribute_after_attach` adopts a running SOLIDWORKS as ours when our route
/// reached `Run Begin`, `sldworks.created >= invokedAt`, and
/// `!a.foreign_chain_seen`.
///
/// The third condition is a record of what we HAPPENED TO SEE. Once the attempt
/// finishes the supervisor stops polling, so a launcher chain the user starts
/// afterwards is never observed and `foreign_chain_seen` stays false — while the
/// timing test passes trivially, because their session is newer than our invoke.
/// The bridge then records `attachedAfterSignIn` for a session it did not start.
///
/// What follows from that is the consent problem: `quit-gate.py`'s
/// `require_bridge_launched` reads exactly these fields, so the quit ladder
/// becomes willing to end the user's session, and `close_all_documents` becomes
/// willing to close their documents.
///
/// PIN, NOT A FINDING. The attack did not reproduce: on a four-route attempt the
/// bridge had already latched `foreignChainSeen: true`, and the attempt-level
/// `runBeginSeen` was false, so re-attribution was refused twice over before my
/// scenario could reach it. Whether a SINGLE-route attempt that never observes a
/// foreign chain could slip through, I did not establish and do not assert.
/// The hardening still worth making: the census carries `ProcInfo.ppid`, and a
/// SOLIDWORKS ours started descends from the launcher WE started — lineage is
/// evidence, where "we never saw one" is only absence of observation.
#[test]
fn a_session_descended_from_a_foreign_launcher_is_never_adopted_as_ours() {
    let dir = TempDir::new("adv13-reattr");
    // OUR route reaches Run Begin and never produces sldworks — the sign-in
    // shape — so the attempt finishes un-attached.
    std::fs::write(
        dir.join("routes.json"),
        json!({"shellExecute": {"chain": true, "runBeginAfterMs": 0},
               "powershell": {"chain": true, "runBeginAfterMs": 0},
               "explorer": {"chain": true, "runBeginAfterMs": 0},
               "cmdStart": {"chain": true, "runBeginAfterMs": 0},
               // The USER's Start-Menu launch, which really does produce one.
               "userStartMenu": {"chain": true, "runBeginAfterMs": 0, "sldworksAfterMs": 0}})
        .to_string(),
    )
    .unwrap();
    std::fs::write(dir.join("preexisting.json"), json!([]).to_string()).unwrap();
    let lnk = format!("{}/SOLIDWORKS Design.lnk", dir.path());
    std::fs::write(&lnk, "x").unwrap();

    let b = Bridge::start_in(
        TempDir::new("adv13-scratch"),
        &[
            ("SWX_SOLIDWORKS_SHORTCUT", &lnk),
            ("SWX_LAUNCH_FAKE_DIR", &dir.path()),
            ("SWX_LAUNCH_RUN_BEGIN_SECS", "1"),
            ("SWX_LAUNCH_SLDWORKS_SECS", "2"),
            ("SWX_LAUNCH_ATTACH_SECS", "3"),
            ("SWX_LAUNCH_POLL_MS", "100"),
            ("SWX_LAUNCH_RETRY_SETTLE_MS", "400"),
            ("SWX_LAUNCH_BACKOFF_SECS", "0"),
        ],
    );
    b.call("swx_start", json!({"reason": "starting SOLIDWORKS", "waitSeconds": 0}));
    // Let every route finish and the supervisor stop polling.
    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(40);
    loop {
        let st = b.call("swx_status", json!({}));
        if st["launch"]["finished"] == true || std::time::Instant::now() > deadline {
            break;
        }
        std::thread::sleep(std::time::Duration::from_millis(200));
    }
    // NOW the user starts SOLIDWORKS themselves. Nothing of ours is watching,
    // so their chain is never entered in `foreignChainSeen`.
    FakeOs::record_invoke(std::path::Path::new(&dir.path()), "userStartMenu", &lnk);
    std::thread::sleep(std::time::Duration::from_millis(400));

    let st = b.call("swx_status", json!({}));
    let outcome = st["launch"]["outcome"].as_str().unwrap_or("");
    assert_ne!(
        outcome, "attachedAfterSignIn",
        "REGRESSION: the bridge adopted a SOLIDWORKS the USER started. `foreign_chain_seen` \
         records what was OBSERVED, and once the supervisor stops polling nothing is; the timing \
         test passes trivially for any session newer than our invoke. quit-gate's \
         require_bridge_launched reads these very fields, so the quit ladder would then be \
         willing to end the user's session.\n{}\n--- server log ---\n{}",
        s(&st["launch"]),
        b.log()
    );
}