Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
391 changes: 372 additions & 19 deletions Xext/namespace/config.c

Large diffs are not rendered by default.

116 changes: 110 additions & 6 deletions Xext/namespace/hook-clientstate.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,108 @@

#include <dix-config.h>

#include <ctype.h>
#include <stdio.h>
#include <string.h>

#include "dix/registry_priv.h"
#include "os/client_priv.h"
#include "os/auth.h"

#include "namespace.h"
#include "namespaceproto.h"
#include "hooks.h"

/*
* Inspect /proc/<pid>/cgroup for a configured sandbox cgroup prefix:
* 0::<namespaceCgroupPrefix>[<uid>/]<boxname>[/...]
* If matched, look up or auto-create a transient Xnamespace "<boxname>" using
* the capabilities configured in the "cgroup <prefix>" block (ns_cgroup_tmpl).
*
* Non-local connections (pid <= 0, e.g. TCP) or unreadable /proc/<pid>/cgroup
* always fail closed to ns_anon. Verified local processes outside the sandbox
* cgroup prefix use ns_root if "default root" was configured, or ns_anon.
*/
static struct Xnamespace *resolveClientNamespace(ClientPtr client)
{
if (!namespaceDefaultRoot && !namespaceCgroupPrefix)
return &ns_anon;

pid_t pid = GetClientPid(client);
if (pid <= 0)
return &ns_anon;

char path[64];
char buf[1024];
const char *box = NULL;
size_t boxlen = 0;

snprintf(path, sizeof(path), "/proc/%d/cgroup", (int)pid);
FILE *fp = fopen(path, "r");
if (!fp)
return &ns_anon;

size_t pfxlen = namespaceCgroupPrefix ? strlen(namespaceCgroupPrefix) : 0;

if (pfxlen > 0) {
while (fgets(buf, sizeof(buf), fp)) {
const char *colon = strrchr(buf, ':');
if (!colon)
continue;
const char *cg = colon + 1;
if (strncmp(cg, namespaceCgroupPrefix, pfxlen) != 0)
continue;
const char *p = cg + pfxlen;

/* if next segment is numeric <uid>/, skip it */
const char *slash = strchr(p, '/');
if (slash && slash > p) {
Bool all_digits = TRUE;
for (const char *d = p; d < slash; d++) {
if (!isdigit((unsigned char)*d)) {
all_digits = FALSE;
break;
}
}
if (all_digits)
p = slash + 1;
}

if (*p && *p != '\n' && *p != '\r') {
const char *end = p;
while (*end && *end != '/' && *end != '\n' && *end != '\r')
end++;
if (end > p) {
box = p;
boxlen = (size_t)(end - p);
break;
}
}
}
}
fclose(fp);

if (!box || boxlen == 0)
return namespaceDefaultRoot ? &ns_root : &ns_anon;

struct Xnamespace *ns = XnsLookup(box, boxlen);
if (ns) {
if (ns->builtin || !ns->autoRemove)
return &ns_anon;
return ns;
}

int err = Success;
CARD32 default_caps = XnsCaps(&ns_cgroup_tmpl);
ns = XnsCreate(box, boxlen, default_caps, XNS_ATTR_TRANSIENT, &err);
if (ns) {
XNS_LOG("auto-created transient namespace \"%s\" for pid %d\n",
ns->name, (int)pid);
return ns;
}
return &ns_anon;
}

