| /* |
| * BRLTTY - A background process providing access to the console screen (when in |
| * text mode) for a blind person using a refreshable braille display. |
| * |
| * Copyright (C) 1995-2023 by The BRLTTY Developers. |
| * |
| * BRLTTY comes with ABSOLUTELY NO WARRANTY. |
| * |
| * This is free software, placed under the terms of the |
| * GNU Lesser General Public License, as published by the Free Software |
| * Foundation; either version 2.1 of the License, or (at your option) any |
| * later version. Please see the file LICENSE-LGPL for details. |
| * |
| * Web Page: http://brltty.app/ |
| * |
| * This software is maintained by Dave Mielke <dave@mielke.cc>. |
| */ |
| |
| #include "prologue.h" |
| |
| #include <string.h> |
| #include <errno.h> |
| #include <sys/stat.h> |
| |
| #include "log.h" |
| #include "strfmt.h" |
| #include "pgmprivs.h" |
| #include "system_linux.h" |
| #include "file.h" |
| #include "parse.h" |
| |
| #define SCF_LOG_LEVEL LOG_DEBUG |
| #define SCF_LOG_PROGRAM 0 |
| |
| //#undef HAVE_PWD_H |
| //#undef HAVE_GRP_H |
| //#undef HAVE_SYS_PRCTL_H |
| //#undef HAVE_SYS_CAPABILITY_H |
| //#undef HAVE_LIBCAP |
| //#undef HAVE_SCHED_H |
| //#undef HAVE_LINUX_AUDIT_H |
| //#undef HAVE_LINUX_FILTER_H |
| //#undef HAVE_LINUX_SECCOMP_H |
| |
| #ifdef HAVE_SYS_PRCTL_H |
| #include <sys/prctl.h> |
| #endif /* HAVE_SYS_PRCTL_H */ |
| |
| #ifdef HAVE_PWD_H |
| #include <pwd.h> |
| #endif /* HAVE_PWD_H */ |
| |
| #ifdef HAVE_GRP_H |
| #include <grp.h> |
| #endif /* HAVE_GRP_H */ |
| |
| #ifdef HAVE_LIBCAP |
| #ifdef HAVE_SYS_CAPABILITY_H |
| #include <sys/capability.h> |
| |
| static |
| STR_BEGIN_FORMATTER(formatCapabilityName, cap_value_t capability) |
| { |
| char *name = cap_to_name(capability); |
| |
| if (name) { |
| STR_PRINTF("%s", name); |
| cap_free(name); |
| } |
| } |
| |
| if (!STR_LENGTH) STR_PRINTF("CAP#%d", capability); |
| STR_END_FORMATTER |
| |
| #define MAKE_CAPABILITY_NAME(buffer,capability) \ |
| char buffer[0X20]; \ |
| STR_BEGIN(buffer, sizeof(buffer)); \ |
| STR_FORMAT(formatCapabilityName, capability); \ |
| STR_END; |
| |
| static int |
| hasCapability (cap_t caps, cap_flag_t set, cap_value_t capability) { |
| cap_flag_value_t value; |
| if (cap_get_flag(caps, capability, set, &value) != -1) return value == CAP_SET; |
| logSystemError("cap_get_flag"); |
| return 0; |
| } |
| |
| static int |
| setCapabilities (cap_t caps) { |
| if (cap_set_proc(caps) != -1) return 1; |
| logSystemError("cap_set_proc"); |
| return 0; |
| } |
| |
| static int |
| addCapability (cap_t caps, cap_flag_t set, cap_value_t capability) { |
| if (cap_set_flag(caps, set, 1, &capability, CAP_SET) != -1) return 1; |
| logSystemError("cap_set_flag"); |
| return 0; |
| } |
| |
| static int |
| requestCapability (cap_t caps, cap_value_t capability, int inheritable) { |
| if (!hasCapability(caps, CAP_EFFECTIVE, capability)) { |
| if (!hasCapability(caps, CAP_PERMITTED, capability)) { |
| MAKE_CAPABILITY_NAME(nameBuffer, capability); |
| logMessage(LOG_DEBUG, "capability not permitted: %s", nameBuffer); |
| return 0; |
| } |
| |
| if (!addCapability(caps, CAP_EFFECTIVE, capability)) return 0; |
| if (!inheritable) return setCapabilities(caps); |
| } else if (!inheritable) { |
| return 1; |
| } |
| |
| if (!hasCapability(caps, CAP_INHERITABLE, capability)) { |
| if (!addCapability(caps, CAP_INHERITABLE, capability)) { |
| return 0; |
| } |
| } |
| |
| if (setCapabilities(caps)) { |
| #ifdef PR_CAP_AMBIENT_RAISE |
| if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, capability, 0, 0) != -1) return 1; |
| logSystemError("prctl[PR_CAP_AMBIENT_RAISE]"); |
| #else /* PR_CAP_AMBIENT_RAISE */ |
| logMessage(LOG_WARNING, "can't raise ambient capabilities"); |
| #endif /* PR_CAP_AMBIENT_RAISE */ |
| } |
| |
| return 0; |
| } |
| |
| static int |
| needCapability (cap_value_t capability, int inheritable, const char *reason) { |
| int haveCapability = 0; |
| const char *outcome = NULL; |
| cap_t caps; |
| |
| if ((caps = cap_get_proc())) { |
| if (hasCapability(caps, CAP_EFFECTIVE, capability)) { |
| haveCapability = 1; |
| outcome = "already added"; |
| } else if (requestCapability(caps, capability, inheritable)) { |
| haveCapability = 1; |
| outcome = "added"; |
| } else { |
| outcome = "not granted"; |
| } |
| |
| cap_free(caps); |
| } else { |
| logSystemError("cap_get_proc"); |
| } |
| |
| if (outcome) { |
| MAKE_CAPABILITY_NAME(nameBuffer, capability); |
| |
| logMessage(LOG_DEBUG, |
| "temporary capability %s: %s (%s)", |
| outcome, nameBuffer, reason |
| ); |
| } |
| |
| return haveCapability; |
| } |
| #endif /* HAVE_SYS_CAPABILITY_H */ |
| #endif /* HAVE_LIBCAP */ |
| |
| #if defined(HAVE_GRP_H) || defined(HAVE_PWD_H) |
| static int |
| canSetSupplementaryGroups (const char *reason) { |
| #ifdef CAP_SETGID |
| if (needCapability(CAP_SETGID, 0, reason)) { |
| return 1; |
| } |
| #endif /* CAP_SETGID */ |
| |
| return 0; |
| } |
| #endif /* defined(HAVE_GRP_H) || defined(HAVE_PWD_H) */ |
| |
| static int |
| amPrivilegedUser (void) { |
| return !geteuid(); |
| } |
| |
| typedef struct { |
| const char *reason; |
| int (*install) (void); |
| } KernelModuleEntry; |
| |
| static const KernelModuleEntry kernelModuleTable[] = { |
| { .reason = "for playing alert tunes via the built-in PC speaker", |
| .install = installSpeakerModule, |
| }, |
| |
| { .reason = "for creating virtual devices", |
| .install = installUinputModule, |
| }, |
| }; static const uint8_t kernelModuleCount = ARRAY_COUNT(kernelModuleTable); |
| |
| static void |
| installKernelModules (int stayPrivileged) { |
| const KernelModuleEntry *kme = kernelModuleTable; |
| const KernelModuleEntry *end = kme + kernelModuleCount; |
| |
| while (kme < end) { |
| kme->install(); |
| kme += 1; |
| } |
| } |
| |
| #ifdef HAVE_GRP_H |
| typedef struct { |
| const char *message; |
| const gid_t *groups; |
| size_t count; |
| } GroupsLogData; |
| |
| static size_t |
| groupsLogFormatter (char *buffer, size_t size, const void *data) { |
| const GroupsLogData *gld = data; |
| |
| size_t length; |
| STR_BEGIN(buffer, size); |
| STR_PRINTF("%s:", gld->message); |
| |
| const gid_t *gid = gld->groups; |
| const gid_t *end = gid + gld->count; |
| |
| while (gid < end) { |
| STR_PRINTF(" %d", *gid); |
| |
| const struct group *grp = getgrgid(*gid); |
| if (grp) STR_PRINTF("(%s)", grp->gr_name); |
| |
| gid += 1; |
| } |
| |
| length = STR_LENGTH; |
| STR_END; |
| return length; |
| } |
| |
| static void |
| logGroups (int level, const char *message, const gid_t *groups, size_t count) { |
| GroupsLogData gld = { |
| .message = message, |
| .groups = groups, |
| .count = count |
| }; |
| |
| logData(level, groupsLogFormatter, &gld); |
| } |
| |
| static void |
| logGroup (int level, const char *message, gid_t group) { |
| logGroups(level, message, &group, 1); |
| } |
| |
| typedef struct { |
| const char *reason; |
| const char *name; |
| const char *path; |
| unsigned char needRead:1; |
| unsigned char needWrite:1; |
| } RequiredGroupEntry; |
| |
| static const RequiredGroupEntry requiredGroupTable[] = { |
| { .reason = "for reading screen content", |
| .name = "tty", |
| .path = "/dev/vcs1", |
| }, |
| |
| { .reason = "for virtual console monitoring and control", |
| .name = "tty", |
| .path = "/dev/tty1", |
| }, |
| |
| { .reason = "for serial I/O", |
| .path = "/dev/ttyS0", |
| }, |
| |
| { .reason = "for USB I/O via USBFS", |
| .path = "/dev/bus/usb", |
| }, |
| |
| { .reason = "for playing sound via the ALSA framework", |
| .name = "audio", |
| .path = "/dev/snd/seq", |
| }, |
| |
| { .reason = "for playing sound via the Pulse Audio daemon", |
| .name = "pulse-access", |
| }, |
| |
| { .reason = "for monitoring keyboard input", |
| .name = "input", |
| .path = "/dev/input/mice", |
| }, |
| |
| { .reason = "for creating virtual devices", |
| .path = "/dev/uinput", |
| .needRead = 1, |
| .needWrite = 1, |
| }, |
| |
| { .reason = "for reading BrlAPI's authorization key file", |
| .path = BRLAPI_ETCDIR "/" BRLAPI_AUTHKEYFILE, |
| .needRead = 1, |
| }, |
| }; static const uint8_t requiredGroupCount = ARRAY_COUNT(requiredGroupTable); |
| |
| static void |
| processRequiredGroups (GroupsProcessor *processGroups, int logProblems, void *data) { |
| gid_t groups[requiredGroupCount * 2]; |
| size_t count = 0; |
| |
| { |
| const RequiredGroupEntry *rge = requiredGroupTable; |
| const RequiredGroupEntry *end = rge + requiredGroupCount; |
| |
| while (rge < end) { |
| { |
| const char *name = rge->name; |
| |
| if (name) { |
| const struct group *grp; |
| |
| if ((grp = getgrnam(name))) { |
| groups[count++] = grp->gr_gid; |
| } else if (logProblems) { |
| logMessage(LOG_DEBUG, "unknown group: %s", name); |
| } |
| } |
| } |
| |
| { |
| const char *path = rge->path; |
| |
| if (path) { |
| struct stat status; |
| |
| if (stat(path, &status) != -1) { |
| groups[count++] = status.st_gid; |
| |
| if (logProblems) { |
| if (rge->needRead && !(status.st_mode & S_IRGRP)) { |
| logMessage(LOG_DEBUG, "path not group readable: %s", path); |
| } |
| |
| if (rge->needWrite && !(status.st_mode & S_IWGRP)) { |
| logMessage(LOG_DEBUG, "path not group writable: %s", path); |
| } |
| } |
| } else if (logProblems) { |
| logMessage(LOG_DEBUG, "path access error: %s: %s", path, strerror(errno)); |
| } |
| } |
| } |
| |
| rge += 1; |
| } |
| } |
| |
| removeDuplicateGroups(groups, &count); |
| processGroups(groups, count, data); |
| } |
| |
| typedef struct { |
| const gid_t *groups; |
| size_t count; |
| } CurrentGroupsData; |
| |
| static void |
| setSupplementaryGroups (const gid_t *groups, size_t count, void *data) { |
| if (haveSupplementaryGroups(groups, count)) return; |
| const CurrentGroupsData *cgd = data; |
| |
| size_t total = count; |
| if (cgd) total += cgd->count; |
| gid_t buffer[total]; |
| |
| if (cgd && (cgd->count > 0)) { |
| gid_t *gid = buffer; |
| gid = mempcpy(gid, groups, ARRAY_SIZE(groups, count)); |
| |
| if (cgd) { |
| gid = mempcpy(gid, cgd->groups, ARRAY_SIZE(cgd->groups, cgd->count)); |
| } |
| |
| count = gid - buffer; |
| removeDuplicateGroups(buffer, &count); |
| groups = buffer; |
| } |
| |
| if (canSetSupplementaryGroups("for joining the required groups")) { |
| logGroups(LOG_DEBUG, "setting supplementary groups", groups, count); |
| |
| if (setgroups(count, groups) == -1) { |
| logSystemError("setgroups"); |
| } |
| } else { |
| logMessage(LOG_WARNING, "can't set supplementary groups"); |
| } |
| } |
| |
| static void |
| joinRequiredGroups (int stayPrivileged) { |
| const int logProblems = 1; |
| |
| #ifdef HAVE_PWD_H |
| if (stayPrivileged || !amPrivilegedUser()) { |
| uid_t uid = geteuid(); |
| const struct passwd *pwd = getpwuid(uid); |
| |
| if (pwd) { |
| const char *user = pwd->pw_name; |
| gid_t group = pwd->pw_gid; |
| |
| int count = 0; |
| getgrouplist(user, group, NULL, &count); |
| |
| count += 1; // allow for the primary group |
| gid_t groups[count]; |
| |
| if (getgrouplist(user, group, groups, &count) != -1) { |
| size_t size = count; |
| removeDuplicateGroups(groups, &size); |
| |
| CurrentGroupsData cgd = { |
| .groups = groups, |
| .count = size |
| }; |
| |
| processRequiredGroups(setSupplementaryGroups, logProblems, &cgd); |
| return; |
| } else { |
| logSystemError("getgrouplist"); |
| } |
| } |
| } |
| #endif /* HAVE_PWD_H */ |
| |
| processRequiredGroups(setSupplementaryGroups, logProblems, NULL); |
| } |
| |
| static void |
| logUnjoinedGroups (const gid_t *groups, size_t count, void *data) { |
| const CurrentGroupsData *cgd = data; |
| |
| const gid_t *cur = cgd->groups; |
| const gid_t *curEnd = cur + cgd->count; |
| |
| const gid_t *req = groups; |
| const gid_t *reqEnd = req + count; |
| |
| while (req < reqEnd) { |
| int relation = (cur < curEnd)? compareGroups(*cur, *req): 1; |
| |
| if (relation > 0) { |
| logGroup(LOG_WARNING, "group not joined", *req++); |
| } else { |
| if (!relation) req += 1; |
| cur += 1; |
| } |
| } |
| } |
| |
| static void |
| logWantedGroups (const gid_t *groups, size_t count, void *data) { |
| CurrentGroupsData cgd = { |
| .groups = groups, |
| .count = count |
| }; |
| |
| processRequiredGroups(logUnjoinedGroups, 0, &cgd); |
| } |
| |
| static void |
| logMissingGroups (void) { |
| processSupplementaryGroups(logWantedGroups, NULL); |
| } |
| |
| static void |
| closeGroupsDatabase (void) { |
| endgrent(); |
| } |
| #endif /* HAVE_GRP_H */ |
| |
| #ifdef CAP_IS_SUPPORTED |
| typedef struct { |
| const char *label; |
| cap_t caps; |
| } CapabilitiesLogData; |
| |
| static size_t |
| capabilitiesLogFormatter (char *buffer, size_t size, const void *data) { |
| const CapabilitiesLogData *cld = data; |
| |
| size_t length; |
| STR_BEGIN(buffer, size); |
| STR_PRINTF("capabilities: %s:", cld->label); |
| |
| int capsAllocated = 0; |
| cap_t caps; |
| |
| if (!(caps = cld->caps)) { |
| if (!(caps = cap_get_proc())) { |
| logSystemError("cap_get_proc"); |
| goto done; |
| } |
| |
| capsAllocated = 1; |
| } |
| |
| { |
| char *text; |
| |
| if ((text = cap_to_text(caps, NULL))) { |
| STR_PRINTF(" %s", text); |
| cap_free(text); |
| } else { |
| logSystemError("cap_to_text"); |
| } |
| } |
| |
| if (capsAllocated) { |
| cap_free(caps); |
| caps = NULL; |
| } |
| |
| done: |
| length = STR_LENGTH; |
| STR_END; |
| return length; |
| } |
| |
| static void |
| logCapabilities (cap_t caps, const char *label) { |
| CapabilitiesLogData cld = { .label=label, .caps=caps }; |
| logData(LOG_DEBUG, capabilitiesLogFormatter, &cld); |
| } |
| |
| static void |
| logCurrentCapabilities (const char *label) { |
| logCapabilities(NULL, label); |
| } |
| |
| typedef struct { |
| const char *reason; |
| cap_value_t value; |
| } RequiredCapabilityEntry; |
| |
| static const RequiredCapabilityEntry requiredCapabilityTable[] = { |
| { .reason = "for injecting input characters typed on a braille device", |
| .value = CAP_SYS_ADMIN, |
| }, |
| |
| { .reason = "for playing alert tunes via the built-in PC speaker", |
| .value = CAP_SYS_TTY_CONFIG, |
| }, |
| |
| { .reason = "for creating needed but missing special device files", |
| .value = CAP_MKNOD, |
| }, |
| }; static const uint8_t requiredCapabilityCount = ARRAY_COUNT(requiredCapabilityTable); |
| |
| static void |
| setRequiredCapabilities (int stayPrivileged) { |
| cap_t newCaps, oldCaps; |
| |
| if (amPrivilegedUser()) { |
| oldCaps = NULL; |
| } else if (!(oldCaps = cap_get_proc())) { |
| logSystemError("cap_get_proc"); |
| return; |
| } |
| |
| { |
| cap_t (*function) (void); |
| const char *name; |
| |
| if (stayPrivileged) { |
| function = cap_get_proc; |
| name = "cap_get_proc"; |
| } else { |
| function = cap_init; |
| name = "cap_init"; |
| } |
| |
| if (!(newCaps = function())) logSystemError(name); |
| } |
| |
| if (newCaps) { |
| { |
| const RequiredCapabilityEntry *rce = requiredCapabilityTable; |
| const RequiredCapabilityEntry *end = rce + requiredCapabilityCount; |
| |
| while (rce < end) { |
| cap_value_t capability = rce->value; |
| |
| if (!oldCaps || hasCapability(oldCaps, CAP_PERMITTED, capability)) { |
| if (!addCapability(newCaps, CAP_PERMITTED, capability)) break; |
| if (!addCapability(newCaps, CAP_EFFECTIVE, capability)) break; |
| } |
| |
| rce += 1; |
| } |
| } |
| |
| setCapabilities(newCaps); |
| cap_free(newCaps); |
| } |
| |
| #ifdef PR_CAP_AMBIENT_CLEAR_ALL |
| if (prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0) == -1) { |
| logSystemError("prctl[PR_CAP_AMBIENT_CLEAR_ALL]"); |
| } |
| #else /* PR_CAP_AMBIENT_CLEAR_ALL */ |
| logMessage(LOG_WARNING, "can't clear ambient capabilities"); |
| #endif /* PR_CAP_AMBIENT_CLEAR_ALL */ |
| |
| if (oldCaps) cap_free(oldCaps); |
| } |
| |
| static void |
| logMissingCapabilities (void) { |
| cap_t caps; |
| |
| if ((caps = cap_get_proc())) { |
| const RequiredCapabilityEntry *rce = requiredCapabilityTable; |
| const RequiredCapabilityEntry *end = rce + requiredCapabilityCount; |
| |
| while (rce < end) { |
| cap_value_t capability = rce->value; |
| |
| if (!hasCapability(caps, CAP_EFFECTIVE, capability)) { |
| MAKE_CAPABILITY_NAME(nameBuffer, capability); |
| |
| logMessage(LOG_WARNING, |
| "required capability not granted: %s (%s)", |
| nameBuffer, rce->reason |
| ); |
| } |
| |
| rce += 1; |
| } |
| |
| cap_free(caps); |
| } else { |
| logSystemError("cap_get_proc"); |
| } |
| } |
| |
| #else /* CAP_IS_SUPPORTED */ |
| static void |
| logCurrentCapabilities (const char *label) { |
| } |
| #endif /* CAP_IS_SUPPORTED */ |
| |
| #ifdef HAVE_SCHED_H |
| #include <sched.h> |
| |
| typedef struct { |
| const char *name; |
| const char *summary; |
| int unshareFlag; |
| } IsolatedNamespaceEntry; |
| |
| static const IsolatedNamespaceEntry isolatedNamespaceTable[] = { |
| #ifdef CLONE_NEWCGROUP |
| { .unshareFlag = CLONE_NEWCGROUP, |
| .name = "cgroup", |
| .summary = "control groups", |
| }, |
| #endif /* CLONE_NEWCGROUP */ |
| |
| #ifdef CLONE_NEWNS |
| { .unshareFlag = CLONE_NEWNS, |
| .name = "mount", |
| .summary = "mount points", |
| }, |
| #endif /* CLONE_NEWNS */ |
| |
| #ifdef CLONE_NEWUTS |
| { .unshareFlag = CLONE_NEWUTS, |
| .name = "UTS", |
| .summary = "host name and NIS domain name", |
| }, |
| #endif /* CLONE_NEWUTS */ |
| }; static const uint8_t isolatedNamespaceCount = ARRAY_COUNT(isolatedNamespaceTable); |
| |
| static void |
| isolateNamespaces (void) { |
| int canIsolateNamespaces = 0; |
| |
| #ifdef CAP_SYS_ADMIN |
| if (needCapability(CAP_SYS_ADMIN, 0, "for isolating namespaces")) { |
| canIsolateNamespaces = 1; |
| } |
| #endif /* CAP_SYS_ADMIN */ |
| |
| if (canIsolateNamespaces) { |
| int unshareFlags = 0; |
| |
| const IsolatedNamespaceEntry *ine = isolatedNamespaceTable; |
| const IsolatedNamespaceEntry *end = ine + isolatedNamespaceCount; |
| |
| while (ine < end) { |
| logMessage(LOG_DEBUG, |
| "isolating namespace: %s (%s)", ine->name, ine->summary |
| ); |
| |
| unshareFlags |= ine->unshareFlag; |
| ine += 1; |
| } |
| |
| if (unshare(unshareFlags) == -1) { |
| logSystemError("unshare"); |
| } |
| } else { |
| logMessage(LOG_WARNING, "can't isolate namespaces"); |
| } |
| } |
| #endif /* HAVE_SCHED_H */ |
| |
| #ifdef HAVE_LINUX_AUDIT_H |
| #include <linux/audit.h> |
| |
| #if defined(__i386__) |
| #define SYSTEM_CALL_ARCHITECTURE AUDIT_ARCH_I386 |
| #elif defined(__x86_64__) |
| #define SYSTEM_CALL_ARCHITECTURE AUDIT_ARCH_X86_64 |
| #else /* system call architecture */ |
| #warning system call architecture not known for this platform |
| #endif /* system call architecture */ |
| #endif /* HAVE_LINUX_AUDIT_H */ |
| |
| #ifdef SYSTEM_CALL_ARCHITECTURE |
| #ifdef HAVE_LINUX_FILTER_H |
| #include <linux/filter.h> |
| |
| #ifdef HAVE_LINUX_SECCOMP_H |
| #include <linux/seccomp.h> |
| #endif /* HAVE_LINUX_SECCOMP_H */ |
| #endif /* HAVE_LINUX_FILTER_H */ |
| #endif /* SYSTEM_CALL_ARCHITECTURE */ |
| |
| #ifdef SECCOMP_MODE_FILTER |
| static const char scfLogLabel[] = "SCF"; |
| |
| typedef struct SCFArgumentDescriptorStruct SCFArgumentDescriptor; |
| |
| // best to protect each of these with an #ifdef for the value's macro |
| typedef struct { |
| // required - must be first |
| uint32_t value; |
| |
| // optional - use SCF_ARGUMENT(index, group) |
| const SCFArgumentDescriptor *argument; |
| } SCFValueDescriptor; |
| |
| typedef struct { |
| const char *name; |
| const SCFValueDescriptor *descriptors; |
| size_t count; |
| } SCFValueGroup; |
| |
| #define SCF_VALUE_GROUP(group) { \ |
| .name = #group, \ |
| .descriptors = group##Values, \ |
| .count = ARRAY_COUNT(group##Values), \ |
| } |
| |
| struct SCFArgumentDescriptorStruct { |
| SCFValueGroup values; |
| uint8_t index; |
| }; |
| |
| #define SCF_ARGUMENT(n, group) \ |
| .argument = &(const SCFArgumentDescriptor){ \ |
| .values = SCF_VALUE_GROUP(group), \ |
| .index = (n) \ |
| } |
| |
| #define SCF_BEGIN_VALUES(group) static const SCFValueDescriptor group##Values[] = { |
| #define SCF_END_VALUES }; |
| |
| #include "syscalls_linux.h" |
| static const SCFValueGroup scfSystemCalls = SCF_VALUE_GROUP(systemCall); |
| |
| typedef struct SCFJumpStruct SCFJump; |
| |
| typedef enum { |
| SCF_JUMP_ALWAYS, |
| SCF_JUMP_TRUE, |
| SCF_JUMP_FALSE |
| } SCFJumpType; |
| |
| struct SCFJumpStruct { |
| SCFJump *next; |
| size_t location; |
| SCFJumpType type; |
| }; |
| |
| typedef struct { |
| const SCFArgumentDescriptor *descriptor; |
| SCFJump jump; |
| } SCFArgument; |
| |
| #define SCF_INSTRUCTION(code, k) \ |
| (const struct sock_filter)BPF_STMT((code), (k)) |
| |
| #define SCF_RETURN_INSTRUCTION(action, value) \ |
| SCF_INSTRUCTION(BPF_RET|BPF_K, (SECCOMP_RET_##action | ((value) & SECCOMP_RET_DATA))) |
| |
| typedef struct { |
| const char *name; // must be first |
| const struct sock_filter *deny; |
| } SCFMode; |
| |
| static const SCFMode scfModes[] = { |
| // must be first |
| { .name = "no", |
| }, |
| |
| #ifdef SECCOMP_RET_LOG |
| { .name = "log", |
| .deny = &SCF_RETURN_INSTRUCTION(LOG, 0) |
| }, |
| #endif /* SECCOMP_RET_LOG */ |
| |
| #ifdef SECCOMP_RET_ERRNO |
| { .name = "fail", |
| .deny = &SCF_RETURN_INSTRUCTION(ERRNO, EPERM) |
| }, |
| #endif /* SECCOMP_RET_ERRNO */ |
| |
| #ifdef SECCOMP_RET_KILL_PROCESS |
| { .name = "kill", |
| .deny = &SCF_RETURN_INSTRUCTION(KILL_PROCESS, 0) |
| }, |
| #endif /* SECCOMP_RET_KILL_PROCESS */ |
| |
| // must be last |
| { .name = NULL } |
| }; |
| |
| static const SCFMode * |
| scfGetMode (const char *name) { |
| unsigned int choice; |
| int valid = validateChoiceEx(&choice, name, scfModes, sizeof(scfModes[0])); |
| const SCFMode *mode = &scfModes[choice]; |
| |
| if (!valid) { |
| logMessage(LOG_WARNING, |
| "unknown system call filter mode: %s: assuming %s", |
| name, mode->name |
| ); |
| } |
| |
| return mode; |
| } |
| |
| typedef struct { |
| const SCFMode *mode; |
| |
| struct { |
| struct sock_filter *array; |
| size_t size; |
| size_t count; |
| } instruction; |
| |
| struct { |
| SCFArgument *array; |
| size_t size; |
| size_t count; |
| } argument; |
| |
| struct { |
| SCFJump *jumps; |
| } allow; |
| } SCFObject; |
| |
| static int |
| scfAddInstruction (SCFObject *scf, const struct sock_filter *instruction) { |
| if (scf->instruction.count == BPF_MAXINSNS) { |
| logMessage(LOG_ERR, "system call filter too large"); |
| return 0; |
| } |
| |
| if (scf->instruction.count == scf->instruction.size) { |
| size_t newSize = scf->instruction.size? scf->instruction.size<<1: 0X10; |
| struct sock_filter *newArray; |
| |
| if (!(newArray = realloc(scf->instruction.array, ARRAY_SIZE(newArray, newSize)))) { |
| logMallocError(); |
| return 0; |
| } |
| |
| scf->instruction.array = newArray; |
| scf->instruction.size = newSize; |
| } |
| |
| scf->instruction.array[scf->instruction.count++] = *instruction; |
| return 1; |
| } |
| |
| static int |
| scfAddAllowInstruction (SCFObject *scf) { |
| static const struct sock_filter allow = SCF_RETURN_INSTRUCTION(ALLOW, 0); |
| return scfAddInstruction(scf, &allow); |
| } |
| |
| static int |
| scfAddDenyInstruction (SCFObject *scf) { |
| return scfAddInstruction(scf, scf->mode->deny); |
| } |
| |
| static int |
| scfLoadData (SCFObject *scf, uint32_t offset, uint8_t width) { |
| struct sock_filter instruction = BPF_STMT(BPF_LD|BPF_ABS, offset); |
| |
| switch (width) { |
| case 1: |
| instruction.code |= BPF_B; |
| break; |
| |
| case 2: |
| instruction.code |= BPF_H; |
| break; |
| |
| case 4: |
| instruction.code |= BPF_W; |
| break; |
| |
| default: |
| logMessage(LOG_WARNING, "unsupported field width: %u", width); |
| return 0; |
| } |
| |
| return scfAddInstruction(scf, &instruction); |
| } |
| |
| #define SCF_DATA_OFFSET(field) (offsetof(struct seccomp_data, field)) |
| |
| static int |
| scfLoadArchitecture (SCFObject *scf) { |
| return scfLoadData(scf, SCF_DATA_OFFSET(arch), 4); |
| } |
| |
| static int |
| scfLoadSystemCall (SCFObject *scf) { |
| return scfLoadData(scf, SCF_DATA_OFFSET(nr), 4); |
| } |
| |
| static int |
| scfLoadArgument (SCFObject *scf, uint8_t index) { |
| return scfLoadData(scf, SCF_DATA_OFFSET(args[index]), 4); |
| } |
| |
| static void |
| scfBeginJump (SCFObject *scf, SCFJump *jump, SCFJumpType type) { |
| memset(jump, 0, sizeof(*jump)); |
| jump->next = NULL; |
| jump->location = scf->instruction.count; |
| jump->type = type; |
| } |
| |
| static int |
| scfEndJump (SCFObject *scf, const SCFJump *jump) { |
| size_t from = jump->location; |
| struct sock_filter *instruction = &scf->instruction.array[from]; |
| |
| size_t to = scf->instruction.count - from - 1; |
| SCFJumpType type = jump->type; |
| |
| switch (type) { |
| case SCF_JUMP_ALWAYS: |
| instruction->k = to; |
| break; |
| |
| case SCF_JUMP_TRUE: |
| instruction->jt = to; |
| break; |
| |
| case SCF_JUMP_FALSE: |
| instruction->jf = to; |
| break; |
| |
| default: |
| logMessage(LOG_WARNING, "unsupported jump type: %u", type); |
| return 0; |
| } |
| |
| return 1; |
| } |
| |
| static void |
| scfPushJump (SCFJump **jumps, SCFJump *jump) { |
| jump->next = *jumps; |
| *jumps = jump; |
| } |
| |
| static SCFJump * |
| scfPopJump (SCFJump **jumps) { |
| SCFJump *jump = *jumps; |
| |
| if (jump) { |
| *jumps = jump->next; |
| jump->next = NULL; |
| } |
| |
| return jump; |
| } |
| |
| static int |
| scfEndJumps (SCFObject *scf, SCFJump **jumps) { |
| int ok = 1; |
| |
| while (1) { |
| SCFJump *jump = scfPopJump(jumps); |
| if (!jump) break; |
| |
| if (!scfEndJump(scf, jump)) ok = 0; |
| free(jump); |
| } |
| |
| return ok; |
| } |
| |
| static int |
| scfJumpTo (SCFObject *scf, SCFJump *jump) { |
| struct sock_filter instruction = BPF_STMT(BPF_JMP|BPF_K|BPF_JA, 0); |
| scfBeginJump(scf, jump, SCF_JUMP_ALWAYS); |
| return scfAddInstruction(scf, &instruction); |
| } |
| |
| typedef enum { |
| SCF_TEST_NE, |
| SCF_TEST_LT, |
| SCF_TEST_LE, |
| SCF_TEST_EQ, |
| SCF_TEST_GE, |
| SCF_TEST_GT, |
| } SCFTest; |
| |
| static int |
| scfJumpIf (SCFObject *scf, SCFTest test, uint32_t value, SCFJump *jump) { |
| struct sock_filter instruction = BPF_STMT(BPF_JMP|BPF_K, value); |
| int invert = 0; |
| |
| switch (test) { |
| case SCF_TEST_NE: |
| invert = 1; |
| case SCF_TEST_EQ: |
| instruction.code |= BPF_JEQ; |
| break; |
| |
| case SCF_TEST_LT: |
| invert = 1; |
| case SCF_TEST_GE: |
| instruction.code |= BPF_JGE; |
| break; |
| |
| case SCF_TEST_LE: |
| invert = 1; |
| case SCF_TEST_GT: |
| instruction.code |= BPF_JGT; |
| break; |
| |
| default: |
| logMessage(LOG_WARNING, "unsupported value test: %u", test); |
| return 0; |
| } |
| |
| scfBeginJump(scf, jump, (invert? SCF_JUMP_FALSE: SCF_JUMP_TRUE)); |
| return scfAddInstruction(scf, &instruction); |
| } |
| |
| static int |
| scfVerifyArchitecture (SCFObject *scf) { |
| SCFJump eqArch; |
| |
| return scfLoadArchitecture(scf) |
| && scfJumpIf(scf, SCF_TEST_EQ, SYSTEM_CALL_ARCHITECTURE, &eqArch) |
| && scfAddDenyInstruction(scf) |
| && scfEndJump(scf, &eqArch); |
| } |
| |
| static int |
| scfJumpToArgument (SCFObject *scf, const SCFArgumentDescriptor *descriptor) { |
| if (scf->argument.count == scf->argument.size) { |
| size_t newSize = scf->argument.size? scf->argument.size<<1: 0X10; |
| SCFArgument *newArray; |
| |
| if (!(newArray = realloc(scf->argument.array, ARRAY_SIZE(newArray, newSize)))) { |
| logMallocError(); |
| return 0; |
| } |
| |
| scf->argument.array = newArray; |
| scf->argument.size = newSize; |
| } |
| |
| { |
| size_t *count = &scf->argument.count; |
| SCFArgument *argument = &scf->argument.array[*count]; |
| |
| argument->descriptor = descriptor; |
| if (!scfJumpTo(scf, &argument->jump)) return 0; |
| |
| *count += 1; |
| } |
| |
| return 1; |
| } |
| |
| static int |
| scfAllowValue (SCFObject *scf, const SCFValueDescriptor *descriptor) { |
| if (descriptor->argument) { |
| SCFJump neValue; |
| |
| return scfJumpIf(scf, SCF_TEST_NE, descriptor->value, &neValue) |
| && scfJumpToArgument(scf, descriptor->argument) |
| && scfEndJump(scf, &neValue); |
| } else { |
| SCFJump *eqValue; |
| |
| if ((eqValue = malloc(sizeof(*eqValue)))) { |
| if (scfJumpIf(scf, SCF_TEST_EQ, descriptor->value, eqValue)) { |
| scfPushJump(&scf->allow.jumps, eqValue); |
| return 1; |
| } |
| |
| free(eqValue); |
| } else { |
| logMallocError(); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| scfAllowValues (SCFObject *scf, const SCFValueDescriptor *descriptors, size_t count) { |
| if (count <= 3) { |
| const SCFValueDescriptor *descriptor = descriptors; |
| const SCFValueDescriptor *end = descriptor + count; |
| |
| while (descriptor < end) { |
| if (!scfAllowValue(scf, descriptor)) return 0; |
| descriptor += 1; |
| } |
| |
| return scfAddDenyInstruction(scf); |
| } |
| |
| const SCFValueDescriptor *descriptor = descriptors + (count / 2); |
| SCFJump gtValue; |
| if (!scfJumpIf(scf, SCF_TEST_GT, descriptor->value, >Value)) return 0; |
| if (!scfAllowValue(scf, descriptor)) return 0; |
| |
| if (!scfAllowValues(scf, descriptors, (descriptor - descriptors))) return 0; |
| if (!scfEndJump(scf, >Value)) return 0; |
| |
| const SCFValueDescriptor *end = descriptors + count; |
| descriptor += 1; |
| return scfAllowValues(scf, descriptor, (end - descriptor)); |
| } |
| |
| static int |
| scfValueSorter (const void *element1, const void *element2) { |
| const SCFValueDescriptor *descriptor1 = element1; |
| const SCFValueDescriptor *descriptor2 = element2; |
| |
| if (descriptor1->value < descriptor2->value) return -1; |
| if (descriptor1->value > descriptor2->value) return 1; |
| return 0; |
| } |
| |
| static void |
| scfSortValues (SCFValueDescriptor *values, size_t count) { |
| qsort(values, count, sizeof(*values), scfValueSorter); |
| } |
| |
| static void |
| scfRemoveDuplicateValues (SCFValueDescriptor *values, size_t *count, const char *name) { |
| if (*count > 1) { |
| SCFValueDescriptor *to = values; |
| const SCFValueDescriptor *from = values + 1; |
| const SCFValueDescriptor *end = values + *count; |
| |
| while (from < end) { |
| if (from->value == to->value) { |
| logMessage(LOG_WARNING, |
| "%s: duplicate value: %s: 0X%08"PRIX32, |
| scfLogLabel, name, from->value |
| ); |
| } else if (++to != from) { |
| *to = *from; |
| } |
| |
| from += 1; |
| } |
| |
| *count = ++to - values; |
| } |
| } |
| |
| static int |
| scfAllowValueGroup (SCFObject *scf, const SCFValueGroup *values) { |
| { |
| const char *name = values->name; |
| size_t count = values->count; |
| |
| SCFValueDescriptor descriptors[count]; |
| memcpy(descriptors, values->descriptors, sizeof(descriptors)); |
| |
| scfSortValues(descriptors, count); |
| scfRemoveDuplicateValues(descriptors, &count, name); |
| |
| logMessage(SCF_LOG_LEVEL, |
| "%s: value group size: %s: %zu", scfLogLabel, name, count |
| ); |
| |
| if (!scfAllowValues(scf, descriptors, count)) return 0; |
| } |
| |
| if (scf->allow.jumps) { |
| if (!scfEndJumps(scf, &scf->allow.jumps)) return 0; |
| if (!scfAddAllowInstruction(scf)) return 0; |
| } |
| |
| return 1; |
| } |
| |
| static int |
| scfCheckSysemCall (SCFObject *scf) { |
| return scfLoadSystemCall(scf) |
| && scfAllowValueGroup(scf, &scfSystemCalls); |
| } |
| |
| static int |
| scfCheckArgument (SCFObject *scf, const SCFArgument *argument) { |
| const SCFArgumentDescriptor *descriptor = argument->descriptor; |
| |
| return scfEndJump(scf, &argument->jump) |
| && scfLoadArgument(scf, descriptor->index) |
| && scfAllowValueGroup(scf, &descriptor->values); |
| } |
| |
| static int |
| scfCheckArguments (SCFObject *scf) { |
| /* An argument's value group can include the specification of another |
| * argument and its associated value group. The following iteration, |
| * therefore, needs to handle the possibility that the pending argument |
| * count may increase as each argument is processed. |
| */ |
| |
| while (scf->argument.count > 0) { |
| if (!scfCheckArgument(scf, &scf->argument.array[--scf->argument.count])) { |
| return 0; |
| } |
| } |
| |
| return 1; |
| } |
| |
| #if SCF_LOG_PROGRAM |
| typedef struct { |
| const struct sock_filter *instruction; |
| size_t location; |
| |
| struct { |
| int decimal; |
| int hexadecimal; |
| } width; |
| } SCFInstructionFormattingData; |
| |
| static |
| STR_BEGIN_FORMATTER(scfFormatLocation, size_t location, const SCFInstructionFormattingData *ifd) |
| STR_PRINTF("X%0*zX", ifd->width.hexadecimal, location); |
| STR_END_FORMATTER |
| |
| static |
| STR_BEGIN_FORMATTER(scfDisassembleInstruction, const SCFInstructionFormattingData *ifd) |
| const struct sock_filter *instruction = ifd->instruction; |
| uint16_t code = instruction->code; |
| uint32_t operand = instruction->k; |
| |
| const char *name = NULL; |
| int hasSize = 0; |
| int hasMode = 0; |
| int hasSource = 0; |
| int isJump = 0; |
| int isReturn = 0; |
| int problem = 0; |
| |
| switch (BPF_CLASS(code)) { |
| case BPF_LD: |
| name = "ld"; |
| hasSize = 1; |
| hasMode = 1; |
| break; |
| |
| case BPF_LDX: |
| name = "ldx"; |
| hasSize = 1; |
| hasMode = 1; |
| break; |
| |
| case BPF_ST: |
| name = "st"; |
| hasSize = 1; |
| hasMode = 1; |
| break; |
| |
| case BPF_STX: |
| name = "stx"; |
| hasSize = 1; |
| hasMode = 1; |
| break; |
| |
| case BPF_ALU: |
| switch (BPF_OP(code)) { |
| case BPF_ADD: |
| name = "add"; |
| break; |
| |
| case BPF_SUB: |
| name = "sub"; |
| break; |
| |
| case BPF_MUL: |
| name = "mul"; |
| break; |
| |
| case BPF_DIV: |
| name = "div"; |
| break; |
| |
| case BPF_MOD: |
| name = "mod"; |
| break; |
| |
| case BPF_LSH: |
| name = "lsh"; |
| break; |
| |
| case BPF_RSH: |
| name = "rsh"; |
| break; |
| |
| case BPF_AND: |
| name = "and"; |
| break; |
| |
| case BPF_OR: |
| name = "or"; |
| break; |
| |
| case BPF_XOR: |
| name = "xor"; |
| break; |
| |
| case BPF_NEG: |
| name = "neg"; |
| break; |
| |
| default: |
| name = "alu"; |
| problem = 1; |
| break; |
| } |
| |
| hasSource = 1; |
| break; |
| |
| case BPF_JMP: |
| switch (BPF_OP(code)) { |
| case BPF_JEQ: |
| name = "jeq"; |
| break; |
| |
| case BPF_JGT: |
| name = "jgt"; |
| break; |
| |
| case BPF_JGE: |
| name = "jge"; |
| break; |
| |
| case BPF_JSET: |
| name = "jseT"; |
| break; |
| |
| default: |
| problem = 1; |
| /* fall through */ |
| |
| case BPF_JA: |
| name = "jmp"; |
| break; |
| } |
| |
| hasSource = 1; |
| isJump = 1; |
| break; |
| |
| case BPF_RET: |
| name = "ret"; |
| isReturn = 1; |
| break; |
| |
| default: |
| problem = 1; |
| break; |
| } |
| |
| if (name) STR_PRINTF("%s", name); |
| |
| if (hasSize) { |
| const char *size = NULL; |
| |
| switch (BPF_SIZE(code)) { |
| case BPF_B: |
| size = "b"; |
| break; |
| |
| case BPF_H: |
| size = "h"; |
| break; |
| |
| case BPF_W: |
| size = "w"; |
| break; |
| |
| default: |
| problem = 1; |
| break; |
| } |
| |
| if (size) STR_PRINTF("%s", size); |
| } |
| |
| if (hasMode) { |
| const char *mode = NULL; |
| |
| switch (BPF_MODE(code)) { |
| case BPF_IMM: |
| mode = "imm"; |
| break; |
| |
| case BPF_ABS: |
| mode = "abs"; |
| break; |
| |
| case BPF_IND: |
| mode = "ind"; |
| break; |
| |
| case BPF_MEM: |
| mode = "mem"; |
| break; |
| |
| case BPF_LEN: |
| mode = "len"; |
| break; |
| |
| default: |
| problem = 1; |
| break; |
| } |
| |
| if (mode) STR_PRINTF("-%s", mode); |
| } |
| |
| if (hasSource) { |
| const char *source = NULL; |
| |
| switch (BPF_SRC(code)) { |
| case BPF_K: |
| source = "k"; |
| break; |
| |
| case BPF_X: |
| source = "x"; |
| break; |
| |
| default: |
| problem = 1; |
| break; |
| } |
| |
| if (source) STR_PRINTF("-%s", source); |
| } |
| |
| if (isReturn) { |
| const char *action = NULL; |
| |
| switch (operand & SECCOMP_RET_ACTION_FULL) { |
| case SECCOMP_RET_KILL_PROCESS: |
| action = "kill-process"; |
| break; |
| |
| case SECCOMP_RET_KILL_THREAD: |
| action = "kill-thread"; |
| break; |
| |
| case SECCOMP_RET_TRAP: |
| action = "trap"; |
| break; |
| |
| case SECCOMP_RET_ERRNO: |
| action = "errno"; |
| break; |
| |
| case SECCOMP_RET_USER_NOTIF: |
| action = "notify"; |
| break; |
| |
| case SECCOMP_RET_TRACE: |
| action = "trace"; |
| break; |
| |
| case SECCOMP_RET_LOG: |
| action = "log"; |
| break; |
| |
| case SECCOMP_RET_ALLOW: |
| action = "allow"; |
| break; |
| |
| default: |
| break; |
| } |
| |
| if (action) { |
| STR_PRINTF("-%s", action); |
| uint16_t data = operand & SECCOMP_RET_DATA; |
| if (data) STR_PRINTF("(%u)", data); |
| } |
| } |
| |
| if (problem) { |
| STR_PRINTF("?"); |
| } else if (isJump) { |
| STR_PRINTF(" -> "); |
| size_t from = ifd->location + 1; |
| |
| if (BPF_OP(code) == BPF_JA) { |
| STR_FORMAT(scfFormatLocation, (from + operand), ifd); |
| } else { |
| STR_FORMAT(scfFormatLocation, (from + instruction->jt), ifd); |
| STR_PRINTF(" "); |
| STR_FORMAT(scfFormatLocation, (from + instruction->jf), ifd); |
| } |
| } |
| STR_END_FORMATTER |
| |
| static |
| STR_BEGIN_FORMATTER(scfFormatInstruction, const SCFInstructionFormattingData *ifd) |
| STR_FORMAT(scfFormatLocation, ifd->location, ifd); |
| |
| STR_PRINTF( |
| ": %04X %08X %02X %02X: ", |
| ifd->instruction->code, ifd->instruction->k, |
| ifd->instruction->jt, ifd->instruction->jf |
| ); |
| |
| STR_FORMAT(scfDisassembleInstruction, ifd); |
| STR_END_FORMATTER |
| |
| static void |
| scfLogProgram (SCFObject *scf) { |
| size_t count = scf->instruction.count; |
| |
| SCFInstructionFormattingData ifd; |
| memset(&ifd, 0, sizeof(ifd)); |
| |
| { |
| size_t index = count; |
| if (index > 0) index -= 1; |
| |
| char buffer[0X40]; |
| ifd.width.decimal = snprintf(buffer, sizeof(buffer), "%zu", index); |
| ifd.width.hexadecimal = snprintf(buffer, sizeof(buffer), "%zx", index); |
| } |
| |
| for (size_t index=0; index<count; index+=1) { |
| char log[0X100]; |
| STR_BEGIN(log, sizeof(log)); |
| STR_PRINTF("%s: instruction: %*zu ", scfLogLabel, ifd.width.decimal, index); |
| |
| ifd.instruction = &scf->instruction.array[index]; |
| ifd.location = index; |
| STR_FORMAT(scfFormatInstruction, &ifd); |
| |
| STR_END; |
| logMessage(SCF_LOG_LEVEL, "%s", log); |
| } |
| } |
| #endif /* SCF_LOG_PROGRAM */ |
| |
| static void |
| scfDestroyJumps (SCFJump **jumps) { |
| while (1) { |
| SCFJump *jump = scfPopJump(jumps); |
| if (!jump) break; |
| free(jump); |
| } |
| } |
| |
| static void |
| scfDestroyObject (SCFObject *scf) { |
| scfDestroyJumps(&scf->allow.jumps); |
| if (scf->instruction.array) free(scf->instruction.array); |
| if (scf->argument.array) free(scf->argument.array); |
| free(scf); |
| } |
| |
| static SCFObject * |
| scfNewObject (const SCFMode *mode) { |
| SCFObject *scf; |
| |
| if ((scf = malloc(sizeof(*scf)))) { |
| memset(scf, 0, sizeof(*scf)); |
| scf->mode = mode; |
| |
| scf->instruction.array = NULL; |
| scf->instruction.size = 0; |
| scf->instruction.count = 0; |
| |
| scf->argument.array = NULL; |
| scf->argument.size = 0; |
| scf->argument.count = 0; |
| |
| scf->allow.jumps = NULL; |
| |
| return scf; |
| } else { |
| logMallocError(); |
| } |
| |
| return NULL; |
| } |
| |
| static SCFObject * |
| scfMakeFilter (const SCFMode *mode) { |
| SCFObject *scf; |
| |
| if ((scf = scfNewObject(mode))) { |
| if (scfVerifyArchitecture(scf)) { |
| if (scfCheckSysemCall(scf)) { |
| if (scfCheckArguments(scf)) { |
| logMessage(SCF_LOG_LEVEL, |
| "%s: program size: %zu", |
| scfLogLabel, scf->instruction.count |
| ); |
| |
| #if SCF_LOG_PROGRAM |
| logMessage(SCF_LOG_LEVEL, "%s: begin program", scfLogLabel); |
| scfLogProgram(scf); |
| logMessage(SCF_LOG_LEVEL, "%s: end program", scfLogLabel); |
| #endif /* SCF_LOG_PROGRAM */ |
| |
| return scf; |
| } |
| } |
| } |
| |
| scfDestroyObject(scf); |
| } |
| |
| return NULL; |
| } |
| |
| static void |
| scfInstallFilter (const char *modeName) { |
| const SCFMode *mode = scfGetMode(modeName); |
| if (!mode->deny) return; |
| SCFObject *scf; |
| |
| #ifdef PR_SET_NO_NEW_PRIVS |
| if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) { |
| logSystemError("prctl[PR_SET_NO_NEW_PRIVS]"); |
| } |
| #endif /* PR_SET_NO_NEW_PRIVS */ |
| |
| if ((scf = scfMakeFilter(mode))) { |
| struct sock_fprog program = { |
| .filter = scf->instruction.array, |
| .len = scf->instruction.count |
| }; |
| |
| #if defined(PR_SET_SECCOMP) |
| if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &program, 0, 0) == -1) { |
| logSystemError("prctl[PR_SET_SECCOMP,SECCOMP_MODE_FILTER]"); |
| } |
| #elif defined(SECCOMP_SET_MODE_FILTER) |
| unsigned int flags = 0; |
| |
| if (seccomp(SECCOMP_SET_MODE_FILTER, flags, &program) == -1) { |
| logSystemError("seccomp[SECCOMP_SET_MODE_FILTER]"); |
| } |
| #else /* install system call filter */ |
| #warning no mechanism for installing the system call filter |
| #endif /* install system call filter */ |
| |
| scfDestroyObject(scf); |
| } |
| } |
| #endif /* SECCOMP_MODE_FILTER */ |
| |
| typedef void PrivilegesEstablishmentFunction (int stayPrivileged); |
| typedef void MissingPrivilegesLogger (void); |
| typedef void ReleaseResourcesFunction (void); |
| |
| typedef struct { |
| const char *reason; |
| PrivilegesEstablishmentFunction *establishPrivileges; |
| MissingPrivilegesLogger *logMissingPrivileges; |
| ReleaseResourcesFunction *releaseResources; |
| |
| #ifdef CAP_IS_SUPPORTED |
| cap_value_t capability; |
| unsigned char inheritable:1; |
| #endif /* CAP_IS_SUPPORTED */ |
| } PrivilegesMechanismEntry; |
| |
| static const PrivilegesMechanismEntry privilegesMechanismTable[] = { |
| { .reason = "for installing kernel modules", |
| .establishPrivileges = installKernelModules, |
| |
| #ifdef CAP_SYS_MODULE |
| .capability = CAP_SYS_MODULE, |
| .inheritable = 1, |
| #endif /* CAP_SYS_MODULE, */ |
| }, |
| |
| #ifdef HAVE_GRP_H |
| { .reason = "for joining the required groups", |
| .establishPrivileges = joinRequiredGroups, |
| .logMissingPrivileges = logMissingGroups, |
| .releaseResources = closeGroupsDatabase, |
| }, |
| #endif /* HAVE_GRP_H */ |
| |
| // This one must be last because it relinquishes the temporary capabilities. |
| #ifdef CAP_IS_SUPPORTED |
| { .reason = "for assigning required capabilities", |
| .establishPrivileges = setRequiredCapabilities, |
| .logMissingPrivileges = logMissingCapabilities, |
| } |
| #endif /* CAP_IS_SUPPORTED */ |
| }; static const uint8_t privilegesMechanismCount = ARRAY_COUNT(privilegesMechanismTable); |
| |
| static void |
| establishPrivileges (int stayPrivileged) { |
| if (amPrivilegedUser()) { |
| const PrivilegesMechanismEntry *pme = privilegesMechanismTable; |
| const PrivilegesMechanismEntry *end = pme + privilegesMechanismCount; |
| |
| while (pme < end) { |
| pme->establishPrivileges(stayPrivileged); |
| pme += 1; |
| } |
| } |
| |
| #ifdef CAP_IS_SUPPORTED |
| else { |
| const PrivilegesMechanismEntry *pme = privilegesMechanismTable; |
| const PrivilegesMechanismEntry *end = pme + privilegesMechanismCount; |
| |
| while (pme < end) { |
| cap_value_t capability = pme->capability; |
| |
| if (!capability || needCapability(capability, pme->inheritable, pme->reason)) { |
| pme->establishPrivileges(stayPrivileged); |
| } |
| |
| pme += 1; |
| } |
| } |
| #endif /* CAP_IS_SUPPORTED */ |
| |
| { |
| const PrivilegesMechanismEntry *pme = privilegesMechanismTable; |
| const PrivilegesMechanismEntry *end = pme + privilegesMechanismCount; |
| |
| while (pme < end) { |
| { |
| MissingPrivilegesLogger *log = pme->logMissingPrivileges; |
| if (log) log(); |
| } |
| |
| { |
| ReleaseResourcesFunction *release = pme->releaseResources; |
| if (release) release(); |
| } |
| |
| pme += 1; |
| } |
| } |
| } |
| |
| static int |
| isEnvironmentVariableSet (const char *name) { |
| const char *value = getenv(name); |
| return value && *value; |
| } |
| |
| static int |
| unsetEnvironmentVariable (const char *name) { |
| if (!isEnvironmentVariableSet(name)) return 1; |
| |
| if (unsetenv(name) != -1) { |
| logMessage(LOG_DEBUG, "environment variable unset: %s", name); |
| return 1; |
| } else { |
| logSystemError("unsetenv"); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| setEnvironmentVariable (const char *name, const char *value) { |
| if (setenv(name, value, 1) != -1) { |
| logMessage(LOG_DEBUG, "environment variable set: %s: %s", name, value); |
| return 1; |
| } else { |
| logSystemError("setenv"); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| changeEnvironmentVariable (const char *name, const char *value) { |
| if (!isEnvironmentVariableSet(name)) return 1; |
| return setEnvironmentVariable(name, value); |
| } |
| |
| static int |
| setHomeDirectory (const char *directory) { |
| if (!directory) return 0; |
| if (!*directory) return 0; |
| |
| if (chdir(directory) != -1) { |
| logMessage(LOG_INFO, "%s: %s", gettext("working directory changed"), directory); |
| setEnvironmentVariable("HOME", directory); |
| return 1; |
| } else { |
| logMessage(LOG_WARNING, |
| "working directory not changed: %s: %s", |
| directory, strerror(errno) |
| ); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| setCommandSearchPath (const char *path) { |
| const char *variable = "PATH"; |
| |
| if (!*path) { |
| int parameter = _CS_PATH; |
| size_t size = confstr(parameter, NULL, 0); |
| |
| if (size > 0) { |
| char buffer[size]; |
| confstr(parameter, buffer, sizeof(buffer)); |
| return setEnvironmentVariable(variable, buffer); |
| } |
| |
| path = "/usr/sbin:/sbin:/usr/bin:/bin"; |
| } |
| |
| return setEnvironmentVariable(variable, path); |
| } |
| |
| static int |
| setDefaultShell (const char *shell) { |
| if (!*shell) shell = "/bin/sh"; |
| return setEnvironmentVariable("SHELL", shell); |
| } |
| |
| #ifdef HAVE_PWD_H |
| static int |
| canSwitchGroup (gid_t gid) { |
| { |
| gid_t rGid, eGid, sGid; |
| getresgid(&rGid, &eGid, &sGid); |
| if ((gid == rGid) || (gid == eGid) || (gid == sGid)) return 1; |
| } |
| |
| return canSetSupplementaryGroups("for switching to the writable group"); |
| } |
| |
| static int |
| setXDGRuntimeDirectory (uid_t uid, gid_t gid) { |
| const char *variable = "XDG_RUNTIME_DIR"; |
| |
| const char *oldPath = getenv(variable); |
| if (!oldPath) return 1; |
| if (!*oldPath) return 1; |
| |
| const char *oldName = locatePathName(oldPath); |
| if (!oldName) return 1; |
| |
| int length = oldName - oldPath; |
| char newPath[length + 0X20]; |
| snprintf(newPath, sizeof(newPath), "%.*s%d", length, oldPath, uid); |
| |
| { |
| logMessage(LOG_DEBUG, "checking XDG runtime directory: %s", newPath); |
| int exists = 0; |
| |
| if (access(newPath, F_OK) != -1) { |
| exists = 1; |
| logMessage(LOG_DEBUG, "%s: %s", gettext("XDG runtime directory exists"), newPath); |
| } else if (errno == ENOENT) { |
| if (mkdir(newPath, S_IRWXU) != -1) { |
| if (chown(newPath, uid, gid) != 01) { |
| exists = 1; |
| logMessage(LOG_INFO, "%s: %s", gettext("XDG runtime directory created"), newPath); |
| } else { |
| logSystemError("chown"); |
| } |
| |
| if (!exists) { |
| if (rmdir(newPath) == -1) { |
| logSystemError("rmdir"); |
| } |
| } |
| } else { |
| logSystemError("mkdir"); |
| } |
| } else { |
| logSystemError("access"); |
| } |
| |
| if (!exists) { |
| logMessage(LOG_WARNING, "%s: %s", gettext("XDG runtime directory access problem"), newPath); |
| } |
| } |
| |
| return setEnvironmentVariable(variable, newPath); |
| } |
| |
| static int |
| setProcessOwnership (uid_t uid, gid_t gid) { |
| if (setXDGRuntimeDirectory(uid, gid)) { |
| gid_t oldRgid, oldEgid, oldSgid; |
| |
| if (getresgid(&oldRgid, &oldEgid, &oldSgid) != -1) { |
| if (setresgid(gid, gid, gid) != -1) { |
| if (setresuid(uid, uid, uid) != -1) { |
| return 1; |
| } else { |
| logSystemError("setresuid"); |
| } |
| |
| if (setresgid(oldRgid, oldEgid, oldSgid) == -1) { |
| logSystemError("setresgid"); |
| } |
| } else { |
| logSystemError("setresgid"); |
| } |
| } else { |
| logSystemError("getresgid"); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int |
| switchToUser (const char *user, int *haveHomeDirectory) { |
| const struct passwd *pwd; |
| |
| if ((pwd = getpwnam(user))) { |
| uid_t uid = pwd->pw_uid; |
| gid_t gid = pwd->pw_gid; |
| |
| if (!uid) { |
| logMessage(LOG_WARNING, "not an unprivileged user: %s", user); |
| } else if (setProcessOwnership(uid, gid)) { |
| logMessage(LOG_NOTICE, "%s: %s", gettext("switched to unprivileged user"), user); |
| |
| changeEnvironmentVariable("USER", user); |
| changeEnvironmentVariable("LOGNAME", user); |
| |
| unsetEnvironmentVariable("XDG_CONFIG_HOME"); |
| unsetEnvironmentVariable("XDG_DATA_DIRS"); |
| |
| if (setHomeDirectory(pwd->pw_dir)) *haveHomeDirectory = 1; |
| forgetOverrideDirectories(); |
| |
| return 1; |
| } |
| } else { |
| logMessage(LOG_WARNING, "unprivileged user not found: %s", user); |
| } |
| |
| return 0; |
| } |
| |
| static int |
| switchUser (const char *user, int stayPrivileged, int *haveHomeDirectory) { |
| if (amPrivilegedUser()) { |
| if (stayPrivileged) { |
| logMessage(LOG_NOTICE, "%s", gettext("not switching to an unprivileged user")); |
| } else if (!*user) { |
| logMessage(LOG_DEBUG, "default unprivileged user not configured"); |
| } else if (switchToUser(user, haveHomeDirectory)) { |
| return 1; |
| } else { |
| logMessage(LOG_WARNING, "couldn't switch to the unprivileged user: %s", user); |
| } |
| } |
| |
| { |
| uid_t uid = getuid(); |
| gid_t gid = getgid(); |
| |
| { |
| const struct passwd *pwd; |
| const char *name; |
| char number[0X10]; |
| |
| if ((pwd = getpwuid(uid))) { |
| name = pwd->pw_name; |
| if (canSwitchGroup(pwd->pw_gid)) gid = pwd->pw_gid; |
| } else { |
| snprintf(number, sizeof(number), "%d", uid); |
| name = number; |
| } |
| |
| logMessage(LOG_NOTICE, "%s: %s", gettext("executing as the invoking user"), name); |
| } |
| |
| setProcessOwnership(uid, gid); |
| if (!amPrivilegedUser()) *haveHomeDirectory = 1; |
| } |
| |
| return 0; |
| } |
| |
| static const char * |
| getSocketsDirectory (void) { |
| const char *path = BRLAPI_SOCKETPATH; |
| if (!ensureDirectory(path, 1)) path = NULL; |
| return path; |
| } |
| |
| typedef struct { |
| const char *whichDirectory; |
| const char *(*getPath) (void); |
| const char *expectedName; |
| } StateDirectoryEntry; |
| |
| static const StateDirectoryEntry stateDirectoryTable[] = { |
| { .whichDirectory = "updatable", |
| .getPath = getUpdatableDirectory, |
| .expectedName = "brltty", |
| }, |
| |
| { .whichDirectory = "writable", |
| .getPath = getWritableDirectory, |
| .expectedName = "brltty", |
| }, |
| |
| { .whichDirectory = "sockets", |
| .getPath = getSocketsDirectory, |
| .expectedName = "BrlAPI", |
| }, |
| }; static const uint8_t stateDirectoryCount = ARRAY_COUNT(stateDirectoryTable); |
| |
| static int |
| canCreateStateDirectory (void) { |
| #ifdef CAP_DAC_OVERRIDE |
| if (needCapability(CAP_DAC_OVERRIDE, 0, "for creating missing state directories")) { |
| return 1; |
| } |
| #endif /* CAP_DAC_OVERRIDE */ |
| |
| return 0; |
| } |
| |
| static const char * |
| getStateDirectoryPath (const StateDirectoryEntry *sde) { |
| { |
| const char *path = sde->getPath(); |
| if (path) return path; |
| } |
| |
| if (!canCreateStateDirectory()) return NULL; |
| return sde->getPath(); |
| } |
| |
| static int |
| canChangePathOwnership (const char *path) { |
| #ifdef CAP_CHOWN |
| if (needCapability(CAP_CHOWN, 0, "for claiming ownership of the state directories")) { |
| return 1; |
| } |
| #endif /* CAP_CHOWN */ |
| |
| return 0; |
| } |
| |
| static int |
| canChangePathPermissions (const char *path) { |
| #ifdef CAP_FOWNER |
| if (needCapability(CAP_FOWNER, 0, "for adding group permissions to the state directories")) { |
| return 1; |
| } |
| #endif /* CAP_FOWNER */ |
| |
| return 0; |
| } |
| |
| typedef struct { |
| uid_t owningUser; |
| gid_t owningGroup; |
| } StateDirectoryData; |
| |
| static int |
| claimStateDirectory (const PathProcessorParameters *parameters) { |
| const StateDirectoryData *sdd = parameters->data; |
| const char *path = parameters->path; |
| uid_t user = sdd->owningUser; |
| gid_t group = sdd->owningGroup; |
| struct stat status; |
| |
| if (stat(path, &status) != -1) { |
| int ownershipClaimed = 0; |
| |
| if ((status.st_uid == user) && (status.st_gid == group)) { |
| ownershipClaimed = 1; |
| } else if (!canChangePathOwnership(path)) { |
| logMessage(LOG_WARNING, "can't claim ownership: %s", path); |
| } else if (chown(path, user, group) == -1) { |
| logSystemError("chown"); |
| } else { |
| logMessage(LOG_INFO, "%s: %s", gettext("ownership claimed"), path); |
| ownershipClaimed = 1; |
| } |
| |
| if (ownershipClaimed) { |
| mode_t oldMode = status.st_mode; |
| mode_t newMode = oldMode; |
| |
| newMode |= S_IRGRP | S_IWGRP; |
| if (S_ISDIR(newMode)) newMode |= S_IXGRP | S_ISGID; |
| |
| if (newMode != oldMode) { |
| if (!canChangePathPermissions(path)) { |
| logMessage(LOG_WARNING, "can't add group permissions: %s", path); |
| } else if (chmod(path, newMode) != -1) { |
| logMessage(LOG_INFO, "%s: %s", gettext("group permissions added"), path); |
| } else { |
| logSystemError("chmod"); |
| } |
| } |
| } |
| } else { |
| logSystemError("stat"); |
| } |
| |
| return 1; |
| } |
| |
| static void |
| claimStateDirectories (void) { |
| StateDirectoryData sdd = { |
| .owningUser = geteuid(), |
| .owningGroup = getegid(), |
| }; |
| |
| const StateDirectoryEntry *sde = stateDirectoryTable; |
| const StateDirectoryEntry *end = sde + stateDirectoryCount; |
| |
| while (sde < end) { |
| const char *path = getStateDirectoryPath(sde); |
| |
| if (path && *path) { |
| const char *name = locatePathName(path); |
| |
| if (strcasecmp(name, sde->expectedName) == 0) { |
| processPathTree(path, claimStateDirectory, &sdd); |
| } else { |
| logMessage(LOG_DEBUG, |
| "not claiming %s directory: %s (expecting %s)", |
| sde->whichDirectory, path, sde->expectedName |
| ); |
| } |
| } |
| |
| sde += 1; |
| } |
| } |
| #endif /* HAVE_PWD_H */ |
| |
| typedef enum { |
| PARM_PATH, |
| PARM_SCFMODE, |
| PARM_SHELL, |
| PARM_USER, |
| } Parameters; |
| |
| |
| static const char *const *const privilegeParameterNames = |
| NULL_TERMINATED_STRING_ARRAY( |
| "path", "scfmode", "shell", "user" |
| ); |
| |
| const char *const * |
| getPrivilegeParameterNames (void) { |
| return privilegeParameterNames; |
| } |
| |
| const char * |
| getPrivilegeParametersPlatform (void) { |
| return "lx"; |
| } |
| |
| void |
| establishProgramPrivileges (char **parameters, int stayPrivileged) { |
| logCurrentCapabilities("at start"); |
| |
| setCommandSearchPath(parameters[PARM_PATH]); |
| setDefaultShell(parameters[PARM_SHELL]); |
| |
| #ifdef PR_SET_KEEPCAPS |
| if (prctl(PR_SET_KEEPCAPS, 1, 0, 0, 0) == -1) { |
| logSystemError("prctl[PR_SET_KEEPCAPS]"); |
| } |
| #endif /* PR_SET_KEEPCAPS */ |
| |
| #ifdef HAVE_SCHED_H |
| isolateNamespaces(); |
| #endif /* HAVE_SCHED_H */ |
| |
| { |
| const char *unprivilegedUser = parameters[PARM_USER]; |
| int haveHomeDirectory = 0; |
| |
| #ifdef HAVE_PWD_H |
| int switched = switchUser( |
| unprivilegedUser, |
| stayPrivileged, |
| &haveHomeDirectory |
| ); |
| |
| if (switched) { |
| umask(umask(0) & ~S_IRWXG); |
| claimStateDirectories(); |
| } else { |
| logMessage(LOG_DEBUG, "not claiming state directories"); |
| } |
| |
| endpwent(); |
| #endif /* HAVE_PWD_H */ |
| |
| if (!haveHomeDirectory) { |
| if (!setHomeDirectory(getUpdatableDirectory())) { |
| logMessage(LOG_WARNING, "home directory not set"); |
| } |
| } |
| } |
| |
| establishPrivileges(stayPrivileged); |
| logCurrentCapabilities("after relinquish"); |
| |
| #ifdef SECCOMP_MODE_FILTER |
| scfInstallFilter(parameters[PARM_SCFMODE]); |
| #endif /* SECCOMP_MODE_FILTER */ |
| } |