#include "Runner.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define PIPE_READ 0 #define PIPE_WRITE 1 // the waiting thread. void waitForChildren(pid_t pid, const int encFsWatchDogPipe) { static std::atomic_int childCount(0); childCount.fetch_add(1); // This small process waits until the child finished // the 'wait' also cleans up the kernel process table and afterwards // we simply exit. int status; waitpid(pid, &status, 0); if (childCount.fetch_sub(1) == 1) { const char *homedir = getenv("HOME"); assert (homedir); // we checked that in the main.cpp try { std::filesystem::path path(homedir); path /= std::string(".iso-pipe"); std::filesystem::remove(path); } catch (const std::exception &e) { // realistically, the next line is irrelevant since nobody is listening. fprintf(stderr, "failed to remove iso-pipe. Error: %s\n", e.what()); } if (encFsWatchDogPipe) write(encFsWatchDogPipe, "go!", 3); // the watchdog sends the kill signal to the encFS process. exit(0); } } /* * The jailed app may use some shell features that detach and they will end up * be reparented to the 'jailer' process. * To avoid anyone becoming zombies we simply wait in a loop. */ void waitForChildren2() { while (true) { wait(nullptr); } } void waitForEncFs(pid_t pid) { int status; waitpid(pid, &status, 0); // if the watched encfs process dies, typically because of an // incorrect password, the starting of the jail should be cancelled. exit(0); } Runner::Runner(const Message &message, int errorFile) : m_outputFD(errorFile), m_message(message) { for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isJailId()) { m_jailId = i.jailId(); break; } } } void Runner::setOwnerUserId(uint32_t uid) { assert(uid > 0); m_ownerUid = uid; } void Runner::setProcessName(char *name, int allocatedSize) { m_processName = name; m_processNameSize = allocatedSize; } void Runner::run() { pid_t pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return; } if (pid) {// we are the parent (pid = child's pid) // printf("Fork one done, created %d (I'm %d). going back to listening\n", pid, getpid()); return; } // remove SIGCHILD handling we inherited from parent. struct sigaction act; memset(&act, 0, sizeof(act)); sigaction(SIGCHLD, &act, 0); // this fork doesn't communicate with the user-space app. for (auto fd : m_pipes) { close(fd); // cleanup } /* * While still fully root, start the encfs base, if needed. */ int encFSWatchDog = 0; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isJailPwd()) { // we decrypt the jail with the password given. encFSWatchDog = runEncFs(i.stringPtr(), i.stringLength()); if (encFSWatchDog == 0) { fprintf(stderr, "Failed to run encfs\n"); exit(18); } break; } } /* * VPN support */ bool hasVpn = false; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isVpnConfig()) { m_vpnBasePath = i.stringPtr(); hasVpn = true; } else if (i.isVpnPwdBool()) { m_vpnHasPwdFile = i.boolData(); } } if (hasVpn) { const std::string jailId = std::to_string(m_jailId); m_netNsName = "iso-jail" + jailId; // Clear and create fresh netns system(("/usr/bin/ip netns delete " + m_netNsName + " >/dev/null 2>&1").c_str()); system(("/usr/bin/ip link delete veth-" + m_netNsName + " >/dev/null 2>&1").c_str()); system(("/usr/bin/ip netns add " + m_netNsName).c_str()); // add a loopback system(("/usr/bin/ip netns exec " + m_netNsName + " ip link set dev lo up").c_str()); } /* * 'unshare' is the nicest way to run a program in a new namespace. * PID 'unshare' means that the to-be-run application can not see which other * processes are running outside of its own, app-local, namespace. * IPC 'unshare' means that shared memory, pipes etc which running processes * may make available are likewise namespaced away from the application. * NEWNS 'unshare' means we fork the filesystem, allowing us to do mounts * and umounts that change what the child process can see. */ if (-1 == unshare(CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS)) { fprintf(stderr, "Runner: Failed to unshare\n"); exit(0); } signal(SIGINT, SIG_IGN); signal(SIGTERM, SIG_IGN); // we make a second fork of the process because that also forks the PID namespace. // the child is (likely) getting PID 1 pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return; } if (pid) {// parent char messageBuf[20]; snprintf(messageBuf, sizeof(messageBuf) - 1, "%d", pid); sendUpstream(messageBuf); exit(0); // Let the jailer do it's own thing from here on out. } close(m_outputFD); // mounts, first propagation if (mount("none", "/", nullptr, MS_REC | MS_PRIVATE, nullptr)) { fprintf(stderr, "cannot change root filesystem propagation\n"); exit(1); } // get us a proc if (mount("proc", "/proc", "proc", MS_NOSUID|MS_NOEXEC|MS_NODEV, nullptr)) { fprintf(stderr, "Runner: Failed to mount proc\n"); exit(1); } int newEnvCount = 0; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isNewEnvVar()) ++newEnvCount; else if (i.isUnmount()) { auto md = i.mountData(); if (md.src.empty()) { fprintf(stderr, "Runner: internal error\n"); exit(1); // internal error. } if (umount2(md.src.c_str(), UMOUNT_NOFOLLOW) == -1) { if (!i.isTry()) { perror("Runner: umount failed"); fprintf(stderr, " '%s'\n", md.src.c_str()); exit(1); } } } else if (i.isRemount()) { auto md = i.mountData(); if (md.src.empty() || md.dst.empty()) { fprintf(stderr, "Runner: internal error\n"); exit(1); // internal error. } struct stat fromDetails; if (lstat(md.src.c_str(), &fromDetails)) { if (i.isTry()) // source missing continue; perror("Runner: failed due to rbind from a non-existing source"); exit(1); } // TODO verify all parts of the source path are available to USER. const bool sourceIsSock = (fromDetails.st_mode & S_IFSOCK) == S_IFSOCK; const bool sourceIsDir = !sourceIsSock && (fromDetails.st_mode & S_IFDIR) == S_IFDIR; // create dest-dir before mounting on it std::filesystem::path dest(md.dst); std::filesystem::path destDir(dest); bool destIsDir = !dest.has_filename(); bool destExists = false; struct stat toDetails; if (lstat(md.dst.c_str(), &toDetails) == 0) { destIsDir = (toDetails.st_mode & S_IFDIR) == S_IFDIR; destExists = true; } if (!destIsDir) { if (destExists && sourceIsDir) { fprintf(stderr, "Runner: rbind failed, source is dir, dest is not\n"); exit(1); } if (!sourceIsDir) destDir.remove_filename(); } else if (!sourceIsDir) { fprintf(stderr, "Runner: rbind failed, dest is dir, source is not"); exit(1); } mkdirs(destDir); if (!sourceIsDir && !destIsDir && !destExists) { // printf(" creating file to mount on %s\n", dest.string().c_str()); // then we need a simple file there. std::ofstream file; file.open(dest); file.close(); } unsigned long mountOptions = MS_BIND; if (sourceIsDir) mountOptions |= MS_REC; // recursive if (mount(md.src.c_str(), md.dst.c_str(), nullptr, mountOptions, nullptr)) { if (!i.isTry()) { perror("Runner: rbind failed"); fprintf(stderr, " %s -> %s\n", md.src.c_str(), md.dst.c_str()); exit(1); } } } else if (i.isCreateTmp()) { auto md = i.mountData(); if (md.dst.empty()) { fprintf(stderr, "Runner: internal error\n"); exit(1); // internal error. } if (!i.isTry()) { // if it is a 'try' we won't create the dir first. struct stat destDetails; if (lstat(md.dst.c_str(), &destDetails)) { if (mkdir(md.dst.c_str(), 0777)) { perror("Runner: tmpfs's mkdir failed"); exit(1); } } else if ((destDetails.st_mode & S_IFDIR) != S_IFDIR) { // is not dir perror("Runner: failed due to tmpfs over a non-dir"); exit(1); } } if (mount("tmpfs", md.dst.c_str(), "tmpfs", MS_NOSUID|MS_NODEV, nullptr)) { if (!i.isTry()) { fprintf(stderr, "Runner: Failed to create tmpfs at %s\n", md.dst.c_str()); exit(1); } } chown(md.dst.c_str(), m_ownerUid, getgid()); } else if (i.isCopy()) { auto cm = i.copyData(); if (cm.from.empty() || cm.to.empty() || cm.from.at(0) != '/' || cm.to.at(0) != '/') { fprintf(stderr, "Runner: invalid copy request.\n"); exit(1); } // is destination a dir? const std::filesystem::path to(cm.to); if (!to.has_filename()) { // copy to dir requested if (!i.isTry()) mkdirs(to); struct stat destDetails; if (lstat(cm.to.c_str(), &destDetails) == 0) { if ((destDetails.st_mode & S_IFDIR) != S_IFDIR) { if (i.isTry()) // lets not even call 'copy' continue; fprintf(stderr, "Runner: copy's target expected dir, but is not\n"); exit(1); } } } if (!runCopy(cm.from, cm.to)) { if (!i.isTry()) { fprintf(stderr, "Runner: Failed to copy %s\n", cm.from.c_str()); exit(1); } } } else if (i.isDBusMapping()) { auto map = i.dbusMapping(); // a dbus mapping is provided by the default tool '/bin/xdg-dbus-proxy' // we start one now. if (map.systemBus) // ignore this one for now, not sure what to filter yet. continue; pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return; } if (pid == 0) {// we are the child if (setuid(m_ownerUid) != 0 || seteuid(m_ownerUid) != 0) { exit(1); } char *arguments[9]; arguments[0] = const_cast("/bin/xdg-dbus-proxy"); arguments[1] = const_cast(map.from.c_str()); arguments[2] = const_cast(map.to.c_str()); arguments[3] = const_cast("--filter"); arguments[4] = const_cast("--call=org.freedesktop.Notifications=freedesktop/Notifications.*@org"); arguments[5] = const_cast("--call=org.freedesktop.Notifications=kde/Notifications.*@org"); arguments[6] = const_cast("--call=org.freedesktop.portal.Desktop=*@/org/freedesktop/portal/desktop/*"); arguments[7] = const_cast("--own=org.kde.*"); arguments[8] = 0; execv(arguments[0], arguments); } } } // Prepare for normal users const char *homedir = getenv("HOME"); assert (homedir); // we checked that in the main.cpp if (chdir(homedir)) { fprintf(stderr, "Runner: Failed to change to dir\n"); exit(1); } if (!m_netNsName.empty()) { // Start openVPN process /* * We start OpenVPN in the jail-ed namespaces for process-ip and the full mount setup. * But openVPN will run in the global networking namespace. * After it has made a connection we'll create the netns and move it there * and indeed force the jail to use the new netns too. */ assert (!m_vpnBasePath.empty()); auto ovpn = m_vpnBasePath + ".ovpn"; auto sh = m_vpnBasePath + ".sh"; auto ac = m_vpnBasePath + "ac"; // the users process copied the config files to the target jail and // gave them protection mask 0400. // It's not really needed, but why not make at least the password // file root jailed process can see them, but can't read them. if (!m_vpnHasPwdFile && chown(ac.c_str(), 0, 0)) { fprintf(stderr, "Runner: Failed to protect vpn ac\n"); exit(0); } auto pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); exit(0); } else if (pid == 0) { // child char *arguments[] = { const_cast("/usr/sbin/openvpn"), const_cast("--config"), const_cast(ovpn.c_str()), const_cast("--auth-user-pass"), const_cast(ac.c_str()), // needed to run our scripts const_cast("--script-security"), const_cast("2"), // notice that /tmp is personal to this jail const_cast("--log"), const_cast("/tmp/openvpn.log"), const_cast("--up"), const_cast(sh.c_str()), const_cast("--down"), const_cast(sh.c_str()), nullptr }; // continue life as openvpn execv(arguments[0], arguments); } assert(pid > 0); // poll for the generated script for (int i = 0; i < 60; ++i) { struct stat conf; if (lstat("/tmp/vpnconf.sh", &conf) == 0) break; std::this_thread::sleep_for(std::chrono::milliseconds(300)); } system("/tmp/vpnconf.sh"); // configure the network stack std::string nsPath = "/var/run/netns/" + m_netNsName; int nsFd = open(nsPath.c_str(), O_RDONLY); if (nsFd == -1) { perror("Runner: open netns"); exit(1); } if (setns(nsFd, CLONE_NEWNET) == -1) { perror("Runner: setns"); exit(1); } close(nsFd); } // drop permissions if (setuid(m_ownerUid) != 0 || seteuid(m_ownerUid) != 0) { fprintf(stderr, "Runner: Failed to change UID\n"); exit(1); } // check if there is an init script and run that as a child process. if (runInitScript()) { fprintf(stderr, "Runner: init script failed\n"); exit(1); } // no point keeping those open. fclose(stdin); fclose(stdout); fclose(stderr); // Keep this process waiting for more requests to start in this 'jail'. // the real process is going to be the child. pid = fork(); if (pid == -1) exit(1); if (pid) { // parent // printf("Fork 3 done, created %d (I'm %d). waiting for child to exit\n", pid, getpid()); renameThisProcess(m_processName, m_processNameSize, "jailer"); new std::thread(waitForChildren, pid, encFSWatchDog); new std::thread(waitForChildren2); // create the pipe that indicates this jail is occupied. // the pipe can be used by the dispatcher to send us more things to run // in this jail. mkfifo(".iso-pipe", 0750); /* * Wait for commands from the dispatcher. We may be asked to run another * app in the existing jail. * * This is a pipe, we open it for read. Read all there is and other side * will close the pipe when they pushed through their message. * We'll open it again for the next message. */ char buf[Message::MAX_SIZE + 1]; int msgSize = 0; while (true) { std::ifstream controlChannel; controlChannel.open(".iso-pipe", std::ios_base::in); if (!controlChannel.is_open()) { exit(0); } while (controlChannel.is_open()) { controlChannel.read(buf + msgSize, sizeof(buf) - msgSize); auto read = controlChannel.gcount(); if (read == 0) // pipe is closed, lets process break; msgSize += read; if (msgSize >= (int) sizeof(buf)) {// message too big. Ignore. msgSize = 0; break; } } if (msgSize) { pid = fork(); if (pid == -1) exit(1); if (pid) { // I'm parent // pid is child's pid, lets wait for them in a thread. new std::thread(waitForChildren, pid, encFSWatchDog); } else { // run up to exec, below! m_message = Message(buf, msgSize); newEnvCount = 0; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isNewEnvVar()) ++newEnvCount; } break; } } } } /* * GUI applications want to run much better when they are started by bash, * so why not? */ char *arguments[4]; arguments[0] = const_cast("/usr/bin/bash"); arguments[1] = const_cast("-c"); std::string args(m_message.path()); for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isArgument()) args += std::string(" '") + i.argument() + "'"; } arguments[2] = const_cast(args.c_str()); arguments[3] = 0; int count = 0; while (environ[count]) ++count; count += newEnvCount; auto envList = new char*[count + 1]; // yap, that's a memleak. :shrug: memset(envList, 0, (count + 1) * sizeof(char*)); copyFilteredEnv(environ, envList); assert(envList[count] == 0); execve(arguments[0], arguments, envList); exit(1); } void Runner::sendUpstream(const char *errorMessage) { char messageBuf[256]; int size = snprintf(messageBuf, sizeof(messageBuf) - 1, "%u %s", m_jailId, errorMessage); messageBuf[size] = '\0'; // notice that on Linux single pipe writes (up to PIPE_BUF, some 4K) are atomic // which protects us from multiple clients messages interacting. write(m_outputFD, messageBuf, size + 1); } bool Runner::runCopy(const std::string &from, const std::filesystem::path &to) const { // the 'from' may include a '*' wildcard, which makes all things a little harder... auto index = from.find('*'); if (index == std::string::npos) { // straight-forward copy std::filesystem::path fromFilePath(from); auto fullTo = to; if (!fullTo.has_filename()) // if the target is a directory, append the src filename fullTo /= fromFilePath.filename(); return copySingle(fromFilePath, fullTo); } auto slashIndex = from.rfind(std::string("/")); if (slashIndex > index) { // TODO error out return false; } assert(index != std::string::npos); // caller should have checked std::filesystem::path path = from.substr(0, slashIndex); std::string pattern = from.substr(slashIndex + 1); pattern = pattern.replace(index - slashIndex, 1, ".*"); std::regex regEx(std::string("/") + pattern + "$"); for (const auto &entry : std::filesystem::directory_iterator(path)) { if (std::regex_search(entry.path().string(), regEx)) { std::filesystem::path source(entry.path()); bool ok = copySingle(source, to / source.filename()); if (!ok) return false; } } return true; } bool Runner::copySingle(const std::filesystem::path &from, const std::filesystem::path &to) const { // std::cout << from.string() << " -> " << to.string() << std::endl; // TODO we copy as root, so this is dangerous! // FIXME check permissions of each of the path elements for the target user. try { if (!std::filesystem::copy_file(from, to, std::filesystem::copy_options::overwrite_existing)) return false; } catch (const std::exception &e) { // std::cout << "Copy instruction failed with: " << e.what() << std::endl; return false; } // stat and apply owner and permissions. std::string fromStr(from.string()); struct stat source; if (lstat(fromStr.c_str(), &source)) { assert(false); return false; } // st mode std::string toStr(to.string()); chmod(toStr.c_str(), source.st_mode); chown(toStr.c_str(), source.st_uid, source.st_gid); return true; } void Runner::copyFilteredEnv(char **from, char **target) { while (*from) { auto equals = strchrnul(*from, '='); const int len = equals - *from; if (len <= 0) continue; bool filtered = false; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if ((i.isNewEnvVar() || i.isEnvVarUnset()) && i.stringLength() >= len && strncmp(i.stringPtr(), *from, len) == 0) { filtered = true; break; } } if (!filtered) { *target = *from; ++target; } ++from; } // finally, append the 'set' properties for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isNewEnvVar()) { *target = i.stringPtr(); ++target; } } } void Runner::mkdirs(const std::filesystem::path &dir) const { std::filesystem::path partial; bool userIdOk = false; bool permissionsOk = false; for (auto chunk = dir.begin(); chunk != dir.end(); ++chunk) { if (partial.empty()) { partial = *chunk; userIdOk = false; } else { partial /= *chunk; struct stat dirStat; if (lstat(partial.string().c_str(), &dirStat) == 0) { userIdOk = dirStat.st_uid == m_ownerUid; permissionsOk = (dirStat.st_mode & 3) == 3; if (!userIdOk && !permissionsOk && ((dirStat.st_mode & 0x30) == 0x30)) { // it is readable to the group, lets check if that includes m_ownerId // ok, we could iterate over all groups, but I'm lazy and we don't need // that today, so this is simpler. permissionsOk = dirStat.st_gid == getgid(); } } else { // does not exist, should we create it? const std::string path = partial.string(); if (!userIdOk && !permissionsOk) { fprintf(stderr, "Runner: mkdir in another user. Refusing [%s]\n", path.c_str()); exit(1); } if (mkdir(path.c_str(), 0700)) { perror("Runner: target-dir mkdir() failed"); fprintf(stderr, " '%s'\n", path.c_str()); exit(1); } chown(path.c_str(), m_ownerUid, getgid()); } } } } int Runner::runInitScript() { int length = 0; const char *script = nullptr; for (auto i = m_message.iBegin(); i.isValid(); i.next()) { if (i.isInitSript()) { length = i.stringLength(); script = i.stringPtr(); break; } } if (length == 0) return 0; std::ofstream scriptFile; constexpr const char *TmpFileName = "/tmp/iso-initscript.sh"; scriptFile.open(TmpFileName); if (!scriptFile.is_open()) { fprintf(stderr, "Failed to create init script in /tmp"); return 1; } scriptFile.write(script, length); scriptFile.flush(); scriptFile.close(); pid_t pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return 1; // if this one failed, the next on by calling method will too. } if (pid) {// we are the parent (pid = child's pid) int status; waitpid(pid, &status, 0); return status; } char *arguments[3]; arguments[0] = const_cast("/usr/bin/bash"); arguments[1] = const_cast(TmpFileName); arguments[2] = 0; execv(arguments[0], arguments); return 0; } int Runner::runEncFs(const char *password, int strlen) const { int aStdinPipe[2]; if (pipe(aStdinPipe) < 0) { perror("allocating pipe for encfs"); return 0; } pid_t pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return 0; } char buf[20]; snprintf(buf, sizeof(buf), "%d", m_jailId); const std::string jailName(buf); char path[2000]; char *curDir = getcwd(path, sizeof(path)); if (curDir == nullptr) { fprintf(stderr, "Runner: runEncFs: path too long\n"); return 0; } const std::string curDirStr(curDir); std::string jaildir = curDirStr + "/" + jailName; if (pid == 0) { // we are the child // redirect stdin to our pipe if (dup2(aStdinPipe[PIPE_READ], STDIN_FILENO) == -1) { exit(errno); } // these are for use by parent only close(aStdinPipe[PIPE_READ]); close(aStdinPipe[PIPE_WRITE]); // close(aStdoutPipe[PIPE_READ]); // close(aStdoutPipe[PIPE_WRITE]); close(STDOUT_FILENO); close(STDERR_FILENO); char *arguments[7]; arguments[0] = const_cast("/usr/bin/encfs"); arguments[1] = const_cast("-f"); // foreground. Don't fork. arguments[2] = const_cast("--public"); arguments[3] = const_cast("--stdinpass"); std::string backend = curDirStr + "/." + jailName; arguments[4] = const_cast(backend.c_str()); arguments[5] = const_cast(jaildir.c_str()); arguments[6] = 0; execv(arguments[0], arguments); } else {// we are the parent (pid = child's pid) close(aStdinPipe[PIPE_READ]); // monitor the process, if it dies we should die too. new std::thread(waitForEncFs, pid); bool ok = false; // next we wait for the target dir to actually become a mount. dev_t oldDevice = 0; for (int i = 0; i < 200; ++i) { // max 15 sec struct stat targetDir; if (stat(jaildir.c_str(), &targetDir)) { printf("stat of encrypted dir failed '%s'\n", jaildir.c_str()); exit(1); } if (i == 0) { // On first loop, send the password to be 100% // certain that the mount hasn't done anything yet. // then remember the device this dir is on and if that // changes then we know that the mount has succeeded. oldDevice = targetDir.st_dev; write(aStdinPipe[PIPE_WRITE], password, strlen); write(aStdinPipe[PIPE_WRITE], "\n", 1); } else if (oldDevice != targetDir.st_dev) { ok = true; break; } std::this_thread::sleep_for(std::chrono::milliseconds(75)); } close(aStdinPipe[PIPE_WRITE]); if (!ok) { printf("EncFS never did its thing. Wrong password?\n"); exit(2); } } /* * We need to kill it later. * Which means we need to have another process that actually has the rights * to kill it we can talk to. * So, a pipe and a simple process that will 'kill' the encfs process later, * on command, it is! */ const auto encFsPid = pid; int encFsWatchdogPipe[2]; if (pipe(encFsWatchdogPipe) < 0) { perror("allocating pipe for encfs"); kill(encFsPid, SIGTERM); return 0; } pid = fork(); if (pid == -1) { fprintf(stderr, "Runner: Failed to fork\n"); return 0; } else if (pid == 0) { close(encFsWatchdogPipe[PIPE_WRITE]); renameThisProcess(m_processName, m_processNameSize, "watchdog-encfs"); char buf[10]; do { auto size = read(encFsWatchdogPipe[PIPE_READ], &buf, sizeof(buf)); if (size < 0) exit(0); if (size >= 2) { kill(encFsPid, SIGTERM); exit(0); } } while(true); } close(encFsWatchdogPipe[PIPE_READ]); return encFsWatchdogPipe[PIPE_WRITE]; } void renameThisProcess(char *nameBlob, int blobSize, const char *newName) { // Maybe more correct is to use prctl(PR_SET_NAME) ? assert(nameBlob); const auto newLength = strlen(newName); if (blobSize > (std::int64_t) newLength) { memcpy(nameBlob, newName, newLength); for (int i = newLength; i < blobSize; ++i) { nameBlob[i] = 0; } } }