mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-30 21:41:10 +00:00
fix(tokens): separate INBOUND from OUTBOUND, without moving a single byte
A grant one way was a grant both ways. TokenManager was ONE flat map with no
direction tag, written from both sides of every relationship: LogosAPIClient
stored the token it will PRESENT to a callee under the CALLEE's name, and a
token RECEIVED from a caller was stored under the CALLER's name. Same key
namespace, last write wins.
Measured on the shipped fleet with two ordinary modules doing nothing unusual:
one grant A -> B leaves the SAME token value under both opposite-meaning keys,
and the never-granted B -> A call then succeeds. Silently.
A.callOther(B, ping) CALL_OK
T1 A holds token for B? val=7685c776-...
T1 B holds token for A? val=7685c776-... <-- one value, two meanings
B.callOther(A, ping) CALL_OK <-- never granted
WHY THE LAYOUT COULD NOT CHANGE. TokenManager's layout is a cross-package ABI:
the host ALLOCATES the object and module/UI-plugin images MUTATE it through
their own statically-linked accessors — and host and modules ship as separate
.lgx that mix versions at runtime by design. The header's ABI-safety note is
about ALLOCATION ("no consumer allocates one, none needs sizeof"); the hazard
is MUTATION.
Splitting into three members took sizeof 32 -> 64 and moved m_mutex 24 -> 56.
QMutex::fastTryLock() compare-exchanges at this+24, which in that layout is
m_inbound's QHash d-pointer. Empty, the old code silently borrows the hash's
pointer slot as a mutex and puts it back, so it LOOKS fine; non-empty, the
exchange fails and lockInternal() interprets the QHash Data* as a
QMutexPrivate* and futex-waits on it — hung forever, inside a token-store
write, on the module host's Qt main thread. No crash, no log line, no timeout
that recovers. Reproduced by calling the shipped 0.6 plugin's own saveToken on
a 0.7 object: exit=124.
So direction lives in the KEY NAMESPACE instead. Outbound is the bare peer name
(byte-identical to master); inbound is "\x01in\x01" + caller. m_tokens@16,
m_mutex@24, sizeof 32 — measured identical to master in every shipped image,
pinned by a static_assert against a reference struct that fires if a member is
added.
Two things a key namespace forces that separate members did not: every door
REFUSES a key carrying the namespace character, or a wire-supplied caller name
could forge across the direction boundary; and credential() is DERIVED from
bootstrapKeys() rather than cached, because a cached field reads empty on a
store another image wrote and then refuses every push.
AN ANCHOR KEY IS NO LONGER SPELLED AS A MODULE NAME. scanIssuedTokens' m_tokens
loop offered every matched key unconditionally while the m_store loop
deliberately never offers, so "an anchor must never name a caller" was enforced
on one side only. A module announcing itself as "core" — which logos-rust-sdk
did unprompted — therefore authorized as kind:module name:core. The rule
generalises: a store may only name a caller with a key it alone can write.
Implemented as a masked operand, so the comparison count is unchanged;
RefusingToNameAnAnchorKeyCostsNoComparison pins that via
logos::tokenComparisonCount().
lp_token_save / lp_token_save_for now return LP_ERR_INVALID_ARG on a reserved
key instead of LP_OK. Only the return code was wrong; saveToken already refused.
PROTOCOL 0.8: logos_module_accept_inbound_token joins the module-impl C ABI
(12 exports). onInit keeps logos_module_accept_token for the module's own
anchor — that one IS outbound, and merging the two paths is what reintroduces
the bug.
Supersedes the field-split approach; the semantics are unchanged from it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
committed by
Dario Lipicar
co-authored by
Claude Opus 5
parent
b37a2e9f1c
commit
42460e5b2a
+59
-3
@@ -585,8 +585,57 @@ char* lp_token_get(const char* module_name)
|
||||
int lp_token_save(const char* module_name, const char* token)
|
||||
{
|
||||
if (!module_name || !token) return LP_ERR_INVALID_ARG;
|
||||
TokenManager::instance().saveToken(QString::fromUtf8(module_name),
|
||||
QString::fromUtf8(token));
|
||||
const QString key = QString::fromUtf8(module_name);
|
||||
// SYMMETRIC WITH lp_token_save_inbound, deliberately. saveToken() refuses a
|
||||
// key carrying the reserved direction namespace and can only say so in the
|
||||
// log: its signature is void and is pinned by the cross-package ABI freeze
|
||||
// (see the layout note in token_manager.h), so it cannot report through a
|
||||
// return value. Without this the two doors disagreed about the SAME
|
||||
// refusal — LP_OK here, LP_ERR_INVALID_ARG there — and a module tripping
|
||||
// the guard read rc=0 and carried on believing it held a credential it does
|
||||
// not hold. Asked FIRST, so the answer cannot depend on saveToken's
|
||||
// internals staying in step.
|
||||
if (TokenManager::isReservedKey(key)) return LP_ERR_INVALID_ARG;
|
||||
TokenManager::instance().saveToken(key, QString::fromUtf8(token));
|
||||
return LP_OK;
|
||||
}
|
||||
|
||||
int lp_token_save_inbound(const char* caller, const char* token)
|
||||
{
|
||||
if (!caller || !token) return LP_ERR_INVALID_ARG;
|
||||
const QString callerName = QString::fromUtf8(caller);
|
||||
const QString value = QString::fromUtf8(token);
|
||||
|
||||
// The inbound half, always. saveInboundToken refuses an empty name, an
|
||||
// empty token, and a name carrying the reserved direction namespace --
|
||||
// `caller` is named by capability_module over RPC, so it must not be able to
|
||||
// address any key but its own.
|
||||
if (!TokenManager::instance().saveInboundToken(callerName, value))
|
||||
return LP_ERR_INVALID_ARG;
|
||||
|
||||
// THE TOKEN-REGISTRY CARVE-OUT. See the declaration for the argument; the
|
||||
// short version is that informModuleToken means opposite things depending on
|
||||
// WHO RECEIVES IT. To an ordinary provider it is "this caller may call you"
|
||||
// and stops at the line above. To the module holding the registry it is
|
||||
// "here is module X's token, present it when you call X", and that is
|
||||
// outbound: capability_module reads lp_token_keys() for its known-caller
|
||||
// gate and lp_token_get() for the credential it presents when pushing to the
|
||||
// target. Both read the OUTBOUND half.
|
||||
//
|
||||
// The grant is the declaration of that role, so the grant decides. It is off
|
||||
// by default, fail-closed, already the gate on lp_token_keys, and it lives
|
||||
// in the image whose store is being written -- unlike a codegen flag, which
|
||||
// would be a second place for the two to disagree and which the glue
|
||||
// generator could not compute anyway (it is handed a LIDL contract, not
|
||||
// metadata.json).
|
||||
//
|
||||
// MEASURED CONSEQUENCE OF OMITTING THIS, so nobody "simplifies" it away:
|
||||
// capability_module's roster empties, every requestModule is refused with
|
||||
// "rejecting request from unknown module identity", and the fleet locks out
|
||||
// at the first cross-module call. Fail-closed, and total.
|
||||
if (hostServiceGranted(ServiceTokenRegistry))
|
||||
TokenManager::instance().saveToken(callerName, value);
|
||||
|
||||
return LP_OK;
|
||||
}
|
||||
|
||||
@@ -631,8 +680,15 @@ int lp_token_save_for(const char* identity, const char* module_name,
|
||||
const char* token)
|
||||
{
|
||||
if (!identity || !module_name || !token) return LP_ERR_INVALID_ARG;
|
||||
const QString key = QString::fromUtf8(module_name);
|
||||
// The same door with a store selector in front of it, so it owes the same
|
||||
// answer — see lp_token_save. Checked BEFORE forIdentity(), which is not
|
||||
// tidiness: forIdentity() records that a shared store was vended under
|
||||
// `identity`, and that record makes a later isolateIdentity() refuse. A
|
||||
// rejected argument must not be able to cost an identity its isolation.
|
||||
if (TokenManager::isReservedKey(key)) return LP_ERR_INVALID_ARG;
|
||||
TokenManager::forIdentity(QString::fromUtf8(identity))
|
||||
.saveToken(QString::fromUtf8(module_name), QString::fromUtf8(token));
|
||||
.saveToken(key, QString::fromUtf8(token));
|
||||
return LP_OK;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user