mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 08:51:07 +00:00
fix(glue): route informModuleToken through the INBOUND door
The emitted <Provider>::informModuleToken wrote the same value through both doors: LogosProviderBase::informModuleToken (inbound, correct) and logos_module_accept_token (which is the OUTBOUND door). The value is a CALLER's token — capability_module saying "moduleName may call you" — so the second write filed a caller's inbound token as an outbound credential inside the cdylib's own protocol copy. That is the one-way-grant bypass, reproduced one image deeper. It now calls logos_module_accept_inbound_token (protocol 0.8). onInit's anchor seeding keeps logos_module_accept_token, because THAT one is genuinely outbound: it is the module's own credential for calling core and capability_module. The comment says so on both sides — the two paths look interchangeable and are not. Below 0.8 the old write stays in an #else: dropping it would break the module's outbound calls to that peer, which is a regression, not a fix. Guards are expanded MAJOR-aware arithmetic, unifdef-resolvable. Requires logos-protocol fix/token-direction-key-namespace (59b27ef). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
8ee0e81c57
commit
2f38edbc9d
@@ -148,6 +148,20 @@ QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi)
|
||||
"(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && "
|
||||
"LOGOS_PROTOCOL_VERSION_MINOR >= 6))\n");
|
||||
|
||||
// The protocol-0.8 guard: logos_module_accept_inbound_token, the door
|
||||
// informModuleToken routes to below. Same shape and same reasons as the
|
||||
// 0.6 guard above -- MAJOR-aware because a bare `MINOR >= 8` goes false at
|
||||
// 1.0.0 and would take this call and the backend's definition of the symbol
|
||||
// away TOGETHER (everything still builds, still loads, and every module
|
||||
// silently goes back to filing its callers as outbound credentials), and
|
||||
// written EXPANDED because unifdef resolves these conditionals in the
|
||||
// backends' ABI checks and no-ops on a function-like macro.
|
||||
const QString inboundTokenGuard =
|
||||
QStringLiteral("#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && "
|
||||
"(LOGOS_PROTOCOL_VERSION_MAJOR > 0 || "
|
||||
"(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && "
|
||||
"LOGOS_PROTOCOL_VERSION_MINOR >= 8))\n");
|
||||
|
||||
QString c;
|
||||
QTextStream s(&c);
|
||||
s << "// AUTO-GENERATED by logos-qt-host-generator --backend cdylib -- do not edit\n";
|
||||
@@ -369,13 +383,41 @@ QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi)
|
||||
s << "}\n\n";
|
||||
|
||||
s << "bool " << provider << "::informModuleToken(const QString& moduleName, const QString& token)\n{\n";
|
||||
s << " // Host-stack save first: ModuleProxy validates INBOUND calls against\n";
|
||||
s << " // the host's TokenManager (LogosProviderBase saves there).\n";
|
||||
s << " // TWO IMAGES, ONE DIRECTION. `moduleName` is the module that will CALL\n";
|
||||
s << " // US and `token` is what it will present, so BOTH writes below are\n";
|
||||
s << " // INBOUND. Two of them because a Qt plugin statically links its own\n";
|
||||
s << " // copy of logos-protocol: the host's TokenManager (which\n";
|
||||
s << " // ModuleProxy::authorize scans) and this image's own are different\n";
|
||||
s << " // objects, and a write to one is invisible to the other.\n";
|
||||
s << " //\n";
|
||||
s << " // Host stack first: that is the store the authorization decision is\n";
|
||||
s << " // actually taken against.\n";
|
||||
s << " const bool hostOk = LogosProviderBase::informModuleToken(moduleName, token);\n";
|
||||
s << " // Then forward across the C ABI: the cdylib's own protocol stack\n";
|
||||
s << " // (a separate static copy) authenticates the module's OUTBOUND calls.\n";
|
||||
s << inboundTokenGuard;
|
||||
s << " // Then this image, THROUGH THE INBOUND DOOR. Do not merge this with\n";
|
||||
s << " // the logos_module_accept_token() calls in onInit(): that one is the\n";
|
||||
s << " // OUTBOUND door and it is correct there, because what onInit seeds is\n";
|
||||
s << " // the module's OWN host-issued anchor -- the credential it PRESENTS.\n";
|
||||
s << " // This one is a caller's token, and sending it through the outbound\n";
|
||||
s << " // door is exactly the bug protocol 0.8 exists to close: the value\n";
|
||||
s << " // landed in this image's per-target cache, so the next time this\n";
|
||||
s << " // module called that same peer its client found a token under the\n";
|
||||
s << " // peer's name, SKIPPED requestModule, presented the peer its own\n";
|
||||
s << " // issued token, was rejected, and re-exchanged -- measured on shipped\n";
|
||||
s << " // artifacts as a rejection plus a full extra round trip on every call\n";
|
||||
s << " // of every two-way pair, forever, reported to the caller as success.\n";
|
||||
s << " const bool implOk = logos_module_accept_inbound_token(\n";
|
||||
s << " moduleName.toUtf8().constData(),\n";
|
||||
s << " token.toUtf8().constData()) == 0;\n";
|
||||
s << "#else\n";
|
||||
s << " // Below protocol 0.8 there is no inbound door in the module-impl ABI,\n";
|
||||
s << " // so the outbound one is the only place the value can go. Kept\n";
|
||||
s << " // deliberately: a module generated against an older protocol keeps\n";
|
||||
s << " // exactly the behaviour it has today rather than losing the write\n";
|
||||
s << " // altogether, which would break its OWN outbound calls to that peer.\n";
|
||||
s << " const bool implOk = logos_module_accept_token(moduleName.toUtf8().constData(),\n";
|
||||
s << " token.toUtf8().constData()) == 0;\n";
|
||||
s << "#endif\n";
|
||||
s << " return hostOk && implOk;\n";
|
||||
s << "}\n\n";
|
||||
|
||||
@@ -407,6 +449,16 @@ QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi)
|
||||
s << " // capability requestModule flow). Seeding before the context\n";
|
||||
s << " // forward means on_context_ready/onContextReady can already make\n";
|
||||
s << " // authenticated calls whenever the impl's ready-latch fires.\n";
|
||||
s << " //\n";
|
||||
s << " // THIS IS THE OUTBOUND DOOR, AND HERE THAT IS CORRECT. The value is\n";
|
||||
s << " // this module's OWN credential -- what it presents when it calls\n";
|
||||
s << " // out -- so logos_module_accept_token is the right export and the\n";
|
||||
s << " // bootstrap keys are role labels, not peer names.\n";
|
||||
s << " // informModuleToken() above carries the opposite direction (a\n";
|
||||
s << " // CALLER's token) and therefore uses a different export. The two\n";
|
||||
s << " // looked interchangeable while there was only one door, and one\n";
|
||||
s << " // value written through the wrong one silently made every capability\n";
|
||||
s << " // grant bidirectional. Do not merge them.\n";
|
||||
s << " const QString authToken = obj->property(\"authToken\").toString();\n";
|
||||
s << " if (!authToken.isEmpty()) {\n";
|
||||
s << " logos_module_accept_token(\"core\", authToken.toUtf8().constData());\n";
|
||||
|
||||
@@ -95,6 +95,14 @@ pkgs.stdenv.mkDerivation {
|
||||
void logos_module_set_context(const char*, const char*, const char*) {}
|
||||
void logos_module_set_emit_callback(logos_module_emit_cb, void*) {}
|
||||
int logos_module_accept_token(const char*, const char*) { return 0; }
|
||||
// The INBOUND door (protocol 0.8), stubbed under the SAME guard the glue
|
||||
// emits its call behind. Unguarded it would be a compile error against an
|
||||
// older logos-protocol, whose logos_module_impl.h does not declare it --
|
||||
// which is the one thing this probe exists to catch rather than cause.
|
||||
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 || \
|
||||
(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 8))
|
||||
int logos_module_accept_inbound_token(const char*, const char*) { return 0; }
|
||||
#endif
|
||||
int logos_module_grant_host_services(const char*) { return 0; }
|
||||
void logos_module_set_unload_done_callback(logos_module_unload_done_cb, void*) {}
|
||||
int logos_module_about_to_unload(void) { return 0; }
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
{ pkgs, generator }:
|
||||
|
||||
pkgs.runCommand "logos-qt-host-generator-test" {
|
||||
nativeBuildInputs = [ generator ];
|
||||
# unifdef, so the emitted protocol-version guards can be RESOLVED rather than
|
||||
# grepped as text. A grep sees both branches of every #if at every version and
|
||||
# would pass vacuously; see the token-doors block below.
|
||||
nativeBuildInputs = [ generator pkgs.unifdef ];
|
||||
} ''
|
||||
mkdir -p work && cd work
|
||||
cat > sample.lidl <<'EOF'
|
||||
@@ -57,6 +60,103 @@ pkgs.runCommand "logos-qt-host-generator-test" {
|
||||
grep -q "$sym" $c || { echo "C-ABI forwarding lost: $sym"; exit 1; }
|
||||
done
|
||||
|
||||
# ---- THE TWO TOKEN DOORS, AND WHICH DIRECTION EACH CARRIES ---------------
|
||||
#
|
||||
# The glue writes a token into the cdylib's own protocol stack from two
|
||||
# places, and they mean opposite things:
|
||||
#
|
||||
# onInit() the module's OWN host-issued anchor, under the two
|
||||
# bootstrap role labels. What this module PRESENTS.
|
||||
# -> logos_module_accept_token (OUTBOUND)
|
||||
# informModuleToken() a CALLER's token, named by capability_module. What
|
||||
# that caller presents to US.
|
||||
# -> logos_module_accept_inbound_token (INBOUND)
|
||||
#
|
||||
# They were one export until protocol 0.8, and that is why this block exists.
|
||||
# A caller's token written through the outbound door lands in this image's
|
||||
# per-target cache, so the next call to that same peer finds a token under the
|
||||
# peer's name, SKIPS requestModule, presents the peer its own issued token,
|
||||
# is rejected, and re-exchanges -- measured on shipped artifacts as a rejection
|
||||
# plus a full extra round trip on every call of every two-way pair, forever,
|
||||
# and reported to the caller as success. Nothing else in this repo would
|
||||
# notice: both spellings compile, link, load and return true.
|
||||
#
|
||||
# RESOLVED, not grepped. The emitter writes the guard as TEXT -- it is
|
||||
# evaluated when the MODULE compiles -- so a plain grep finds both branches at
|
||||
# every protocol version and would pass vacuously. unifdef turns the text into
|
||||
# the code a module built against a given protocol actually compiles.
|
||||
# (rc 0 = unchanged, 1 = changed, >= 2 = error.)
|
||||
resolve() { # <major> <minor> <out>
|
||||
set +e
|
||||
unifdef -DLOGOS_PROTOCOL_VERSION_MAJOR="$1" -DLOGOS_PROTOCOL_VERSION_MINOR="$2" \
|
||||
$c > "$3"
|
||||
rc=$?
|
||||
set -e
|
||||
[ "$rc" -le 1 ] || { echo "unifdef exited $rc on $c"; exit 1; }
|
||||
if grep -nE '^[[:space:]]*#[[:space:]]*(if|ifdef|ifndef|else|elif|endif)' "$3"; then
|
||||
echo "unifdef left the conditionals above unresolved at $1.$2"; exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
resolve 0 8 at-0.8
|
||||
resolve 0 7 at-0.7
|
||||
# One MAJOR up, where a bare `MINOR >= 8` silently goes false and takes the
|
||||
# call and the backend's definition away TOGETHER -- everything still builds
|
||||
# and loads, and every module quietly goes back to filing its callers as
|
||||
# outbound credentials.
|
||||
resolve 1 0 at-1.0
|
||||
|
||||
# The CODE of one function, comments stripped. Stripping is not tidiness: the
|
||||
# emitted bodies deliberately cross-reference the OTHER door by name ("do not
|
||||
# merge this with the logos_module_accept_token() calls in onInit"), and that
|
||||
# comment is exactly what a naive grep would score as a call.
|
||||
body() { # <resolved file> <function signature fragment> -> stdout
|
||||
awk -v sig="$2" '
|
||||
index($0, sig) { inb = 1 }
|
||||
inb { print }
|
||||
inb && /^}/ { exit }
|
||||
' "$1" | grep -vE '^[[:space:]]*//' || true
|
||||
}
|
||||
|
||||
# At 0.8 the caller path takes the INBOUND door and only that door.
|
||||
body at-0.8 "::informModuleToken(const QString& moduleName" > inform-0.8
|
||||
grep -q "logos_module_accept_inbound_token(" inform-0.8 \
|
||||
|| { echo "informModuleToken does not use the inbound door at protocol 0.8"; exit 1; }
|
||||
grep -q "logos_module_accept_token(" inform-0.8 \
|
||||
&& { echo "informModuleToken still files a CALLER's token through the OUTBOUND door"; exit 1; }
|
||||
|
||||
# ...and onInit still takes the OUTBOUND one, which is correct there: what it
|
||||
# seeds is this module's own credential, not a peer's.
|
||||
body at-0.8 "::onInit(LogosAPI* api)" > oninit-0.8
|
||||
grep -q 'logos_module_accept_token("core"' oninit-0.8 \
|
||||
|| { echo "onInit no longer seeds the module's own anchor outbound"; exit 1; }
|
||||
grep -q 'logos_module_accept_token("capability_module"' oninit-0.8 \
|
||||
|| { echo "onInit seeds only one of the two bootstrap role labels"; exit 1; }
|
||||
grep -q "logos_module_accept_inbound_token(" oninit-0.8 \
|
||||
&& { echo "onInit files the module's OWN anchor through the inbound door"; exit 1; }
|
||||
|
||||
# At 0.7 there is no inbound door in the module-impl ABI, so the fallback must
|
||||
# still write SOMETHING: dropping the write would break the module's own
|
||||
# outbound calls to that peer, which is a regression rather than a fix.
|
||||
body at-0.7 "::informModuleToken(const QString& moduleName" > inform-0.7
|
||||
grep -q "logos_module_accept_inbound_token(" inform-0.7 \
|
||||
&& { echo "an inbound-door call survives at protocol 0.7, where the symbol does not exist"; exit 1; }
|
||||
grep -q "logos_module_accept_token(" inform-0.7 \
|
||||
|| { echo "the pre-0.8 fallback write was lost"; exit 1; }
|
||||
|
||||
# THE MAJOR PROBE. This is the assertion that fails against
|
||||
# `#if LOGOS_PROTOCOL_VERSION_MINOR >= 8` and passes against the expanded,
|
||||
# MAJOR-aware form.
|
||||
body at-1.0 "::informModuleToken(const QString& moduleName" > inform-1.0
|
||||
grep -q "logos_module_accept_inbound_token(" inform-1.0 \
|
||||
|| { echo "the inbound door vanishes at protocol 1.0: the guard tests MINOR without MAJOR"; exit 1; }
|
||||
|
||||
# And the guard is spelled EXPANDED rather than behind a function-like macro,
|
||||
# because unifdef -- which every backend's ABI check runs -- handles nested
|
||||
# integer arithmetic and silently no-ops on anything it cannot evaluate.
|
||||
grep -q "LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 8" $c \
|
||||
|| { echo "the 0.8 guard is not the expanded MAJOR-aware arithmetic"; exit 1; }
|
||||
|
||||
# ---- drift guard, kept from when there were TWO copies of this emitter ----
|
||||
# logos-qt-sdk used to ship qt-generator/lidl_gen_cdylib_glue.cpp, the same
|
||||
# emitter this file tests. Both compiled, both emitted loadable glue, so
|
||||
|
||||
Reference in New Issue
Block a user