SandboxEnvironmentLinux.cpp [plain text]
#include "SandboxEnvironmentLinux.h"
#include <dirent.h>
#include <dlfcn.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <grp.h>
#include <limits.h>
#include <link.h>
#include <pwd.h>
#include <sched.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/capability.h>
#include <sys/mount.h>
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <utime.h>
#include <vector>
static const unsigned maximumPathLength = 512;
static char sandboxDirectory[maximumPathLength];
static uid_t sandboxUserUID;
static uid_t sandboxUserGID;
static inline void strlcpy(char *destination, const char* source, int maxLength)
{
destination[0] = '\0';
strncat(destination, source, maxLength - 1);
}
static inline void strlcat(char* destination, const char* source, int maxLength)
{
strncat(destination, source, maxLength - 1 - strnlen(destination, maxLength - 1));
}
static inline void appendDirectoryComponent(char* fullPath, const char* directoryPath, const char* fileName)
{
strlcpy(fullPath, directoryPath, maximumPathLength);
strlcat(fullPath, fileName, maximumPathLength);
}
static void launchChangeRootHelper(int helperSocket, int webProcessSocket)
{
struct rlimit restrictedResource = { 0, 0 };
if (setrlimit(RLIMIT_NOFILE, &restrictedResource) == -1) {
fprintf(stderr, "Helper couldn't set the resource limit: %s.\n", strerror(errno));
return;
}
if (close(webProcessSocket) == -1) {
fprintf(stderr, "Failed to close socket %d: %s.\n", webProcessSocket, strerror(errno));
return;
}
char message;
if (read(helperSocket, &message, 1) != 1) {
fprintf(stderr, "Failed to read message from the web process: %s %d.\n", strerror(errno), errno);
return;
}
if (message != MSG_CHROOTME) {
fprintf(stderr, "Wrong message recieved: %x.\n", message);
return;
}
struct stat sandboxDirectoryInfo;
if (lstat(sandboxDirectory, &sandboxDirectoryInfo) == -1) {
fprintf(stderr, "Sandbox directory (%s) is not available: %s.\n", sandboxDirectory, strerror(errno));
return;
}
if (!S_ISDIR(sandboxDirectoryInfo.st_mode)) {
fprintf(stderr, "%s is not a directory!\n", sandboxDirectory);
return;
}
if (chroot(sandboxDirectory) == -1) {
fprintf(stderr, "Chrooting failed: %s.\n", strerror(errno));
return;
}
if (chdir("/") == -1) {
fprintf(stderr, "Couldn't change the working directory to /.: %s\n", strerror(errno));
return;
}
message = MSG_CHROOTED;
if (write(helperSocket, &message, 1) != 1) {
fprintf(stderr, "Couldn't send acknowledgement to WebProcess: %s.\n", strerror(errno));
return;
}
exit(EXIT_SUCCESS);
}
static bool setEnvironmentVariablesForChangeRootHelper(pid_t pid, int helperSocket, int webProcessSocket)
{
const int descriptorSize = 32;
char socketDescriptor[descriptorSize];
char sandboxHelperPID[descriptorSize];
int length = snprintf(sandboxHelperPID, sizeof(sandboxHelperPID), "%u", pid);
if (length < 0 || length >= sizeof(sandboxHelperPID)) {
fprintf(stderr, "Failed to convert the sandbox helper PID to a string.\n");
return false;
}
if (setenv(SANDBOX_HELPER_PID, sandboxHelperPID, 1) == -1) {
fprintf(stderr, "Couldn't set the SBX_HELPER_PID environment variable: %s\n", strerror(errno));
return false;
}
length = snprintf(socketDescriptor, sizeof(socketDescriptor), "%u", webProcessSocket);
if (length < 0 || length >= sizeof(socketDescriptor)) {
fprintf(stderr, "Failed to convert the sandbox helper file descriptor to a string.\n");
return false;
}
if (setenv(SANDBOX_DESCRIPTOR, socketDescriptor, 1) == -1) {
fprintf(stderr, "Failed to store the helper's file descriptor into an environment variable: %s.\n", strerror(errno));
return false;
}
if (close(helperSocket) == -1) {
fprintf(stderr, "Closing of %d failed: %s.\n", helperSocket, strerror(errno));
return false;
}
return true;
}
static bool prepareAndStartChangeRootHelper()
{
int socketPair[2];
if (socketpair(AF_UNIX, SOCK_STREAM, 0, socketPair) == -1) {
fprintf(stderr, "Couldn't create socketpair: %s\n", strerror(errno));
return false;
}
pid_t pid = syscall(SYS_clone, CLONE_FS | SIGCHLD, 0, 0, 0);
if (pid == -1) {
fprintf(stderr, "Clone failed: %s\n", strerror(errno));
return false;
}
if (!pid) {
launchChangeRootHelper(socketPair[0], socketPair[1]);
exit(EXIT_FAILURE);
return false;
}
return setEnvironmentVariablesForChangeRootHelper(pid, socketPair[0], socketPair[1]);
}
static bool setCapabilities(cap_value_t* capabilityList, int length)
{
cap_t capabilities = cap_init();
if (!capabilities) {
fprintf(stderr, "Failed to initialize process capabilities: %s.\n", strerror(errno));
return false;
}
if (cap_clear(capabilities) == -1) {
fprintf(stderr, "Failed to clear process capabilities: %s.\n", strerror(errno));
return false;
}
if (capabilityList && length) {
if (cap_set_flag(capabilities, CAP_EFFECTIVE, length, capabilityList, CAP_SET) == -1
|| cap_set_flag(capabilities, CAP_INHERITABLE, length, capabilityList, CAP_SET) == -1
|| cap_set_flag(capabilities, CAP_PERMITTED, length, capabilityList, CAP_SET) == -1) {
fprintf(stderr, "Failed to set process capability flags: %s.\n", strerror(errno));
cap_free(capabilities);
return false;
}
}
if (cap_set_proc(capabilities) == -1) {
fprintf(stderr, "Failed to set process capabilities: %s.\n", strerror(errno));
cap_free(capabilities);
return false;
}
cap_free(capabilities);
return true;
}
static bool dropPrivileges()
{
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) == -1) {
fprintf(stderr, "Setting dumpable is failed: %s\n", strerror(errno));
return false;
}
if (setresgid(sandboxUserGID, sandboxUserGID, sandboxUserGID) == -1) {
fprintf(stderr, "Failed to fallback to group: %d.\n", sandboxUserGID);
return false;
}
if (setresuid(sandboxUserUID, sandboxUserUID, sandboxUserUID) == -1) {
fprintf(stderr, "Failed to fallback to user: %d.\n", sandboxUserUID);
return false;
}
return setCapabilities(0, 0);
}
static bool fileExists(const char* path)
{
struct stat fileStat;
if (lstat(path, &fileStat) == -1) {
if (errno == ENOENT)
return false;
}
return true;
}
static mode_t directoryPermissions(const char* directory)
{
struct stat fileStat;
if (lstat(directory, &fileStat) == -1) {
fprintf(stderr, "Failed to obtain information about directory (%s): %s\n", directory, strerror(errno));
return false;
}
return fileStat.st_mode;
}
static bool createDirectory(char* pathToCreate, const char* nextDirectoryToCreate)
{
strlcat(pathToCreate, nextDirectoryToCreate, maximumPathLength);
char pathToCreateInSandbox[maximumPathLength];
appendDirectoryComponent(pathToCreateInSandbox, sandboxDirectory, pathToCreate);
mode_t mode = directoryPermissions(pathToCreate);
if (mkdir(pathToCreateInSandbox, mode) == -1) {
if (errno != EEXIST) {
fprintf(stderr, "Creation of %s failed: %s\n", pathToCreateInSandbox, strerror(errno));
return false;
}
}
struct stat fileInfo;
if (lstat(pathToCreate, &fileInfo) == -1) {
fprintf(stderr, "Couldn't obtain information about directory (%s): %s\n", pathToCreate, strerror(errno));
return false;
}
if (fileInfo.st_uid == getuid()) {
if (chown(pathToCreateInSandbox, sandboxUserUID, sandboxUserGID) == -1) {
fprintf(stderr, "Failed to assign the ownership of %s to the sandbox user: %s.\n", pathToCreateInSandbox, strerror(errno));
return false;
}
}
if (chmod(pathToCreateInSandbox, fileInfo.st_mode) == -1) {
fprintf(stderr, "Failed to set the permissions of %s: %s.\n", pathToCreateInSandbox, strerror(errno));
return false;
}
return true;
}
static bool createDirectoryChain(const char* path)
{
char fullPathInSandbox[maximumPathLength];
appendDirectoryComponent(fullPathInSandbox, sandboxDirectory, path);
if (fileExists(fullPathInSandbox))
return true;
char alreadyCreatedPath[maximumPathLength];
alreadyCreatedPath[0] = '\0';
const char* startPos = path + 1;
const char* endPos;
while ((endPos = strchr(startPos, '/'))) {
char nextDirectoryToCreate[maximumPathLength];
strlcpy(nextDirectoryToCreate, startPos - 1, strnlen(startPos - 1, endPos - startPos + 1) + 1);
if (!createDirectory(alreadyCreatedPath, nextDirectoryToCreate))
return false;
startPos = endPos + 1;
}
alreadyCreatedPath[0] = '\0';
return createDirectory(alreadyCreatedPath, path);
}
static bool createDeviceFiles()
{
const char* devDirectory = "/dev";
if (!createDirectoryChain(devDirectory))
return false;
const char* devices[2] = { "/dev/random", "/dev/urandom" };
for (int i = 0; i < sizeof(devices) / sizeof(devices[0]); ++i) {
struct stat status;
if (lstat(devices[i], &status)) {
fprintf(stderr, "Failed to stat device file (%s): %s\n", devices[i], strerror(errno));
return false;
}
dev_t dev = status.st_rdev;
char device[maximumPathLength];
appendDirectoryComponent(device, sandboxDirectory, devices[i]);
if (mknod(device, S_IFCHR | S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH, makedev(major(dev), minor(dev))) == -1) {
if (errno != EEXIST) {
fprintf(stderr, "Couldn't create device file %s: %s\n", device, strerror(errno));
return false;
}
}
}
return true;
}
static bool mountFileSystems()
{
const char* procPath = "/proc";
if (!createDirectoryChain(procPath))
return false;
char procPathInSandbox[maximumPathLength];
appendDirectoryComponent(procPathInSandbox, sandboxDirectory, procPath);
if (mount(procPath, procPathInSandbox, "proc", 0, 0) == -1) {
if (errno != EBUSY) {
fprintf(stderr, "Failed to mount '%s': %s\n", procPath, strerror(errno));
return false;
}
}
const char* sharedMemoryPath = "/run/shm";
if (!createDirectoryChain(sharedMemoryPath)) {
fprintf(stderr, "Failed to create directory for /run/shm in the sandbox: %s.\n", strerror(errno));
return false;
}
char sharedMemoryPathInSandbox[maximumPathLength];
appendDirectoryComponent(sharedMemoryPathInSandbox, sandboxDirectory, sharedMemoryPath);
if (mount(sharedMemoryPath, sharedMemoryPathInSandbox, "tmpfs", 0, 0) == -1) {
if (errno != EBUSY) {
fprintf(stderr, "Failed to mount '%s': %s.\n", sharedMemoryPath, strerror(errno));
return false;
}
}
return true;
}
static bool linkFile(const char* sourceFile, const char* targetFile)
{
char oldPath[maximumPathLength];
char targetPath[maximumPathLength];
strlcpy(oldPath, sourceFile, maximumPathLength);
strlcpy(targetPath, targetFile, maximumPathLength);
while (true) {
struct stat fileInfo;
if (lstat(oldPath, &fileInfo) == -1) {
if (errno != ENOENT) {
fprintf(stderr, "Couldn't obtain information about %s: %s\n", oldPath, strerror(errno));
return false;
}
return true;
}
const char* endOfBaseDirectoryInSource = strrchr(oldPath, '/');
if (!endOfBaseDirectoryInSource) {
fprintf(stderr, "Invalid source: %s.\n", oldPath);
return false;
}
char baseDirectoryOfSource[maximumPathLength];
strlcpy(baseDirectoryOfSource, oldPath, endOfBaseDirectoryInSource - oldPath + 2);
if (!createDirectoryChain(baseDirectoryOfSource)) {
fprintf(stderr, "Creating %s failed: %s.\n", baseDirectoryOfSource, strerror(errno));
return false;
}
if (link(oldPath, targetPath) == -1) {
if (errno != EEXIST && errno != ENOENT) {
fprintf(stderr, "Linking %s failed: %s.\n", oldPath, strerror(errno));
return false;
}
}
if ((fileInfo.st_mode & S_IFMT) != S_IFLNK)
break;
char symlinkTarget[maximumPathLength];
int lengthOfTheLink = readlink(oldPath, symlinkTarget, sizeof(symlinkTarget) - 1);
if (lengthOfTheLink > 0)
symlinkTarget[lengthOfTheLink] = '\0';
char symlinkTargetInRealWorld[maximumPathLength];
char symlinkTargetInSandbox[maximumPathLength];
if (symlinkTarget[0] == '/') {
strlcpy(symlinkTargetInRealWorld, symlinkTarget, maximumPathLength);
appendDirectoryComponent(symlinkTargetInSandbox, sandboxDirectory, symlinkTarget);
} else {
appendDirectoryComponent(symlinkTargetInRealWorld, baseDirectoryOfSource, symlinkTarget);
appendDirectoryComponent(symlinkTargetInSandbox, sandboxDirectory, symlinkTargetInRealWorld);
}
oldPath[0] = '\0';
targetPath[0] = '\0';
strlcat(oldPath, symlinkTargetInRealWorld, maximumPathLength);
strlcat(targetPath, symlinkTargetInSandbox, maximumPathLength);
}
return true;
}
static bool linkDirectory(const char* sourceDirectoryPath, const char* targetDirectoryPath)
{
if (!createDirectoryChain(sourceDirectoryPath))
return false;
DIR* directory = opendir(sourceDirectoryPath);
if (!directory) {
fprintf(stderr, "Couldn't open directory %s: %s\n", sourceDirectoryPath, strerror(errno));
return false;
}
while (struct dirent *directoryInfo = readdir(directory)) {
char* fileName = directoryInfo->d_name;
if (!strcmp(fileName, ".") || !strcmp(fileName, ".."))
continue;
char sourceFile[maximumPathLength];
char targetFile[maximumPathLength];
appendDirectoryComponent(sourceFile, sourceDirectoryPath, fileName);
appendDirectoryComponent(targetFile, targetDirectoryPath, fileName);
bool returnValue;
if (directoryInfo->d_type == DT_DIR) {
strncat(sourceFile, "/", 1);
strncat(targetFile, "/", 1);
returnValue = linkDirectory(sourceFile, targetFile);
} else
returnValue = linkFile(sourceFile, targetFile);
if (!returnValue)
return false;
}
struct stat fileStat;
if (lstat(sourceDirectoryPath, &fileStat) == -1) {
fprintf(stderr, "Failed to obtain information about the directory '%s': %s\n", sourceDirectoryPath, strerror(errno));
return false;
}
struct utimbuf times;
times.actime = fileStat.st_atime;
times.modtime = fileStat.st_mtime;
if (utime(targetDirectoryPath, ×) == -1) {
fprintf(stderr, "Couldn't set back the last modification time of '%s': %s\n", targetDirectoryPath, strerror(errno));
return false;
}
return true;
}
static bool collectRunTimeDependencies()
{
const char* runtimeDependencies[] = {
"libnss_dns.so",
"libresolv.so",
"libssl.so",
"libcrypto.so"
};
for (int i = 0; i < sizeof(runtimeDependencies) / sizeof(runtimeDependencies[0]); ++i) {
void* handle = dlopen(runtimeDependencies[i], RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Couldn't get the handler of %s: %s\n", runtimeDependencies[i], dlerror());
return false;
}
struct link_map* linkMap;
if (dlinfo(handle, RTLD_DI_LINKMAP, &linkMap) == -1) {
fprintf(stderr, "Couldn't get information about %s: %s\n", runtimeDependencies[i], dlerror());
return false;
}
if (!linkMap) {
fprintf(stderr, "Couldn't get the linkmap of %s: %s.\n", runtimeDependencies[i], strerror(errno));
return false;
}
char pathOfTheLibraryInSandbox[maximumPathLength];
appendDirectoryComponent(pathOfTheLibraryInSandbox, sandboxDirectory, linkMap->l_name);
if (!linkFile(linkMap->l_name, pathOfTheLibraryInSandbox)) {
fprintf(stderr, "Linking runtime dependency: %s failed: %s\n", linkMap->l_name, strerror(errno));
dlclose(handle);
return false;
}
dlclose(handle);
}
return true;
}
static bool setupXauthorityForNobodyUser()
{
char buffer[BUFSIZ];
size_t size;
struct passwd* realUser = getpwuid(getuid());
if (!realUser) {
fprintf(stderr, "Couldn't obtain the current user: %s\n", strerror(errno));
return false;
}
char xauthorityOfRealUser[maximumPathLength];
char xauthorityInSandbox[maximumPathLength];
appendDirectoryComponent(xauthorityOfRealUser, realUser->pw_dir, "/.Xauthority");
appendDirectoryComponent(xauthorityInSandbox, sandboxDirectory, xauthorityOfRealUser);
FILE* source = fopen(xauthorityOfRealUser, "rb");
if (!source) {
fprintf(stderr, "Couldn't open %s: %s\n", xauthorityOfRealUser, strerror(errno));
return false;
}
FILE* dest = fopen(xauthorityInSandbox, "wb");
if (!dest) {
fprintf(stderr, "Couldn't open %s: %s\n", xauthorityInSandbox, strerror(errno));
return false;
}
while ((size = fread(buffer, 1, BUFSIZ, source))) {
if (fwrite(buffer, 1, size, dest) != size) {
fprintf(stderr, "Failed to copy .Xauthority to the sandbox: %s.\n", strerror(errno));
return false;
}
}
if (fclose(source)) {
fprintf(stderr, "Closing the .Xauthority file of the real user failed: %s\n", strerror(errno));
return false;
}
if (fclose(dest)) {
fprintf(stderr, "Closing the .Xauthority file of the sandbox user failed: %s\n", strerror(errno));
return false;
}
if (chown(xauthorityInSandbox, sandboxUserUID, sandboxUserGID) == -1) {
fprintf(stderr, "Chowning .Xauthority (%s) failed: %s.\n", xauthorityInSandbox, strerror(errno));
return false;
}
if (setenv("XAUTHORITY", xauthorityInSandbox, 1) == -1) {
fprintf(stderr, "Couldn't set the XAUTHORITY envrionment variable: %s\n", strerror(errno));
return false;
}
return true;
}
static bool initializeSandbox()
{
if (mkdir(sandboxDirectory, S_IFDIR | S_IXUSR | S_IXOTH) == -1) {
if (errno != EEXIST) {
fprintf(stderr, "Couldn't create the sandbox directory: %s\n", strerror(errno));
return false;
}
}
if (!createDeviceFiles())
return false;
if (!mountFileSystems())
return false;
struct passwd* userInfo = getpwuid(getuid());
const char* home = userInfo->pw_dir;
char localDirectory[maximumPathLength];
char cacheDirectory[maximumPathLength];
char fontDirectory[maximumPathLength];
appendDirectoryComponent(localDirectory, home, "/.local/share/");
appendDirectoryComponent(cacheDirectory, home, "/.cache/");
appendDirectoryComponent(fontDirectory, home, "/.fontconfig/");
const char* linkedDirectories[] = {
cacheDirectory,
fontDirectory,
localDirectory,
"/etc/fonts/",
"/etc/ssl/certs/",
"/var/cache/fontconfig/",
"/usr/share/fonts/"
};
for (int i = 0; i < sizeof(linkedDirectories) / sizeof(linkedDirectories[0]); ++i) {
char linkedDirectoryInSandbox[maximumPathLength];
appendDirectoryComponent(linkedDirectoryInSandbox, sandboxDirectory, linkedDirectories[i]);
if (!linkDirectory(linkedDirectories[i], linkedDirectoryInSandbox))
return false;
}
if (!setupXauthorityForNobodyUser())
return false;
return collectRunTimeDependencies();
}
static bool restrictCapabilities()
{
cap_value_t capabilityList[] = { CAP_SETUID, CAP_SETGID, CAP_SYS_ADMIN, CAP_SYS_CHROOT};
if (!setCapabilities(capabilityList, sizeof(capabilityList) / sizeof(capabilityList[0]))) {
fprintf(stderr, "Could not adjust process capabilities: %s.\n", strerror(errno));
return false;
}
return true;
}
static bool moveToNewPIDNamespace()
{
int status;
pid_t expectedPID;
pid_t pid = syscall(SYS_clone, CLONE_NEWPID | SIGCHLD, 0, 0, 0);
if (pid == -1) {
fprintf(stderr, "Cloning is failed: %s\n", strerror(errno));
return false;
}
if (!pid) {
if (getpid() != 1) {
fprintf(stderr, "Couldn't create a new PID namespace.\n");
return false;
}
return true;
}
expectedPID = waitpid(pid, &status, 0);
if (expectedPID != pid) {
fprintf(stderr, "Process with PID %d terminated instead of the expected one with PID %d: %s.\n", expectedPID, pid, strerror(errno));
exit(EXIT_FAILURE);
}
if (WIFEXITED(status))
exit(WEXITSTATUS(status));
exit(EXIT_SUCCESS);
}
static bool run(int argc, char *const argv[])
{
struct passwd* userInfo = getpwuid(getuid());
if (!userInfo) {
fprintf(stderr, "Couldn't get the current user: %s.\n", strerror(errno));
return false;
}
appendDirectoryComponent(sandboxDirectory, userInfo->pw_dir, "/.wk2-sandbox");
if (struct passwd* nobodyUser = getpwnam("nobody")) {
sandboxUserUID = nobodyUser->pw_uid;
sandboxUserGID = nobodyUser->pw_gid;
} else {
sandboxUserUID = getuid();
sandboxUserGID = getgid();
}
if (argc != 3) {
fprintf(stderr, "Starting SandboxProcess requires 3 parameters!\n");
return false;
}
if (geteuid()) {
fprintf(stderr, "The sandbox is not seteuid root.\n");
return false;
}
if (!getuid()) {
fprintf(stderr, "The sandbox is not designed to be run by root.\n");
return false;
}
if (!initializeSandbox())
return false;
if (!restrictCapabilities())
return false;
if (!moveToNewPIDNamespace())
return false;
if (!prepareAndStartChangeRootHelper())
return false;
if (!dropPrivileges())
return false;
if (!geteuid() || !getegid() || !setuid(0) || !setgid(0)) {
fprintf(stderr, "Dropping privileges failed!\n");
return false;
}
if (execl(argv[1], argv[1], argv[2], reinterpret_cast<char*>(0)) == -1) {
fprintf(stderr, "Couldn't start WebProcess: %s\n", strerror(errno));
return false;
}
return true;
}
int main(int argc, char *const argv[])
{
return run(argc, argv) ? 0 : 1;
}