app
SOLIDWORKS Bridge
Public Made by Adomby adom
Launch, model and quit SOLIDWORKS from any Adom container. Nothing is ever lost.
main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
//! 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()
);
}