CreateNamespace's field comment and the §8 required-changes list still named the pre-implementation identifiers XnsAttrAutoRemove/XnsAttrBuiltin; the actual wire constants are XNS_ATTR_TRANSIENT/XNS_ATTR_IMMUTABLE. Also clean up the garbled §8 item 4 sentence. Flagged as an advisory (non-blocking) note in the bot review. Signed-off-by: Enrico Weigelt, metux IT consult <enrico.weigelt@vnc.biz>
17 KiB
Xnamespace protocol extension — design v0.1 (DRAFT)
This document specifies an X11 protocol extension for the Xnamespace
feature. Its purpose is to let an authorized management client
dynamically manage namespaces at runtime — the settings that today can
only be provisioned statically from the namespace.conf file at server
startup (see doc/Xnamespace.md and Xext/namespace/config.c).
The expected user is a container/session manager: it creates a namespace when a container starts and deletes it when the container finishes. Traffic is very low, so requests are kept simple and synchronous (round-tripped) rather than optimised for throughput — many X11 packets are needlessly complex and this extension deliberately avoids that.
The wire protocol mirrors the existing in-memory model (struct Xnamespace
in Xext/namespace/namespace.h) so the extension stays a thin, auditable
layer over data the server already maintains.
Status: DESIGN DRAFT. Opcodes, encodings and the capability bit layout are not yet frozen.
- Goals and non-goals
Goals:
- Enumerate namespaces and their settings.
- Create and delete namespaces at runtime.
- Query and modify per-namespace capability flags
(
mouse-motion,shape,transparency,xinput,xkeyboard,superpower). - Add, list and remove authentication tokens that map clients into a
namespace (the
authlines in the config file today). - Auto-remove a namespace when its last client exits (transient namespaces for transient containers).
Non-goals (decided, not just deferred):
- No persistence. Xservers never persist state; runtime changes live
only in memory. The config file remains the boot-time seed and is never
rewritten. There is no
SaveConfig/ReloadConfig. - No client reassignment. A client's namespace is fixed at connect
time by its auth token. There is no
AssignClientrequest. A namespace is emptied by terminating its clients, not by moving them. - No namespace hierarchy yet. A name delimiter is reserved for future nesting (section 3), but inheritance semantics are undefined and using the delimiter now is an error.
- No events yet. With a single synchronous manager there is no
multi-manager consistency problem to solve. A
NamespaceNotifyevent may be added later if a use case appears.
- Security model (the load-bearing part)
The entire point of namespaces is isolation, and a management protocol is a direct path to subverting it. So the access rule is simple and absolute:
The whole extension is reachable only by clients in a
superPowernamespace (today only the built-inroot, plus any namespace explicitly grantedsuperpower). To every other client the extension does not exist.
This is enforced exactly where the existing code already enforces the same
boundary for SECURITY, RECORD and XTEST — the two Xace extension
hooks, both of which already gate on subj->ns->superPower:
hook-ext-access.c(fires onQueryExtension): non-superPower clients getBadAccess, so the extension is invisible — it reports as not present. A sandboxed client cannot even discover it exists.hook-ext-dispatch.c(fires on every request to the extension): non-superPower clients getBadAccess, so even a client that hard-codes the major opcode cannot invoke anything.
Adding Xnamespace-mgmt to the blacklist in both hooks is the entire
access-control implementation. Consequences:
- There is no per-request privilege table. Every request handler may assume its caller is superPower. One choke point to audit, not twelve.
QueryVersionis reachable only by superPower clients too. That is fine: a client that cannot see the extension has nothing to negotiate, and "not present" is the honest answer for it.- No client can grant itself capabilities it lacks, because a non-superPower client cannot reach any request, mutating or not.
The built-in namespaces root and anon are immutable over the
protocol: their name, capabilities, attributes and token set are fixed at
boot. Any mutating request targeting them (delete, set-flags, add/remove
token) returns BadAccess. They are configured only via the config file.
- Naming and identity
Namespaces are identified by name — the same unique key the code
already uses (XnsFindByName(), select_ns()). Names are the only
namespace identifier on the wire; there are no protocol-level numeric IDs.
The server is free to keep an internal id or resource for its own
bookkeeping, but that is an implementation detail and never appears in a
request, reply or error. Given the low, synchronous traffic, string lookup
costs nothing and a single identifier is simpler to reason about.
Name rules (violations return BadName):
- Must be non-empty.
- Must be unique (duplicate on
CreateNamespace→BadName). - Must not contain the reserved nesting delimiter
'/'. The delimiter is reserved now so that a future hierarchy (parent/child) can be added without a grammar break; using it today is rejected. - Should be restricted to printable, non-whitespace characters so that any
protocol-created name remains expressible in
namespace.conf(the config parser splits on whitespace and treats#as a comment). Implementations reject other characters withBadName.
BadName (core error 15) is reused as the single name-related error:
malformed, reserved-delimiter, duplicate-on-create, or no-such-namespace.
Auth tokens, which have no natural human name, are referenced within a
namespace by a small server-assigned opaque handle (CARD32) returned at
creation. This is a per-namespace bookkeeping handle, not a global resource.
- Capabilities and attributes
Two CARD32 words describe a namespace.
Capabilities — access permissions (the allow* / superPower fields):
| Bit | Name |
|---|---|
| 1 << 0 | XNS_CAPABILITY_MOUSE_MOTION |
| 1 << 1 | XNS_CAPABILITY_SHAPE |
| 1 << 2 | XNS_CAPABILITY_TRANSPARENCY |
| 1 << 3 | XNS_CAPABILITY_INPUT |
| 1 << 4 | XNS_CAPABILITY_KEYBOARD |
| 1 << 5 | XNS_CAPABILITY_ADMIN |
Attributes — lifecycle/status (new fields on struct Xnamespace):
| Bit | Name | meaning |
|---|---|---|
| 1 << 0 | XNS_ATTR_IMMUTABLE |
read-only; set by server for root/anon |
| 1 << 1 | XNS_ATTR_TRANSIENT |
destroy namespace when last client exits |
Unused bits in either word are reserved, must be 0, and yield BadValue.
New capabilities/attributes are added by allocating the next bit and
bumping the minor version.
- Versioning
QueryVersion negotiates (major, minor). This document is major 1, minor 0.
The client sends the highest version it supports; the server replies with
the highest it supports that is not greater than the client's.
- Requests
Every request begins with the standard 4-byte extension header (major
opcode, 1-byte minor opcode, 16-bit length in 4-byte units). All requests
are 4-byte aligned. A namespace name is encoded as a single length-prefixed
STRING8 (CARD16 length + bytes + zero padding to a 4-byte boundary) — one
uniform encoding, easy to parse.
All mutating requests carry a reply so the manager round-trips and learns success or failure synchronously, instead of relying on asynchronous error delivery. Replies are deliberately minimal.
Minor opcodes:
| Opcode | Request | Reply |
|---|---|---|
| 0 | QueryVersion |
yes |
| 1 | ListNamespaces |
yes |
| 2 | CreateNamespace |
yes (ack) |
| 3 | DeleteNamespace |
yes (ack) |
| 4 | QueryNamespace |
yes |
| 5 | SetNamespaceFlags |
yes (ack) |
| 6 | AddAuthToken |
yes |
| 7 | RemoveAuthToken |
yes (ack) |
| 8 | ListAuthTokens |
yes |
| 9 | GetClientNamespace |
yes |
6.0 QueryVersion
QueryVersion
client-major-version: CARD16
client-minor-version: CARD16
=>
major-version: CARD16
minor-version: CARD16
6.1 ListNamespaces
ListNamespaces
=>
namespaces: LISTofNAMESPACEINFO
where each NAMESPACEINFO is a fixed header followed by the name:
capabilities: CARD32 (section 4)
attributes: CARD32 (section 4)
refcnt: CARD32 (clients currently assigned)
num-tokens: CARD32
name: STRING8 (length-prefixed, padded)
6.2 CreateNamespace
CreateNamespace
capabilities: CARD32 (initial caps)
attributes: CARD32 (XNS_ATTR_TRANSIENT honored; XNS_ATTR_IMMUTABLE
rejected with BadValue if set)
name: STRING8
=>
(empty ack)
Errors: BadName (empty / illegal char / reserved '/' / duplicate),
BadValue (reserved capability or attribute bit set), BadAlloc.
6.3 DeleteNamespace
DeleteNamespace
onclients: CARD8 (0 = fail if any client present,
1 = terminate all clients, then delete)
pad: 3 bytes
name: STRING8
=>
(empty ack)
- Deleting a built-in namespace →
BadAccess. onclients == 0and the namespace still has clients →BadAccess(busy). The manager must terminate them first or passonclients == 1.onclients == 1: the server disconnects every client in the namespace (as if each had closed its connection), then frees the namespace and removes its auth tokens (callingRemoveAuthorization()for each).- Any other
onclientsvalue →BadValue.
6.4 QueryNamespace
QueryNamespace
name: STRING8
=>
capabilities: CARD32
attributes: CARD32
refcnt: CARD32
num-tokens: CARD32
BadName if no such namespace.
6.5 SetNamespaceFlags
Atomically updates the subset of capability bits selected by value-mask:
SetNamespaceFlags
value-mask: CARD32 (which capability bits to apply)
values: CARD32 (new values for the masked bits)
name: STRING8
=>
capabilities: CARD32 (resulting capabilities)
new_caps = (old_caps & ~value-mask) | (values & value-mask). Targeting a
built-in namespace → BadAccess. A reserved bit in value-mask →
BadValue. BadName if no such namespace. (Attributes are set only at
creation in v0.1; there is no runtime attribute change.)
6.6 AddAuthToken
Runtime equivalent of an auth config line; internally calls
AddAuthorization() exactly as config.c does and records the token.
AddAuthToken
name: STRING8 (namespace name)
auth-proto: STRING8 (e.g. "MIT-MAGIC-COOKIE-1")
auth-data: STRING8 (raw key bytes, length-prefixed — NOT hex)
=>
token-handle: CARD32 (per-namespace handle for removal)
The key is carried as raw bytes; the hex encoding in the config file
(hex2bin() in config.c) is a text-file affordance with no place in a
binary protocol. Targeting a built-in namespace → BadAccess. BadName
if no such namespace; BadValue if auth-proto is empty; BadAlloc on
failure.
(Three length-prefixed STRING8s in a fixed order — the request stays trivially parseable despite three variable-length fields.)
6.7 RemoveAuthToken
RemoveAuthToken
token-handle: CARD32
name: STRING8
=>
(empty ack)
Calls RemoveAuthorization() and unlinks the token. Removing a token does
not disconnect clients already authenticated with it (standard X
authorization semantics); it only prevents future connections from using
it. BadMatch if the handle does not belong to the named namespace;
BadAccess for a built-in namespace; BadName if no such namespace.
6.8 ListAuthTokens
ListAuthTokens
name: STRING8
=>
tokens: LISTofAUTHTOKENINFO
where AUTHTOKENINFO is:
token-handle: CARD32
proto: STRING8 (length-prefixed, padded)
Secret key material is never returned — only the protocol name and the handle. A compromised manager cannot exfiltrate cookies it did not create.
6.9 GetClientNamespace
Read-only introspection: which namespace a given client belongs to. Useful
for auditing a namespace's members before a DeleteNamespace onclients=1.
GetClientNamespace
client-resource: CARD32 (a resource base; 0 = the calling client)
=>
is-server: BOOL
pad: 3 bytes
name: STRING8 (the client's namespace name)
This is the protocol view of XnsClientPriv(client)->ns.
- Errors
The extension defines no new error codes; it reuses core errors:
BadAccess— operation on an immutable built-in namespace; delete of a busy namespace withoutonclients=1. (Also the result of a non-superPower client reaching the extension, though the Xace hooks make that unreachable in practice — defence in depth.)BadName— namespace name empty, illegal, reserved-delimiter, duplicate on create, or not found.BadValue— reserved capability/attribute bit set; badonclientsvalue; empty auth protocol.BadMatch— token handle does not belong to the named namespace.BadAlloc— out of memory.
- Mapping to the existing implementation
Registered like any other extension (AddExtension, cf.
Xext/dpms/dpms.c:515) from NamespaceExtensionInit() in
Xext/namespace/namespace.c, after XnsLoadConfig() succeeds — no point
exposing management when namespaces are disabled.
| Protocol concept | Existing code |
|---|---|
| namespace record | struct Xnamespace (namespace.h) |
| capability bits | allow* / superPower bool fields |
| find by name | XnsFindByName() |
| create namespace | select_ns() logic, factored out of config.c |
| add token | AddAuthorization() + xorg_list_append |
| remove token | RemoveAuthorization() + xorg_list_del |
| client → namespace | XnsClientPriv()->ns |
| refcount | struct Xnamespace.refcnt |
Required changes:
- Access gate. Add
Xnamespace-mgmtto the blacklist in bothhook-ext-access.c(invisible to non-superPower) andhook-ext-dispatch.c(BadAccesson dispatch), alongsideSECURITY/RECORD/XTEST. - Shared setters. Factor the per-field parsing in
parseLine()into helpers (XnsCreateNamespace,XnsSetCaps,XnsAddToken,XnsRemoveToken) so the config loader and the protocol handlers share one code path and cannot drift apart. - Removable tokens. Add an opaque per-namespace
token-handletostruct auth_tokenand support unlinking +RemoveAuthorization()(today tokens are only ever appended at boot). - New attribute fields.
Bool builtinalready exists; addBool autoRemove. Setbuiltinforroot/anon. - Auto-remove. Centralise in
XnamespaceAssignClient(): after thepriv->ns->refcnt--path, if the namespace is non-builtin, hasautoRemoveset, and refcnt reached 0, destroy it (free tokens viaRemoveAuthorization(), unlink fromns_list, free). This is reached naturally fromhookClientDestroy(), which already callsXnamespaceAssignClient(subj, NULL).root/anonare safe because they are builtin and start atrefcnt = 1. - Client termination on delete. For
DeleteNamespace onclients=1, walk clients whoseXnsClientPriv()->nsmatches and close them (CloseDownClient), then free the namespace. - Dispatch. A
ProcXnsDispatch/SProcXnsDispatchpair with the usual byte-swapped variants for each request.
Serialization: everything runs in the single-threaded dispatch loop, so no locking is needed. Refcount and list mutations are safe as they happen only from request handlers and the existing callbacks.
- Remaining minor questions
- Reserved name charset.
'/'is reserved for nesting. Confirm the full allowed charset (proposal: printable ASCII excluding whitespace,#, and/) so protocol-created names round-trip through the config grammar. - Auto-remove vs. explicit delete race. If a transient namespace
auto-removes just before a manager issues
DeleteNamespace, the delete sees no such name and returnsBadName. Is that acceptable (treat as "already gone, success") or should auto-removed names linger briefly? Proposal:BadNameis fine; the namespace is gone either way.