void hookClientState(CallbackListPtr *pcbl, void *unused, void *calldata)
{
XNS_HOOK_HEAD(NewClientInfoRec);
Expand All @@ -21,18 +116,27 @@ void hookClientState(CallbackListPtr *pcbl, void *unused, void *calldata)
XnamespaceAssignClient(subj, &ns_anon);
break;

case ClientStateRunning:
case ClientStateRunning: {
subj->authId = AuthorizationIDOfClient(client);

struct Xnamespace *target = NULL;
short unsigned int name_len = 0, data_len = 0;
const char * name = NULL;
char * data = NULL;
const char *name = NULL;
char *data = NULL;
if (AuthorizationFromID(subj->authId, &name_len, &name, &data_len, &data)) {
XnamespaceAssignClient(subj, XnsFindByAuth(name_len, name, data_len, data));
} else {
XNS_HOOK_LOG("no auth data - assuming anon\n");
struct Xnamespace *byAuth = XnsFindByAuth(name_len, name, data_len, data);
if (byAuth && byAuth != &ns_anon)
target = byAuth;
}

if (!target)
target = resolveClientNamespace(client);

XnamespaceAssignClient(subj, target);
XNS_HOOK_LOG("assigned client #%d (pid %d) -> namespace \"%s\"\n",
client->index, (int)GetClientPid(client), target->name);
break;
}

case ClientStateRetained:
break;
Expand Down
121 changes: 91 additions & 30 deletions Xext/namespace/hook-device.c
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
#include "dix/dix_priv.h"
#include "dix/extension_priv.h"
#include "dix/registry_priv.h"
#include "include/inputstr.h"

#include "namespace.h"
#include "hooks.h"
Expand All @@ -20,42 +21,102 @@ void hookDevice(CallbackListPtr *pcbl, void *unused, void *calldata)
{
XNS_HOOK_HEAD(DeviceAccessCallbackParam);

if (param->access_mode & DixSetFocusAccess)
XnsRevokeAllPasteTickets(subj->ns);

if (subj->ns->superPower)
goto pass;

// should be safe to pass for anybody
switch (client->majorOp) {
case X_QueryPointer:
case X_GetInputFocus:
case X_GetKeyboardMapping:
case X_GetModifierMapping:
case X_GrabButton: // needed by xterm -- should be safe
/* Reading live physical device state (DixReadAccess: QueryKeymap,
KeymapNotify, QueryDeviceState, XIQueryDevice button/axis state,
XkbGetState, XkbSendStateNotify, QueryPointer/XIQueryPointer):
- Global keymap polling (QueryKeymap, QueryDeviceState) is always
denied so the caller receives a zeroed bitmap without a fatal XError.
- Pointer coordinates in QueryPointer/XIQueryPointer require allowMouseMotion.
- Live keyboard/modifier/button state requires namespace focus. */
if (param->access_mode & DixReadAccess) {
if (client->majorOp == X_QueryKeymap ||
(client->majorOp == EXTENSION_MAJOR_XINPUT &&
client->minorOp == X_QueryDeviceState)) {
goto block;
}
if ((client->majorOp == X_QueryPointer ||
(client->majorOp == EXTENSION_MAJOR_XINPUT &&
client->minorOp == X_XIQueryPointer)) &&
param->dev && IsPointerDevice(param->dev)) {
if (!subj->ns->allowMouseMotion)
goto block;
} else if (!XnsNamespaceHasFocus(subj->ns)) {
goto block;
}
}

/* Prevent unfocused background sandboxes from stealing keyboard focus,
warping the pointer, or actively grabbing input devices.
Passive grabs (GrabButton, GrabKey, XIPassiveGrabDevice) on the client's
own windows are already gated by dixLookupWindow(..., DixSetAttrAccess)
and must succeed during window creation before focus is assigned. */
Bool isPassiveGrab =
(client->majorOp == X_GrabButton ||
client->majorOp == X_GrabKey ||
(client->majorOp == EXTENSION_MAJOR_XINPUT &&
(client->minorOp == X_GrabDeviceButton ||
client->minorOp == X_GrabDeviceKey ||
client->minorOp == X_XIPassiveGrabDevice)));

if (!isPassiveGrab &&
((param->access_mode & (DixSetFocusAccess | DixGrabAccess | DixWriteAccess)) ||
client->majorOp == X_SetInputFocus ||
client->majorOp == X_GrabPointer ||
client->majorOp == X_GrabKeyboard ||
client->majorOp == X_WarpPointer)) {
if (!XnsNamespaceHasFocus(subj->ns)) {
XNS_HOOK_LOG("BLOCKED background focus/grab/warp by %s\n",
subj->ns->name);
goto block;
}
}

if (client->majorOp == X_WarpPointer) {
if (subj->ns->allowMouseMotion && XnsNamespaceHasFocus(subj->ns))
goto pass;
case EXTENSION_MAJOR_XKEYBOARD:
switch(client->minorOp) {
case X_kbSelectEvents: // needed by xterm
case X_kbGetMap: // needed by xterm
case X_kbBell: // needed by GIMP
case X_kbPerClientFlags: // needed by firefox
case X_kbGetState: // needed by firefox
case X_kbGetNames: // needed by firefox
case X_kbGetControls: // needed by firefox
goto pass;
default:
XNS_HOOK_LOG("BLOCKED unhandled XKEYBOARD %s\n", LookupRequestName(client->majorOp, client->minorOp));
goto block;
}
case EXTENSION_MAJOR_XINPUT:
switch (client->minorOp) {
case X_ListInputDevices:
case X_XIQueryDevice:
goto pass;
default:
XNS_HOOK_LOG("BLOCKED unhandled Xinput request\n");
goto block;
}
goto block;
}

if (client->majorOp == EXTENSION_MAJOR_XKEYBOARD) {
if (subj->ns->allowXKeyboard || client->minorOp == X_kbPerClientFlags)
goto pass;
if ((param->access_mode & ~(DixReadAccess | DixGetAttrAccess |
DixUseAccess | DixBellAccess)) == 0)
goto pass;
XNS_HOOK_LOG("BLOCKED XKEYBOARD write/manage request %s\n",
LookupRequestName(client->majorOp, client->minorOp));
goto block;
}

if (client->majorOp == EXTENSION_MAJOR_XINPUT) {
if (client->minorOp == X_XIWarpPointer) {
if (subj->ns->allowMouseMotion && XnsNamespaceHasFocus(subj->ns))
goto pass;
goto block;
}
if (subj->ns->allowXInput) {
if ((param->access_mode & (DixManageAccess | DixCreateAccess | DixDestroyAccess)) == 0)
goto pass;
}
if ((param->access_mode & ~(DixReadAccess | DixGetAttrAccess)) == 0)
goto pass;
XNS_HOOK_LOG("BLOCKED XInput request %s\n",
LookupRequestName(client->majorOp, client->minorOp));
goto block;
}

/* Core device queries, focus, bell, grabs, and event thawing */
if ((param->access_mode & ~(DixReadAccess | DixGetAttrAccess | DixGetFocusAccess |
DixSetFocusAccess | DixUseAccess | DixBellAccess |
DixGrabAccess | DixFreezeAccess | DixForceAccess)) == 0)
goto pass;

block:
param->status = BadAccess;
return;
Expand Down
34 changes: 16 additions & 18 deletions Xext/namespace/hook-ext-access.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ void hookExtAccess(CallbackListPtr *pcbl, void *unused, void *calldata)
{
XNS_HOOK_HEAD(ExtensionAccessCallbackParam);

/* root NS has super powers */
/* only the builtin root namespace may access the X-NAMESPACE management
extension; granting ADMIN (superPower) to a sandbox disables its X11
isolation without letting it manage namespaces */
if (streq(param->ext->name, XNS_EXTENSION_NAME)) {
if (subj->ns == &ns_root)
goto pass;
goto reject;
}

/* root NS or ADMIN bypass has super powers */
if (subj->ns->superPower)
goto pass;

/* the namespace management extension is invisible to non-superPower
clients - they must not even learn it exists */
if (streq(param->ext->name, XNS_EXTENSION_NAME))
goto reject;

switch (param->ext->index + EXTENSION_BASE) {
/* unrestricted access */
case EXTENSION_MAJOR_BIG_REQUESTS:
Expand All @@ -42,28 +46,22 @@ void hookExtAccess(CallbackListPtr *pcbl, void *unused, void *calldata)
goto pass;

/* really blacklisted */
case EXTENSION_MAJOR_COMPOSITE:
case EXTENSION_MAJOR_MIT_SCREEN_SAVER:
case EXTENSION_MAJOR_RECORD:
case EXTENSION_MAJOR_SECURITY:
case EXTENSION_MAJOR_XTEST:
case EXTENSION_MAJOR_XVIDEO:
goto reject;

/* only allowed if namespace has flag set */
/* allow QueryExtension for SHAPE and XINPUT so client libraries do not
permanently cache extension absence; actual requests are gated live
in hookExtDispatch */
case EXTENSION_MAJOR_SHAPE:
if (subj->ns->allowShape)
goto pass;
goto reject;

/* only allowed if namespace has flag set */
case EXTENSION_MAJOR_XINPUT:
if (subj->ns->allowXInput)
goto pass;
goto reject;
goto pass;
}

XNS_HOOK_LOG("unhandled extension query: %s (%d)\n", param->ext->name, param->ext->index);
return;
goto pass;

reject:
param->status = BadAccess;
Expand Down
Loading