parent
							
								
									8832b455f5
								
							
						
					
					
						commit
						29d095cccc
					
				
				 19 changed files with 4011 additions and 0 deletions
			
			
		@ -0,0 +1,8 @@ | 
				
			||||
/* | 
				
			||||
!/**/ | 
				
			||||
!/*.hpp | 
				
			||||
!/*.cpp | 
				
			||||
!/makefile | 
				
			||||
!/.* | 
				
			||||
!/*.code-workspace | 
				
			||||
!/README.md | 
				
			||||
@ -0,0 +1,80 @@ | 
				
			||||
Random Sources | 
				
			||||
============== | 
				
			||||
Playground for random utilities, mostly unprivileged single-user linux namespaces | 
				
			||||
 | 
				
			||||
Binaries | 
				
			||||
-------- | 
				
			||||
 | 
				
			||||
### chome | 
				
			||||
Bind mount a different directory on top of $HOME to (partially) isolate a process | 
				
			||||
 | 
				
			||||
### fakensudo | 
				
			||||
Pretend to be root (uid 0) by running in a single-user namespace mapping one's own UID to 0 | 
				
			||||
 | 
				
			||||
### keepassxc-print | 
				
			||||
Retrieve passwords from KeePassXC on the commandline via the browser interface. | 
				
			||||
 | 
				
			||||
### overlayns | 
				
			||||
Run a command in a custom mount namespace. Like `unshare -mUc` with the added possibility of setting up custom mounts in the namespace before running the target application | 
				
			||||
 | 
				
			||||
### ssh-overlay-kiosk | 
				
			||||
Create an emphemeral home directory for each invocation. | 
				
			||||
 | 
				
			||||
### steamns | 
				
			||||
Isolate steam (and other 32-bit apps) in an unprivileged single-user-namespace "chroot" | 
				
			||||
 | 
				
			||||
Libraries | 
				
			||||
--------- | 
				
			||||
 | 
				
			||||
### keepassxc-browser.hpp | 
				
			||||
Very simple library for interacting with KeePassXC's browser interface from native code | 
				
			||||
 | 
				
			||||
Depends on libsodium, jsoncpp, ko::proc | 
				
			||||
 | 
				
			||||
### ko::fd | 
				
			||||
Convenient wrapper around Linux APIs with dirfd support | 
				
			||||
 | 
				
			||||
kofd\_pipe.hpp adds a class for working with pairs of uni-directional pipes | 
				
			||||
 | 
				
			||||
Depends on ko::fs | 
				
			||||
 | 
				
			||||
### ko::fs | 
				
			||||
Misc. filesystem utilities | 
				
			||||
 | 
				
			||||
- cpath: Type that is trivially convertible to const char\* and from std::string and std::filesystem::path | 
				
			||||
- dir\_ptr: Convenient iterator-based wrapper around the dirent API | 
				
			||||
 | 
				
			||||
### ko::ns | 
				
			||||
Utilities for working with Linux Namespaces (unshare, clone, setns) | 
				
			||||
 | 
				
			||||
Depends on ko::util, ko::fd, ko::os | 
				
			||||
 | 
				
			||||
- ko::ns::idmap: Functions for writing /proc/$$/Xidmap | 
				
			||||
- ko::ns::mount: Functions for setting up mount namespaces | 
				
			||||
- ko::ns::clone: Helpers for spawning processes in new namespaces (kons\_clone.hpp, requires ko::proc) | 
				
			||||
 | 
				
			||||
### ko::os | 
				
			||||
Misc. OS helpers | 
				
			||||
 | 
				
			||||
Depends on ko:: fs | 
				
			||||
 | 
				
			||||
- get\_home() | 
				
			||||
- is\_mountpoint() | 
				
			||||
 | 
				
			||||
### ko::proc | 
				
			||||
Utilities for spawning and managing child processes | 
				
			||||
 | 
				
			||||
Depends on pthread, ko::fd | 
				
			||||
 | 
				
			||||
- popen[p]: Spawn subprocess and communicate via pipes | 
				
			||||
- sync::semapair: Synchronization across processes | 
				
			||||
- child\_ref: Child process reference with cleanup | 
				
			||||
- [s]vclone: Wrappers around linux clone(CLONE\_VM) | 
				
			||||
- simple\_spawn: Trivial fork(); execvp() primitive | 
				
			||||
 | 
				
			||||
### ko::util | 
				
			||||
Misc. utilities | 
				
			||||
 | 
				
			||||
- str: Type-safe-ly concatenate all arguments | 
				
			||||
- cvshort: Short-circuit continuation using C-Style return codes | 
				
			||||
 | 
				
			||||
@ -0,0 +1,141 @@ | 
				
			||||
#include <iostream> | 
				
			||||
#include <filesystem> | 
				
			||||
#include <unordered_set> | 
				
			||||
#include <fstream> | 
				
			||||
 | 
				
			||||
#include "koutil.hpp" | 
				
			||||
#include "kofd.hpp" | 
				
			||||
#include "koos.hpp" | 
				
			||||
 | 
				
			||||
namespace fs = std::filesystem; | 
				
			||||
 | 
				
			||||
void usage(const char *prog) { | 
				
			||||
    std::cout << "Usage: " << prog << " [option...] <newhome> [prog] [arg...]" << std::endl | 
				
			||||
              << "  (c) 2019 Taeyeon Mori" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "  This program allows confining an application to it's own home directory" << std::endl | 
				
			||||
              << "  without chainging the literal home directory path." << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "Options:" << std::endl | 
				
			||||
              << "  -h        Display this help text" << std::endl | 
				
			||||
              << "  -H HOME   Override the home directory path" << std::endl | 
				
			||||
              << "  -w        Don't make / read-only" << std::endl | 
				
			||||
              << "  -W        Preserve working directory" << std::endl | 
				
			||||
              //<< "  -s        Make (the rest of) /home inaccessible" << std::endl
 | 
				
			||||
              //<< "  -S        Make /media and /mnt inaccessible as well (implies -s)" << std::endl
 | 
				
			||||
              //<< "  -x PATH   Make path inaccessible" << std::endl
 | 
				
			||||
              << std::endl | 
				
			||||
              << "Parameters:" << std::endl | 
				
			||||
              << "   newhome  The new home directory path" << std::endl | 
				
			||||
              << "   prog     The executable to run (defaults to $SHELL)" << std::endl | 
				
			||||
              << "   arg...   The executable parameters" << std::endl; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
struct params { | 
				
			||||
    fs::path home, newhome; | 
				
			||||
    bool rw = false, | 
				
			||||
         nohome = false, | 
				
			||||
         nomnt = false, | 
				
			||||
         pwd = true; | 
				
			||||
    std::unordered_set<std::string> hide; | 
				
			||||
    const char *const *argv = nullptr; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
int bindfile(const params &p, fs::path path) { | 
				
			||||
    auto opath = p.home / path; | 
				
			||||
    if (fs::exists(opath)) { | 
				
			||||
        auto npath = p.newhome / path; | 
				
			||||
 | 
				
			||||
        if (!fs::exists(npath)) { | 
				
			||||
            if (fs::is_directory(opath)) | 
				
			||||
                fs::create_directories(npath); | 
				
			||||
            else { | 
				
			||||
                fs::create_directories(npath.parent_path()); | 
				
			||||
                auto touch = std::ofstream(npath); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        if(ko::os::bind(opath, npath, 0)) | 
				
			||||
            return -1; | 
				
			||||
        return ko::os::bind(npath, npath, MS_REMOUNT|MS_RDONLY); | 
				
			||||
    } | 
				
			||||
    return 0; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
int pmain(params p) { | 
				
			||||
    auto uid = getuid(), | 
				
			||||
         gid = getgid(); | 
				
			||||
    auto [e, eloc] = ko::util::cvshort() | 
				
			||||
        .then("unshare",           ::unshare, CLONE_NEWUSER|CLONE_NEWNS) | 
				
			||||
        .then("bind Xauthority",   bindfile, p, ".Xauthority") | 
				
			||||
        .then("bind pulse cookie", bindfile, p, ".config/pulse/cookie") | 
				
			||||
        .then("bind home",         ko::os::bind, p.newhome, p.home, MS_REC) | 
				
			||||
        .ifthen("make / ro",       !p.rw, ko::os::bind, "/", "/", MS_REMOUNT|MS_RDONLY) | 
				
			||||
        .ifthen("chdir",           p.pwd, ::chdir, p.home.c_str()) | 
				
			||||
        .then([uid,gid]() -> ko::util::cvresult { | 
				
			||||
                auto dir = ko::fd::opendir("/proc/self"); | 
				
			||||
                if (!dir) | 
				
			||||
                    return {-1, "open /proc/self"}; | 
				
			||||
                if (!ko::fd::dump("deny", "setgroups", 0644, dir)) | 
				
			||||
                    return {-1, "write setgroups"}; | 
				
			||||
                if (!ko::fd::dump(ko::util::str(gid, " ", gid, " 1\n"), "gid_map", 0644, dir)) | 
				
			||||
                    return {-1, "write gid_map"}; | 
				
			||||
                if (!ko::fd::dump(ko::util::str(uid, " ", uid, " 1\n"), "uid_map", 0644, dir)) | 
				
			||||
                    return {-1, "write uid_map"}; | 
				
			||||
                return {0, nullptr}; | 
				
			||||
                }) | 
				
			||||
        .then("setresgid", ::setresgid, gid, gid, gid) | 
				
			||||
        .then("setresuid", ::setresuid, uid, uid, uid) | 
				
			||||
        .then("exec",      ::execvp, p.argv[0], const_cast<char *const *>(p.argv)); | 
				
			||||
    perror(eloc); | 
				
			||||
    return e; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
int main(int argc, char **argv) { | 
				
			||||
    static const char *exec_argv[] = {getenv("SHELL"), nullptr}; | 
				
			||||
    params p{ | 
				
			||||
        .home = ko::os::get_home(), | 
				
			||||
        .argv = exec_argv | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    constexpr auto spec = "+hH:wsSxW"; | 
				
			||||
 | 
				
			||||
    while (true) { | 
				
			||||
        auto opt = getopt(argc, const_cast<char *const *>(argv), spec); | 
				
			||||
 | 
				
			||||
        if (opt == -1) | 
				
			||||
            break; | 
				
			||||
        else if (opt == '?' || opt == 'h') { | 
				
			||||
            usage(argv[0]); | 
				
			||||
            return opt == 'h' ? 0 : 1; | 
				
			||||
        } | 
				
			||||
        else if (opt == 'H') | 
				
			||||
            p.home = ::optarg; | 
				
			||||
        else if (opt == 'w') | 
				
			||||
            p.rw = true; | 
				
			||||
        else if (opt == 'W') | 
				
			||||
            p.pwd = false; | 
				
			||||
        else if (opt == 's' || opt == 'S') { | 
				
			||||
            p.hide.emplace("/home"); | 
				
			||||
            if (opt == 'S') { | 
				
			||||
                p.hide.emplace("/media"); | 
				
			||||
                p.hide.emplace("/mnt"); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        else if (opt == 'x') | 
				
			||||
            p.hide.emplace(::optarg); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (argc == ::optind) { | 
				
			||||
        std::cout << "Error: missing mandatory newhome argument, see `" << argv[0] << " -h`" << std::endl; | 
				
			||||
        return 2; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    p.newhome = argv[::optind++]; | 
				
			||||
 | 
				
			||||
    if (argc > ::optind) | 
				
			||||
        p.argv = &argv[::optind]; | 
				
			||||
 | 
				
			||||
    return pmain(p); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,187 @@ | 
				
			||||
// Fake sudo using user namespace; Similar to fakeroot
 | 
				
			||||
// (c) 2020 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
 | 
				
			||||
#include "kons.hpp" | 
				
			||||
 | 
				
			||||
#include <cstdlib> | 
				
			||||
#include <iostream> | 
				
			||||
 | 
				
			||||
#include <unistd.h> | 
				
			||||
#include <getopt.h> | 
				
			||||
#include <pwd.h> | 
				
			||||
#include <grp.h> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// Helpers
 | 
				
			||||
int xerror(const char *desc) { | 
				
			||||
    perror(desc); | 
				
			||||
    return -errno; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die(int r, const char *msg) { | 
				
			||||
    std::cerr << msg << std::endl; | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die_errno(int r, const char *msg) { | 
				
			||||
    perror(msg); | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// ========================================================
 | 
				
			||||
// Main
 | 
				
			||||
// ========================================================
 | 
				
			||||
void usage(const char *prog) { | 
				
			||||
    std::cout << "Usage:" << std::endl | 
				
			||||
              << "    " << prog << " -h | -K | -k | -V" << std::endl | 
				
			||||
              << "    " << prog << " -v [-k] [-u user] [-g group]" << std::endl | 
				
			||||
              << "    " << prog << " -e [-k] [-u user] [-g group] [--] file" << std::endl | 
				
			||||
              << "    " << prog << " [-bEHPk] [-u user] [-g group] [-i|-s] [--] command" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "General Options:" << std::endl | 
				
			||||
              << "  -h        Display this help text" << std::endl | 
				
			||||
              << std::endl; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
struct config { | 
				
			||||
    const char *const *exec_argv = nullptr; | 
				
			||||
    bool background = false, | 
				
			||||
         preserve_env = false, | 
				
			||||
         editor = false, | 
				
			||||
         login = false, | 
				
			||||
         set_home = false, | 
				
			||||
         preserve_groups = false, | 
				
			||||
         run_shell = false; | 
				
			||||
    uid_t uid = 0; | 
				
			||||
    gid_t gid = 0; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
template <typename F, typename R, typename T> | 
				
			||||
R get_pwd(F f, R std::remove_pointer_t<std::invoke_result_t<F,T>>::*fld, T nam) { | 
				
			||||
    auto s = f(nam); | 
				
			||||
 | 
				
			||||
    if (!s) | 
				
			||||
        die(20, "Could not resolve user or group"); | 
				
			||||
 | 
				
			||||
    return s->*fld; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Parse commandline arguments
 | 
				
			||||
// returns -1 on success, exit code otherwise
 | 
				
			||||
int parse_cmdline(config &conf, int argc, const char *const *argv) { | 
				
			||||
    constexpr auto spec = "+hbEeg:HiKkPpsu:Vv"; | 
				
			||||
    constexpr option longspec[] = {{"help",0,nullptr,'h'}, | 
				
			||||
                                   {"background",0,nullptr,'b'}, | 
				
			||||
                                   {"preserve-env",2,nullptr,'E'}, | 
				
			||||
                                   {"edit",0,nullptr,'e'}, | 
				
			||||
                                   {"group",1,nullptr,'g'}, | 
				
			||||
                                   {"set-home",0,nullptr,'H'}, | 
				
			||||
                                   {"login",0,nullptr,'i'}, | 
				
			||||
                                   {"remove-timestamp",0,nullptr,'K'}, | 
				
			||||
                                   {"reset-timestamp",0,nullptr,'k'}, | 
				
			||||
                                   {"preserve-groups",0,nullptr,'P'}, | 
				
			||||
                                   {"prompt",1,nullptr,'p'}, | 
				
			||||
                                   {"shell",0,nullptr,'s'}, | 
				
			||||
                                   {"user",1,nullptr,'u'}, | 
				
			||||
                                   {"version",0,nullptr,'V'}, | 
				
			||||
                                   {"validate",0,nullptr,'v'}, | 
				
			||||
                                   {nullptr,0,nullptr,0}}; | 
				
			||||
 | 
				
			||||
    while (true) { | 
				
			||||
        auto opt = getopt_long(argc, const_cast<char *const *>(argv), spec, longspec, nullptr); | 
				
			||||
 | 
				
			||||
        if (opt == -1) | 
				
			||||
            break; | 
				
			||||
        else if (opt == '?' || opt == 'h') { | 
				
			||||
            usage(argv[0]); | 
				
			||||
            return opt == 'h' ? 0 : 1; | 
				
			||||
        } | 
				
			||||
        else if (opt == 'V') { | 
				
			||||
            std::cout << "fakensudo Namespace fake sudo version 0.1" << std::endl | 
				
			||||
                      << "(c) 2020 Taeyeon Mori" << std::endl; | 
				
			||||
            return 0; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        else if (opt == 'b') conf.background = true; | 
				
			||||
        else if (opt == 'E') conf.preserve_env = true; // XXX: ignores the optinal list
 | 
				
			||||
        else if (opt == 'e') conf.editor = true; | 
				
			||||
        else if (opt == 'g') conf.gid = get_pwd(getgrnam, &group::gr_gid, optarg); | 
				
			||||
        else if (opt == 'H') conf.set_home = true; | 
				
			||||
        else if (opt == 'i') conf.login = true; | 
				
			||||
        else if (opt == 'K') return 0; // XXX: check for clashes
 | 
				
			||||
        else if (opt == 'k') /* pass */; | 
				
			||||
        else if (opt == 'P') conf.preserve_groups = true; | 
				
			||||
        else if (opt == 'p') /* pass */; | 
				
			||||
        else if (opt == 's') conf.run_shell = true; | 
				
			||||
        else if (opt == 'u') conf.uid = get_pwd(getpwnam, &passwd::pw_uid, optarg); | 
				
			||||
        else if (opt == 'v') return 0; // XXX: properly check options
 | 
				
			||||
        else die(10, "Unknown option encountered"); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Check sanity
 | 
				
			||||
    bool good = true; | 
				
			||||
 | 
				
			||||
    if (conf.run_shell || conf.login) { | 
				
			||||
        if (conf.run_shell && conf.login) | 
				
			||||
            good = false; | 
				
			||||
        if (conf.editor) | 
				
			||||
            good = false; | 
				
			||||
    } else if (::optind >= argc) | 
				
			||||
        good = false; | 
				
			||||
 | 
				
			||||
    if (!good) { | 
				
			||||
        usage(argv[0]); | 
				
			||||
        return 5; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Rest is child cmnd
 | 
				
			||||
    if (argc > ::optind) | 
				
			||||
        conf.exec_argv = &argv[::optind]; | 
				
			||||
 | 
				
			||||
    return -1; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
int main(int argc, char **argv) { | 
				
			||||
    // Set defaults
 | 
				
			||||
    auto conf = config{}; | 
				
			||||
 | 
				
			||||
    // Parse commandline
 | 
				
			||||
    auto perr = parse_cmdline(conf, argc, argv); | 
				
			||||
    if (perr != -1) | 
				
			||||
        return perr; | 
				
			||||
 | 
				
			||||
    auto uerr = ko::ns::unshare_single(conf.uid, conf.gid, CLONE_NEWUSER); | 
				
			||||
    if (uerr != 0) | 
				
			||||
        die_errno(31, "unshare"); | 
				
			||||
 | 
				
			||||
    // Drop Permissions
 | 
				
			||||
    setresgid(conf.gid, conf.gid, conf.gid); | 
				
			||||
    setresuid(conf.uid, conf.uid, conf.uid); | 
				
			||||
 | 
				
			||||
    auto exec_argv = conf.exec_argv; | 
				
			||||
    if (conf.run_shell) { | 
				
			||||
        auto shell = getenv("SHELL"); | 
				
			||||
        if (shell == nullptr) | 
				
			||||
            die(41, "Could not get SHELL from environment"); | 
				
			||||
        if (conf.exec_argv == nullptr || *conf.exec_argv == nullptr) | 
				
			||||
            exec_argv = new char*[]{shell, nullptr}; | 
				
			||||
        else | 
				
			||||
            die(200, "-s not fully implemented"); | 
				
			||||
    } else if (conf.login) { | 
				
			||||
        auto shell = get_pwd(getpwuid, &passwd::pw_shell, conf.uid); | 
				
			||||
        if (shell == nullptr) | 
				
			||||
            die(41, "Could not get SHELL from passwd record"); | 
				
			||||
        if (conf.exec_argv == nullptr || *conf.exec_argv == nullptr) | 
				
			||||
            exec_argv = new char*[]{shell, "-l", nullptr}; | 
				
			||||
        else | 
				
			||||
            die(200, "-i not fully implemented"); | 
				
			||||
    } else if (conf.editor) { | 
				
			||||
        die(200, "-e not implemented"); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Exec
 | 
				
			||||
    execvpe(exec_argv[0], const_cast<char *const*>(exec_argv), environ); | 
				
			||||
    die_errno(33, "exec"); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,429 @@ | 
				
			||||
// Simple Client-Library for the KeePassXC-Browser API
 | 
				
			||||
// (c) 2019 Taeyeon Mori
 | 
				
			||||
//
 | 
				
			||||
// Depends on: libsodium, jsoncpp
 | 
				
			||||
//
 | 
				
			||||
// NOTE: Users must make sure to initialize libsodium!
 | 
				
			||||
// WARNING: This currently does nothing to protect the keys in memory.
 | 
				
			||||
//          Such measures could be added to crypto::, but as the key material
 | 
				
			||||
//          is stored in a plain file on disk anyway, that seems to be a lot of useless work.
 | 
				
			||||
//          This applies especially for small, short-lived cli utilities.
 | 
				
			||||
// WARNING: With a plain secrets file, the 'Never ask before accessing credentials' option in
 | 
				
			||||
//          in KeePassXC becomes an even bigger security risk!
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "koproc.hpp" | 
				
			||||
 | 
				
			||||
#include <json/json.h> | 
				
			||||
#include <sodium.h> | 
				
			||||
 | 
				
			||||
#include <string> | 
				
			||||
#include <iostream> | 
				
			||||
#include <sstream> | 
				
			||||
#include <optional> | 
				
			||||
#include <utility> | 
				
			||||
#include <memory> | 
				
			||||
#include <cstring> | 
				
			||||
#include <variant> | 
				
			||||
#include <unordered_map> | 
				
			||||
 | 
				
			||||
#include <unistd.h> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace keepassxc { | 
				
			||||
    using string = std::string; | 
				
			||||
    using data = std::basic_string<uint8_t>; | 
				
			||||
    
 | 
				
			||||
    // Hack, but libsodium insists on unsigned char
 | 
				
			||||
    // The result of this is cleaner than having individual
 | 
				
			||||
    // casts all over the place and as a side benefit, it
 | 
				
			||||
    // tends to prevent toughtlessly trying to put binary
 | 
				
			||||
    // data into json directly.
 | 
				
			||||
    const data &data_cast(const string &s) { | 
				
			||||
        return *reinterpret_cast<const data*>(&s); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    const string &nodata_cast(const data &d) { | 
				
			||||
        return *reinterpret_cast<const string*>(&d); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Cryptography goes here | 
				
			||||
     */ | 
				
			||||
    namespace crypto { | 
				
			||||
        data generate_nonce() { | 
				
			||||
            auto nonce = data(crypto_box_NONCEBYTES, 0); | 
				
			||||
            ::randombytes(nonce.data(), crypto_box_NONCEBYTES); | 
				
			||||
            return nonce; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        /// Return [[public_key, secret_key]]
 | 
				
			||||
        std::optional<std::pair<data, data>> generate_keypair() { | 
				
			||||
            auto seckey = data(crypto_box_SECRETKEYBYTES, 0); | 
				
			||||
            auto pubkey = data(crypto_box_PUBLICKEYBYTES, 0); | 
				
			||||
            if (::crypto_box_keypair(pubkey.data(), seckey.data()) == 0) | 
				
			||||
                return {{pubkey, seckey}}; | 
				
			||||
            return {}; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        data encrypt(const data &plain, const data &nonce, const data &pubkey, const data &seckey) { | 
				
			||||
            auto cipher = data(plain.size() + crypto_box_MACBYTES, 0); | 
				
			||||
            const auto ok = crypto_box_easy(cipher.data(), plain.data(), plain.size(), nonce.data(), pubkey.data(), seckey.data()) == 0; | 
				
			||||
            return ok ? cipher : data(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        data decrypt(const data &cipher, const data &nonce, const data &pubkey, const data &seckey) { | 
				
			||||
            auto plain = data(cipher.size() - crypto_box_MACBYTES, 0); | 
				
			||||
            const auto ok = crypto_box_open_easy(plain.data(), cipher.data(), cipher.size(), nonce.data(), pubkey.data(), seckey.data()) == 0; | 
				
			||||
            return ok ? plain : data(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        string b64encode(const data &dec) { | 
				
			||||
            auto enc = string(sodium_base64_ENCODED_LEN(dec.size(), sodium_base64_VARIANT_ORIGINAL), 0); | 
				
			||||
            ::sodium_bin2base64(enc.data(), enc.size(), dec.data(), dec.size(), sodium_base64_VARIANT_ORIGINAL); | 
				
			||||
            enc.resize(enc.find_first_of('\0')); | 
				
			||||
            return enc; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        std::optional<data> b64decode(const string &enc) { | 
				
			||||
            auto dec = data(enc.size() * 3 / 4 + 1, 0); | 
				
			||||
            size_t data_len = 0; | 
				
			||||
            if (::sodium_base642bin(dec.data(), dec.size(), enc.data(), enc.size(), | 
				
			||||
                        nullptr, &data_len, nullptr, sodium_base64_VARIANT_ORIGINAL) == 0) { | 
				
			||||
                dec.resize(data_len); | 
				
			||||
                return dec; | 
				
			||||
            } | 
				
			||||
            return {}; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        void increment(data &n) { | 
				
			||||
            ::sodium_increment(n.data(), n.size()); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    /**
 | 
				
			||||
     * The keepassxc client configuration | 
				
			||||
     */ | 
				
			||||
    struct config { | 
				
			||||
        static constexpr auto CONF_PUBKEY = "public_key", | 
				
			||||
                              CONF_PRIVKEY = "private_key", | 
				
			||||
                              CONF_DATABASES = "databases"; | 
				
			||||
 | 
				
			||||
        data public_key, private_key; | 
				
			||||
        std::unordered_map<string, string> dbs; | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Create a new configuration | 
				
			||||
         * @note This creates the persistent key pair | 
				
			||||
         */ | 
				
			||||
        static std::optional<config> create() { | 
				
			||||
            auto keys = crypto::generate_keypair(); | 
				
			||||
            if (!keys) | 
				
			||||
                return {}; | 
				
			||||
            auto [public_key, private_key] = keys.value(); | 
				
			||||
            return config{ | 
				
			||||
                .public_key = public_key, | 
				
			||||
                .private_key = private_key, | 
				
			||||
                .dbs = {}, | 
				
			||||
            }; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Load configuration from a JSON object | 
				
			||||
         * @param conf The JSON object | 
				
			||||
         */ | 
				
			||||
        static std::optional<config> load(const Json::Value &conf) { | 
				
			||||
            if (!conf.isMember(CONF_PUBKEY) || !conf.isMember(CONF_PRIVKEY)) | 
				
			||||
                return std::nullopt; | 
				
			||||
 | 
				
			||||
            auto public_key = crypto::b64decode(conf[CONF_PUBKEY].asString()); | 
				
			||||
            if (!public_key) | 
				
			||||
                return std::nullopt; | 
				
			||||
            
 | 
				
			||||
            auto private_key = crypto::b64decode(conf[CONF_PRIVKEY].asString()); | 
				
			||||
            if (!private_key) | 
				
			||||
                return std::nullopt; | 
				
			||||
 | 
				
			||||
            return config{ | 
				
			||||
                .public_key = public_key.value(), | 
				
			||||
                .private_key = private_key.value(), | 
				
			||||
                .dbs = [&conf]() { | 
				
			||||
                    auto ids = std::unordered_map<string, string>{}; | 
				
			||||
                    
 | 
				
			||||
                    if (!conf.isMember(CONF_DATABASES)) | 
				
			||||
                        return ids; | 
				
			||||
 | 
				
			||||
                    for (auto it = conf[CONF_DATABASES].begin(); it != conf[CONF_DATABASES].end(); it++) | 
				
			||||
                        ids.emplace(it.name(), it->asString()); | 
				
			||||
                    
 | 
				
			||||
                    return ids; | 
				
			||||
                }(), | 
				
			||||
            }; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Write the configuration into a JSON object | 
				
			||||
         */ | 
				
			||||
        void serialize(Json::Value &conf) const { | 
				
			||||
            conf[CONF_PUBKEY] = crypto::b64encode(this->public_key); | 
				
			||||
            conf[CONF_PRIVKEY] = crypto::b64encode(this->private_key); | 
				
			||||
            conf[CONF_DATABASES] = Json::objectValue; | 
				
			||||
            for (auto [dbhash, id] : this->dbs) | 
				
			||||
                conf[CONF_DATABASES][dbhash] = id; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        /**
 | 
				
			||||
         * Dump the configuration as a JSON object | 
				
			||||
         */ | 
				
			||||
        Json::Value serialize() const { | 
				
			||||
            Json::Value json(Json::objectValue); | 
				
			||||
            serialize(json); | 
				
			||||
            return json; | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Simple, blocking client for interacting with KeePassXC | 
				
			||||
     */ | 
				
			||||
    class client { | 
				
			||||
        config conf; | 
				
			||||
 | 
				
			||||
        data conn_pubkey = {}, | 
				
			||||
             conn_privkey = {}, | 
				
			||||
             remote_pubkey = {}; | 
				
			||||
        string conn_id = {}, | 
				
			||||
               remote_dbhash = {}; | 
				
			||||
 | 
				
			||||
        pid_t pid = -1; | 
				
			||||
        std::unique_ptr<ko::fd::pipe> pipe = {}; | 
				
			||||
 | 
				
			||||
        std::array<const char *, 2> proc_cmd = {"keepassxc-proxy", nullptr}; | 
				
			||||
 | 
				
			||||
        const std::unique_ptr<Json::StreamWriter> dumper{[](){ | 
				
			||||
            auto builder = Json::StreamWriterBuilder(); | 
				
			||||
            builder["indentation"] = ""; | 
				
			||||
            return builder.newStreamWriter(); | 
				
			||||
        }()}; | 
				
			||||
        const std::unique_ptr<Json::CharReader> loader{Json::CharReaderBuilder().newCharReader()}; | 
				
			||||
        
 | 
				
			||||
        inline string dumps(const Json::Value &v) { | 
				
			||||
            auto s = std::ostringstream(); | 
				
			||||
            this->dumper->write(v, &s); | 
				
			||||
            return s.str(); | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        inline std::variant<Json::Value, string> loads(const string &json) { | 
				
			||||
            auto v = Json::Value(); | 
				
			||||
            auto err = std::string(); | 
				
			||||
            if (this->loader->parse(json.data(), json.data() + json.size(), &v, &err)) | 
				
			||||
                return v; | 
				
			||||
            return err; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
    public: | 
				
			||||
        client(config conf) : | 
				
			||||
            conf(conf) | 
				
			||||
        {} | 
				
			||||
        
 | 
				
			||||
        const config &get_config() const { | 
				
			||||
            return this->conf; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        void set_command(const char *cmd) { | 
				
			||||
            this->proc_cmd[0] = cmd; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        bool is_connected() { | 
				
			||||
            return this->pid > 0 && this->pipe; // XXX check pipe is connected
 | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        bool is_associated() { | 
				
			||||
            return !this->remote_pubkey.empty() && !this->remote_dbhash.empty(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        /**
 | 
				
			||||
         * Start the KeePassXC process | 
				
			||||
         * @note This generates necessary ephemeral keys and ids | 
				
			||||
         * XXX Should move the key pair into associate()? | 
				
			||||
         */ | 
				
			||||
        bool connect() { | 
				
			||||
            auto keys_opt = crypto::generate_keypair(); | 
				
			||||
            if (!keys_opt) | 
				
			||||
                return false; | 
				
			||||
 | 
				
			||||
            std::tie(this->conn_pubkey, this->conn_privkey) = keys_opt.value(); | 
				
			||||
            std::tie(this->pid, this->pipe) = ko::proc::popenp(this->proc_cmd.data()); | 
				
			||||
            
 | 
				
			||||
            this->conn_id = crypto::b64encode(crypto::generate_nonce()); | 
				
			||||
 | 
				
			||||
            return is_connected(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value jerror(string reason) { | 
				
			||||
            auto err = Json::Value{Json::objectValue}; | 
				
			||||
            err["action"] = "client-error"; | 
				
			||||
            err["success"] = "false"; | 
				
			||||
            err["errorCode"] = -1; | 
				
			||||
            err["error"] = reason; | 
				
			||||
            return err; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_message(const Json::Value &msg) { | 
				
			||||
            auto &pipe = *(this->pipe); | 
				
			||||
            auto msg_s = this->dumps(msg); | 
				
			||||
 | 
				
			||||
            pipe.write_bin<uint32_t>(msg_s.size()); | 
				
			||||
            pipe.write(msg_s); | 
				
			||||
 | 
				
			||||
            auto sz_opt = pipe.read_bin<uint32_t>(); | 
				
			||||
            if (!sz_opt) | 
				
			||||
                return jerror(string{"Could not read result size: "} + strerror(errno)); | 
				
			||||
 | 
				
			||||
            auto reply = pipe.read(sz_opt.value()); | 
				
			||||
            if (reply.size() < sz_opt.value()) | 
				
			||||
                return jerror(string{"Could not read result: "} + strerror(errno)); | 
				
			||||
 | 
				
			||||
            auto result_err = this->loads(reply); | 
				
			||||
            if (result_err.index()) | 
				
			||||
                return jerror(string{"Could not parse message: "} + std::get<string>(result_err)); | 
				
			||||
 | 
				
			||||
            //std::cerr << "Conversation: " << msg_s << " -> (" << sz_opt.value() << ") " << reply << std::endl;
 | 
				
			||||
 | 
				
			||||
            return std::get<0>(result_err); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_message_enc(const Json::Value &msg) { | 
				
			||||
            auto nonce = crypto::generate_nonce(); | 
				
			||||
            auto msg_enc = crypto::encrypt(data_cast(this->dumps(msg)), nonce, this->remote_pubkey, this->conn_privkey); | 
				
			||||
 | 
				
			||||
            auto wrap = Json::Value(Json::objectValue); | 
				
			||||
            wrap["action"] = msg["action"]; | 
				
			||||
            wrap["nonce"] = crypto::b64encode(nonce); | 
				
			||||
            wrap["clientID"] = this->conn_id; | 
				
			||||
            wrap["message"] = crypto::b64encode(msg_enc); | 
				
			||||
 | 
				
			||||
            auto res = this->send_message(wrap); | 
				
			||||
            
 | 
				
			||||
            if (res.isMember("error")) | 
				
			||||
                return res; | 
				
			||||
 | 
				
			||||
            crypto::increment(nonce); | 
				
			||||
            if (res.get("nonce", "").asString() != crypto::b64encode(nonce)) | 
				
			||||
                return this->jerror("Invalid response nonce"); | 
				
			||||
 | 
				
			||||
            auto cipher_opt = crypto::b64decode(res["message"].asString()); | 
				
			||||
            if (!cipher_opt) | 
				
			||||
                return this->jerror("Malformed ciphertext"); | 
				
			||||
            
 | 
				
			||||
            auto data = crypto::decrypt(cipher_opt.value(), nonce, this->remote_pubkey, this->conn_privkey); | 
				
			||||
            auto result_err = this->loads(nodata_cast(data)); | 
				
			||||
            if (result_err.index()) | 
				
			||||
                return this->jerror(string{"Could not parse inner message: "} + std::get<1>(result_err)); | 
				
			||||
 | 
				
			||||
            return std::get<0>(result_err); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // ----------------------------------------------------------
 | 
				
			||||
        // Message types
 | 
				
			||||
        inline Json::Value msg_skeleton(const string &action) { | 
				
			||||
            auto msg = Json::Value{Json::objectValue}; | 
				
			||||
            msg["action"] = action; | 
				
			||||
            return msg; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_change_public_keys() { | 
				
			||||
            auto msg = this->msg_skeleton("change-public-keys"); | 
				
			||||
            msg["publicKey"] = crypto::b64encode(this->conn_pubkey); | 
				
			||||
            msg["clientID"] = this->conn_id; | 
				
			||||
            msg["nonce"] = crypto::b64encode(crypto::generate_nonce()); | 
				
			||||
            return this->send_message(msg); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_get_databasehash() { | 
				
			||||
            return this->send_message_enc(this->msg_skeleton("get-databasehash")); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_associate() { | 
				
			||||
            auto msg = this->msg_skeleton("associate"); | 
				
			||||
            msg["key"] = crypto::b64encode(this->conn_pubkey); | 
				
			||||
            msg["idKey"] = crypto::b64encode(this->conf.public_key); | 
				
			||||
            return this->send_message_enc(msg); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_test_associate(const string &id) { | 
				
			||||
            auto msg = this->msg_skeleton("test-associate"); | 
				
			||||
            msg["key"] = crypto::b64encode(this->conf.public_key); | 
				
			||||
            msg["id"] = id; | 
				
			||||
            return this->send_message_enc(msg); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        Json::Value send_get_logins(const string &url, const string &submitUrl=string(), bool httpAuth=false) { | 
				
			||||
            auto msg = this->msg_skeleton("get-logins"); | 
				
			||||
            msg["url"] = url; | 
				
			||||
            if (!submitUrl.empty()) | 
				
			||||
                msg["submitUrl"] = submitUrl; | 
				
			||||
            if (httpAuth) | 
				
			||||
                msg["httpAuth"] = httpAuth; | 
				
			||||
            msg["keys"] = Json::Value{Json::arrayValue}; | 
				
			||||
            msg["keys"][0] = Json::Value{Json::objectValue}; | 
				
			||||
            msg["keys"][0]["id"] = crypto::b64encode(this->conf.public_key); | 
				
			||||
            msg["keys"][0]["key"] = crypto::b64encode(this->conn_pubkey); | 
				
			||||
            return this->send_message_enc(msg); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // ----------------------------------------------------------
 | 
				
			||||
        // Composite functions
 | 
				
			||||
        /**
 | 
				
			||||
         * Try to associate with KeePassXC using existing IDs | 
				
			||||
         * @return A non-empty error message on failure | 
				
			||||
         */ | 
				
			||||
        string try_associate() { | 
				
			||||
            // Exchange pubkeys
 | 
				
			||||
            auto res = this->send_change_public_keys(); | 
				
			||||
            if (res.isMember("error")) | 
				
			||||
                return res["error"].asString(); | 
				
			||||
 | 
				
			||||
            if (!res.isMember("publicKey")) | 
				
			||||
                return "publicKey not in change-public-keys reply"; | 
				
			||||
            this->remote_pubkey = crypto::b64decode(res["publicKey"].asString()).value(); | 
				
			||||
 | 
				
			||||
            // Get the dbhash
 | 
				
			||||
            res = this->send_get_databasehash(); | 
				
			||||
            if (res.isMember("error")) | 
				
			||||
                return res["error"].asString(); | 
				
			||||
            this->remote_dbhash = res["hash"].asString(); | 
				
			||||
            
 | 
				
			||||
            // Look up database
 | 
				
			||||
            auto f = conf.dbs.find(this->remote_dbhash); | 
				
			||||
            if (f == conf.dbs.end()) | 
				
			||||
                return "Not associated with database"; | 
				
			||||
            
 | 
				
			||||
            // Verify association
 | 
				
			||||
            res = this->send_test_associate(f->second); | 
				
			||||
            if (res.get("success", "false") != "true") | 
				
			||||
                return "Key appears to have been revoked"; | 
				
			||||
            return {}; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        /**
 | 
				
			||||
         * Try to associate with KeePassXC using either existing or new IDs | 
				
			||||
         * @return A non-empty error message on failure | 
				
			||||
         */ | 
				
			||||
        string associate() { | 
				
			||||
            auto err = try_associate(); | 
				
			||||
            if (err.empty()) | 
				
			||||
                return {}; | 
				
			||||
 | 
				
			||||
            auto res = this->send_associate(); | 
				
			||||
            if (res.isMember("error")) | 
				
			||||
                return res["error"].asString(); | 
				
			||||
            if (res.get("success", "false") != "true") | 
				
			||||
                return "Unknown error"; | 
				
			||||
 | 
				
			||||
            this->conf.dbs.emplace(this->remote_dbhash, res["id"].asString()); | 
				
			||||
            return {}; | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,68 @@ | 
				
			||||
#include "keepassxc-browser.hpp" | 
				
			||||
 | 
				
			||||
#include <fstream> | 
				
			||||
#include <filesystem> | 
				
			||||
#include <cstdlib> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace fs = std::filesystem; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
template <typename... Args> | 
				
			||||
void die(int code, Args... msg) { | 
				
			||||
    (std::cerr << ... << msg) << std::endl; | 
				
			||||
    exit(code); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
int main(int argc, char **argv) { | 
				
			||||
    if (::sodium_init() < 0) | 
				
			||||
        die(-44, "Error: Could not initialize libsodium"); | 
				
			||||
 | 
				
			||||
    if (argc < 2) | 
				
			||||
        die(-1, "Usage: ", argv[0], " <url>"); | 
				
			||||
 | 
				
			||||
    // Try to make the cli emulate pass at some point
 | 
				
			||||
    auto config_path = fs::path{getenv("HOME")} / ".config/keepassxc-pass.json"; | 
				
			||||
 | 
				
			||||
    auto conf = [&config_path]() { | 
				
			||||
        if (!fs::exists(config_path)) { | 
				
			||||
            auto opt = keepassxc::config::create(); | 
				
			||||
            if (!opt) | 
				
			||||
                die(-6, "Error: Could not initialize secrets"); | 
				
			||||
            return opt.value(); | 
				
			||||
        } else { | 
				
			||||
            auto s = std::ifstream(config_path); | 
				
			||||
            auto v = Json::Value{}; | 
				
			||||
            s >> v; | 
				
			||||
            auto opt = keepassxc::config::load(v); | 
				
			||||
            if (!opt) | 
				
			||||
                die(-5, "Error: Could not load secrets from config"); | 
				
			||||
            return opt.value(); | 
				
			||||
        } | 
				
			||||
    }(); | 
				
			||||
 | 
				
			||||
    auto client = keepassxc::client(conf); | 
				
			||||
 | 
				
			||||
    if (!client.connect()) | 
				
			||||
        die(-2, "Error: Could not popen keepass"); | 
				
			||||
    
 | 
				
			||||
    // Hide new association behind a flag?
 | 
				
			||||
    auto err = client.associate(); | 
				
			||||
    if (!err.empty()) | 
				
			||||
        die(-3, "Error: Could not associate with keepass: ", err); | 
				
			||||
 | 
				
			||||
    auto s = std::ofstream(config_path); | 
				
			||||
    s << client.get_config().serialize(); | 
				
			||||
    s.close(); | 
				
			||||
    fs::permissions(config_path, fs::perms::owner_read|fs::perms::owner_write); | 
				
			||||
 | 
				
			||||
    auto res = client.send_get_logins(argv[1]); | 
				
			||||
    if (res["success"] != "true") | 
				
			||||
        die(-4, "Error: Could not get logins: ", res["error"].asString()); | 
				
			||||
 | 
				
			||||
    if (res["count"] == "0") | 
				
			||||
        die(1, "No logins found"); | 
				
			||||
 | 
				
			||||
    std::cout << res["entries"][0]["password"].asString() << std::endl; | 
				
			||||
    return 0; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,692 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// kofd.hpp
 | 
				
			||||
// ko::fd
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// File descriptor functions
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "kofs.hpp" | 
				
			||||
 | 
				
			||||
#include <fcntl.h> | 
				
			||||
#include <sys/stat.h> | 
				
			||||
#include <sys/types.h> | 
				
			||||
#include <sys/sendfile.h> | 
				
			||||
#include <sys/ioctl.h> | 
				
			||||
#include <sys/mount.h> // linux/fs.h includes linux/mount.h which overrides some of the things from sys/mount.h | 
				
			||||
#include <linux/fs.h> | 
				
			||||
#include <unistd.h> | 
				
			||||
 | 
				
			||||
#include <cstring> | 
				
			||||
#include <string> | 
				
			||||
#include <utility> | 
				
			||||
#include <optional> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// ==================================================================
 | 
				
			||||
namespace ko::fd { | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Working with file descriptors
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Auto-close move-only filedescriptor wrapper | 
				
			||||
 */ | 
				
			||||
class fd { | 
				
			||||
    int _fd; | 
				
			||||
 | 
				
			||||
public: | 
				
			||||
    fd() : | 
				
			||||
        _fd(-1) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    fd(int fd) : | 
				
			||||
        _fd(fd) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    fd(fd const &) = delete; | 
				
			||||
 | 
				
			||||
    fd(fd &&o) : | 
				
			||||
        _fd(o.move()) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    fd &operator=(int fd) { | 
				
			||||
        if (_fd >= 0) | 
				
			||||
            ::close(_fd); | 
				
			||||
        _fd = fd; | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    fd &operator=(fd &&o) { | 
				
			||||
        if (_fd >= 0) | 
				
			||||
            ::close(_fd); | 
				
			||||
        _fd = o.move(); | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    ~fd() { | 
				
			||||
        if (_fd >= 0) | 
				
			||||
            ::close(_fd); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Boolean operator | 
				
			||||
     * @note This differs from a raw int fd | 
				
			||||
     */ | 
				
			||||
    operator bool() const { | 
				
			||||
        return _fd >= 0; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Negation operator | 
				
			||||
     * @note This differs from a raw int fd | 
				
			||||
     */ | 
				
			||||
    bool operator !() const { | 
				
			||||
        return _fd < 0; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Comparison
 | 
				
			||||
    bool operator ==(int i) const { | 
				
			||||
        return _fd == i; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    bool operator !=(int i) const{ | 
				
			||||
        return _fd != i; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    bool operator <(int i) const { | 
				
			||||
        return _fd < i; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    bool operator >(int i) const { | 
				
			||||
        return _fd > i; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    bool operator <=(int i) const { | 
				
			||||
        return _fd <= i; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    bool operator >=(int i) const { | 
				
			||||
        return _fd >= i; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Get the raw int fd | 
				
			||||
     * @note This is not allowed on temporaries | 
				
			||||
     * @note Use move() instead to transfer ownership. | 
				
			||||
     * @see move() | 
				
			||||
     */ | 
				
			||||
    operator int() & { | 
				
			||||
        return _fd; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /**
 | 
				
			||||
     * Disown this object | 
				
			||||
     * @note 
 | 
				
			||||
     */ | 
				
			||||
    int move() { | 
				
			||||
        auto tmp = _fd; | 
				
			||||
        _fd = -1; | 
				
			||||
        return tmp; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    /**
 | 
				
			||||
     * Close the file descriptor early | 
				
			||||
     */ | 
				
			||||
    bool close() { | 
				
			||||
        if (_fd < 0) return false; | 
				
			||||
        if (::close(_fd) && errno != EBADF) return false; | 
				
			||||
        _fd = -1; | 
				
			||||
        return true; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    /**
 | 
				
			||||
     * Copy the file descriptor | 
				
			||||
     */ | 
				
			||||
    fd dup() { | 
				
			||||
        return ::dup(_fd); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
//-------------------------------------------------------------------
 | 
				
			||||
// Opening file descriptors
 | 
				
			||||
// @{
 | 
				
			||||
/**
 | 
				
			||||
 * Open a file descriptor | 
				
			||||
 * @param path The path | 
				
			||||
 * @param flags The open(2) flags | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @param cloexec Add O_CLOEXEC to \p flags | 
				
			||||
 * @return A \c fd file descriptor | 
				
			||||
 */ | 
				
			||||
fd open(const fs::cpath &path, long flags, int dirfd=AT_FDCWD, bool cloexec=true) { | 
				
			||||
    return ::openat(dirfd, path, flags | (cloexec ? O_CLOEXEC : 0)); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Open a file descriptor, creating the file if it doesn't exist | 
				
			||||
 * @param path The path | 
				
			||||
 * @param flags The open(2) flags | 
				
			||||
 * @param mode The file mode to create with | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @param cloexec Add O_CLOEXEC to \p flags | 
				
			||||
 * @return A \c fd file descriptor | 
				
			||||
 */ | 
				
			||||
fd open_creat(const fs::cpath &path, long flags, mode_t mode, int dirfd=AT_FDCWD, bool cloexec=true) { | 
				
			||||
    return ::openat(dirfd, path, O_CREAT | flags | (cloexec ? O_CLOEXEC : 0), mode); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Open a directory file descriptor | 
				
			||||
 * @param path The directory path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return A \c fd directory file descriptor | 
				
			||||
 */ | 
				
			||||
fd opendir(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::openat(dirfd, path, O_DIRECTORY|O_RDONLY|O_CLOEXEC); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Open a directory file descriptor with custom flags | 
				
			||||
 * @param path The directory path | 
				
			||||
 * @param flags The flags to pass to open(2) | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return A directory \c fd | 
				
			||||
 */ | 
				
			||||
fd opendir2(const fs::cpath &path, long flags, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::openat(dirfd, path, flags|O_DIRECTORY); | 
				
			||||
} | 
				
			||||
// @}
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
//-------------------------------------------------------------------
 | 
				
			||||
// Checking properties
 | 
				
			||||
// @{
 | 
				
			||||
/**
 | 
				
			||||
 * Check if a path exists | 
				
			||||
 * @param path The path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return true if path exists | 
				
			||||
 */ | 
				
			||||
bool exists(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    return !::faccessat(dirfd, path, F_OK, 0); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Check if a path is a directory | 
				
			||||
 * @param path The path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return true if path is a directory | 
				
			||||
 */ | 
				
			||||
bool is_dir(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    struct stat st; | 
				
			||||
    if (::fstatat(dirfd, path, &st, 0)) | 
				
			||||
        return false; | 
				
			||||
    return S_ISDIR(st.st_mode); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Read the target of a symbolic link | 
				
			||||
 * @param path The symlink path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return A fs::path. It is empty on error | 
				
			||||
 */ | 
				
			||||
fs::path readlink(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    constexpr auto static_bufsize = 4096; | 
				
			||||
    char buf[static_bufsize]; | 
				
			||||
    auto sz = ::readlinkat(dirfd, path, buf, static_bufsize); | 
				
			||||
    if (sz < 0) | 
				
			||||
        return {}; | 
				
			||||
    if (sz < static_bufsize) | 
				
			||||
        return {buf, buf + sz}; | 
				
			||||
    
 | 
				
			||||
    struct stat st; | 
				
			||||
    if (::fstatat(dirfd, path, &st, AT_SYMLINK_NOFOLLOW)) | 
				
			||||
        return {}; | 
				
			||||
    
 | 
				
			||||
    auto extbuf = std::make_unique<char[]>(st.st_size); | 
				
			||||
    sz = ::readlinkat(dirfd, path, extbuf.get(), sz); | 
				
			||||
    if (sz < 0) | 
				
			||||
        return {}; | 
				
			||||
    return {&extbuf[0], &extbuf[sz]}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Get the target if a file is a symbolic link or return the path as-is if it is something else | 
				
			||||
 * @param path The path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @param notexist_ok Whether or not to return the path as-is if it doesn't exist (default=true) | 
				
			||||
 * @return A fs::path, possibly relative to dirfd. It may be empty on error | 
				
			||||
 */ | 
				
			||||
fs::path readlink_or_path(const fs::path &path, int dirfd=AT_FDCWD, bool notexist_ok=true) { | 
				
			||||
    auto target = readlink(path, dirfd); | 
				
			||||
    if (target.empty()) { | 
				
			||||
        if (errno == EINVAL || (errno == ENOENT && notexist_ok)) | 
				
			||||
            return path; | 
				
			||||
        else | 
				
			||||
            return {}; | 
				
			||||
    } | 
				
			||||
    // Make (relative) returned value relative to dirfd
 | 
				
			||||
    if (target.is_relative()) | 
				
			||||
        return path.parent_path() / target; | 
				
			||||
    return target; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Check if a directory is empty | 
				
			||||
 */ | 
				
			||||
bool is_dir_empty(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    auto fd = opendir(path, dirfd); | 
				
			||||
    if (!fd) | 
				
			||||
        return false; | 
				
			||||
    auto dir = fs::dir_ptr(fd); | 
				
			||||
    if (!dir) | 
				
			||||
        return false; | 
				
			||||
    errno = 0; | 
				
			||||
    while (true) { | 
				
			||||
        auto res = dir.readdir(); | 
				
			||||
        if (res == nullptr) | 
				
			||||
            return errno == 0; | 
				
			||||
        if (strcmp(".", res->d_name) && strcmp("..", res->d_name)) | 
				
			||||
            return false; | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
// @}
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
//-------------------------------------------------------------------
 | 
				
			||||
// Creating files and directories
 | 
				
			||||
// @{
 | 
				
			||||
/**
 | 
				
			||||
 * Create a symbolic link | 
				
			||||
 * @param target The link target. | 
				
			||||
 * @param path The path of the new symlink | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 * @note target is relative to the directory containing the link, NOT dirfd | 
				
			||||
 */ | 
				
			||||
int symlink(const fs::cpath &target, const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::symlinkat(target, dirfd, path); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Create a directory | 
				
			||||
 * @param path The new directory path | 
				
			||||
 * @param mode The permissions to assign | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 */ | 
				
			||||
int mkdir(const fs::cpath &path, mode_t mode=0755, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::mkdirat(dirfd, path, mode); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Create all parent directories | 
				
			||||
 * @param path The path of the innermost directory to create | 
				
			||||
 * @param mode The permissions to assign | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return The number of directories created, or -1 on error | 
				
			||||
 */ | 
				
			||||
int makedirs(const fs::path &path, mode_t mode=0755, int dirfd=AT_FDCWD) { | 
				
			||||
    struct stat st; | 
				
			||||
    // Treat empty path as .
 | 
				
			||||
    // Check if exists
 | 
				
			||||
    if (!fstatat(dirfd, path.empty() ? "." : path.c_str(), &st, 0)) { | 
				
			||||
        // If directory, we're fine.
 | 
				
			||||
        if (S_ISDIR(st.st_mode)) | 
				
			||||
            return 0; | 
				
			||||
        // Else, this is an error
 | 
				
			||||
        errno = ENOTDIR; | 
				
			||||
        return -1; | 
				
			||||
    } | 
				
			||||
    // Propagate any error other than ENOENT
 | 
				
			||||
    if (errno != ENOENT || path.empty()) | 
				
			||||
        return -1; | 
				
			||||
    // Ensure parents
 | 
				
			||||
    auto parents = makedirs(path.parent_path(), mode, dirfd); | 
				
			||||
    // Actually create directory
 | 
				
			||||
    if (mkdir(path, mode, dirfd)) | 
				
			||||
        return -1; | 
				
			||||
    return parents + 1; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Create a file if it doesn't exist | 
				
			||||
 * @param path The path of the file | 
				
			||||
 * @param mode The permissions to assign if it has to be created | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 */ | 
				
			||||
int touch(const fs::cpath &path, mode_t mode=0755, int dirfd=AT_FDCWD) { | 
				
			||||
    auto fd = open_creat(path, O_WRONLY, mode, dirfd); | 
				
			||||
    return fd ? 0 : -1; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Remove a file | 
				
			||||
 * @param path The path of the file to remove | 
				
			||||
 * @param dirfd The directory fd \p may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 */ | 
				
			||||
int unlink(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::unlinkat(dirfd, path, 0); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Remove a directory | 
				
			||||
 * @param path The path of the directory to remove | 
				
			||||
 * @param dirfd The directory fd \p may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 */ | 
				
			||||
int rmdir(const fs::cpath &path, int dirfd=AT_FDCWD) { | 
				
			||||
    return ::unlinkat(dirfd, path, AT_REMOVEDIR); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Copy a symbolic link | 
				
			||||
 * @param from The source symbolic link path | 
				
			||||
 * @param to The target symbolic link path (must not exist) | 
				
			||||
 * @param from_dirfd The directory fd \p from may be relative to | 
				
			||||
 * @param dirfd The directory fd \p to may be relative to | 
				
			||||
 * @return 0 on success | 
				
			||||
 */ | 
				
			||||
int copy_symlink(const fs::cpath &from, fs::cpath to, | 
				
			||||
                 int from_dirfd=AT_FDCWD, int dirfd=AT_FDCWD) { | 
				
			||||
    auto target = readlink(from, from_dirfd); | 
				
			||||
    return ::symlinkat(target.c_str(), dirfd, to); | 
				
			||||
} | 
				
			||||
// @}
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
//-------------------------------------------------------------------
 | 
				
			||||
// File descriptor I/O
 | 
				
			||||
// @{
 | 
				
			||||
// Read
 | 
				
			||||
/**
 | 
				
			||||
 * Read until \p size bytes have been read or an error has been encoutnered | 
				
			||||
 * @param fd A file descriptor | 
				
			||||
 * @param dest The destination buffer | 
				
			||||
 * @param size The desired number of bytes read | 
				
			||||
 * @return The actual number of bytes read | 
				
			||||
 * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF | 
				
			||||
 */ | 
				
			||||
size_t read(int fd, char *dest, size_t size) { | 
				
			||||
    size_t have = 0; | 
				
			||||
    
 | 
				
			||||
    while (have < size) { | 
				
			||||
        auto got = ::read(fd, dest + have, size - have); | 
				
			||||
        
 | 
				
			||||
        if (got == 0) { | 
				
			||||
            errno = 0; | 
				
			||||
            break; | 
				
			||||
        } else if (got < 0) | 
				
			||||
            break; | 
				
			||||
        
 | 
				
			||||
        have += got; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    return have; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Read until \p size bytes have been read or an error has been encoutnered | 
				
			||||
 * @param fd A file descriptor | 
				
			||||
 * @param size The desired number of bytes read | 
				
			||||
 * @return The resulting string | 
				
			||||
 * @note If returned string.size() != \p size, errno will be set. errno == 0 indicates EOF | 
				
			||||
 */ | 
				
			||||
std::string read(int fd, size_t size) { | 
				
			||||
    auto buf = std::string(size, 0); | 
				
			||||
    buf.resize(read(fd, buf.data(), size)); | 
				
			||||
    return buf; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Read until \p size bytes have been read, an error has been encoutnered, or the timeout is hit | 
				
			||||
 * @param fd A file descriptor | 
				
			||||
 * @param dest The destination buffer | 
				
			||||
 * @param size The desired number of bytes read | 
				
			||||
 * @param timeout The timeout that must not be exceeded between chunk reads | 
				
			||||
 * @return The actual number of bytes read | 
				
			||||
 * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF. | 
				
			||||
 *       Timeout is indicated by ETIMEDOUT. | 
				
			||||
 */ | 
				
			||||
size_t read(int fd, char *dest, size_t size, timeval timeout) { | 
				
			||||
    size_t have = 0; | 
				
			||||
    
 | 
				
			||||
    auto fds = fd_set(); | 
				
			||||
    FD_ZERO(&fds); | 
				
			||||
    FD_SET(fd, &fds); | 
				
			||||
    
 | 
				
			||||
    while (have < size) { | 
				
			||||
        auto rv = select(fd + 1, &fds, nullptr, nullptr, &timeout); | 
				
			||||
        
 | 
				
			||||
        if (rv == 0) { | 
				
			||||
            errno = ETIMEDOUT; | 
				
			||||
            break; | 
				
			||||
        } else if (rv < 0) { | 
				
			||||
            break; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        auto got = ::read(fd, dest + have, size - have); | 
				
			||||
        if (got == 0) { | 
				
			||||
            errno = 0; | 
				
			||||
            break; | 
				
			||||
        } else if (got < 0) | 
				
			||||
            break; | 
				
			||||
 | 
				
			||||
        have += got; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    return have; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Read until \p size bytes have been read, an error has been encoutnered, or the timeout is hit | 
				
			||||
 * @param fd A file descriptor | 
				
			||||
 * @param size The desired number of bytes read | 
				
			||||
 * @param timeout The timeout that must not be exceeded between chunk reads | 
				
			||||
 * @return The resulting string | 
				
			||||
 * @note If returned value != \p size, errno will be set. errno == 0 indicates EOF | 
				
			||||
 *       Timeout is indicated by ETIMEDOUT. | 
				
			||||
 */ | 
				
			||||
std::string read(int fd, size_t size, timeval timeout) { | 
				
			||||
    auto buf = std::string(size, 0); | 
				
			||||
    buf.resize(read(fd, buf.data(), size, timeout)); | 
				
			||||
    return buf; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Read a POD type from a file descriptor | 
				
			||||
 * @tparam T The type | 
				
			||||
 * @param fd The file descriptor | 
				
			||||
 * @return The object on success, std::nullopt on failure | 
				
			||||
 * @note If std::nullopt is returned, errno will be set. | 
				
			||||
 */ | 
				
			||||
template <typename T> | 
				
			||||
std::optional<T> read_bin(int fd) { | 
				
			||||
    char buf[sizeof(T)]; | 
				
			||||
    if (read(fd, buf, sizeof(T)) == sizeof(T)) | 
				
			||||
        return *reinterpret_cast<T*>(buf); | 
				
			||||
    else | 
				
			||||
        return std::nullopt; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Write
 | 
				
			||||
/**
 | 
				
			||||
 * Write all bytes to a file descriptor unless an error occurs (blocking) | 
				
			||||
 * @param fd The file descriptor | 
				
			||||
 * @param buf The source buffer | 
				
			||||
 * @param size The number of bytes to write | 
				
			||||
 * @return The number of bytes written | 
				
			||||
 * @note If returned value != \p size, errno will be set. | 
				
			||||
 */ | 
				
			||||
size_t write(int fd, const char *buf, size_t size) { | 
				
			||||
    size_t have = 0; | 
				
			||||
    
 | 
				
			||||
    while (have < size) { | 
				
			||||
        auto got = ::write(fd, buf + have, size - have); | 
				
			||||
        
 | 
				
			||||
        if (got == 0) { | 
				
			||||
            errno = 0; | 
				
			||||
            break; | 
				
			||||
        } else if (got < 0) | 
				
			||||
            break; | 
				
			||||
        
 | 
				
			||||
        have += got; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    return have; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Write all bytes to a file descriptor unless an error occurs (blocking) | 
				
			||||
 * @param fd The file descriptor | 
				
			||||
 * @param s A string to write | 
				
			||||
 * @return The number of bytes written | 
				
			||||
 * @note If returned value != \p s.size(), errno will be set. | 
				
			||||
 */ | 
				
			||||
size_t write(int fd, const std::string &s) { | 
				
			||||
    return write(fd, s.data(), s.size()); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Write a POD object to a file descriptor | 
				
			||||
 * @tparam T The POD type | 
				
			||||
 * @param fd The file descriptor | 
				
			||||
 * @param v The object | 
				
			||||
 * @return The number of bytes written | 
				
			||||
 * @note If returned value != sizeof(T), errno will be set. | 
				
			||||
 */ | 
				
			||||
template <typename T> | 
				
			||||
size_t write_bin(int fd, const T &v) { | 
				
			||||
    return write(fd, reinterpret_cast<const char*>(&v), sizeof(v)); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Shortcuts
 | 
				
			||||
/**
 | 
				
			||||
 * Read a file from disk | 
				
			||||
 * @param path The file path | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 * @param max The maximum number of bytes to read | 
				
			||||
 * @return A pair of (data read, errno) | 
				
			||||
 * @note If data.size() == max, more data may be available. | 
				
			||||
 */ | 
				
			||||
std::pair<std::string, int> cat(const fs::cpath &path, int dirfd=AT_FDCWD, size_t max=1024) { | 
				
			||||
    auto fd = open(path, O_RDONLY, dirfd); | 
				
			||||
    if (!fd) | 
				
			||||
        return {}; | 
				
			||||
    auto r = read(fd, max); | 
				
			||||
    if (r.size() < max) | 
				
			||||
        return {r, errno}; | 
				
			||||
    return {r, 0}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Write a file to disk | 
				
			||||
 * @param s The data to write | 
				
			||||
 * @param path The path to write to | 
				
			||||
 * @param mode The mode to create the file with, if neccessary | 
				
			||||
 * @param dirfd The directory fd \p path may be relative to | 
				
			||||
 */ | 
				
			||||
bool dump(const std::string &s, const fs::cpath &path, mode_t mode, int dirfd=AT_FDCWD) { | 
				
			||||
    auto fd = open_creat(path, O_WRONLY, mode, dirfd); | 
				
			||||
    if (!fd) | 
				
			||||
        return -1; | 
				
			||||
    return write(fd, s) == s.size(); | 
				
			||||
} | 
				
			||||
// @}
 | 
				
			||||
 | 
				
			||||
//-------------------------------------------------------------------
 | 
				
			||||
// Copying Files
 | 
				
			||||
// @{
 | 
				
			||||
/**
 | 
				
			||||
 * Naively copy data between file descriptors | 
				
			||||
 * @param fs The source file descriptor | 
				
			||||
 * @param fd The destination file descriptor | 
				
			||||
 * @param len The number of bytes to copy | 
				
			||||
 */ | 
				
			||||
bool fcopy_raw(int fs, int fd, size_t len) { | 
				
			||||
    constexpr size_t bufsz = 8192; | 
				
			||||
    char buf[bufsz]; | 
				
			||||
    do { | 
				
			||||
        auto target = std::min(len, bufsz); | 
				
			||||
        auto nread = read(fs, buf, target); | 
				
			||||
        if (nread < target && errno != 0) | 
				
			||||
            return false; | 
				
			||||
        auto written = write(fd, buf, nread); | 
				
			||||
        if (written < nread) | 
				
			||||
            return false; | 
				
			||||
        if (nread < target) | 
				
			||||
            return true; | 
				
			||||
        len -= nread; | 
				
			||||
    } while (len > 0); | 
				
			||||
    return true; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Copy data between file descriptors | 
				
			||||
 * @param fs The source file descriptor | 
				
			||||
 * @param fd The destination file descriptor | 
				
			||||
 * @param len The number of bytes to copy | 
				
			||||
 * @return false on failure with errno set | 
				
			||||
 * @note This attempts to use copy_file_range(2) and sendfile(2) | 
				
			||||
 *       before falling back to fcopy_raw | 
				
			||||
 */ | 
				
			||||
bool fcopy(int fs, int fd, size_t len) { | 
				
			||||
    while (len > 0) { | 
				
			||||
        auto r = ::copy_file_range(fs, NULL, fd, NULL, len, 0); | 
				
			||||
        if (r < 0) { | 
				
			||||
            if (errno == ENOSYS || errno == EXDEV || errno == EINVAL) | 
				
			||||
                break; | 
				
			||||
            return fcopy_raw(fs, fd, len); | 
				
			||||
        } | 
				
			||||
        len -= r; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    while (len > 0) { | 
				
			||||
        auto r = ::sendfile(fd, fs, NULL, len); | 
				
			||||
        if (r < 0) | 
				
			||||
            return fcopy_raw(fs, fd, len); | 
				
			||||
        len -= r; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    return true; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Copy a file | 
				
			||||
 * @param src The path to copy from | 
				
			||||
 * @param dst The path to copy to | 
				
			||||
 * @param src_dir The directory fd \p src may be relative to | 
				
			||||
 * @param dst_dir The directory fd \p dst may be relative to | 
				
			||||
 * @return false on failure with errno set | 
				
			||||
 * @note This variant will only try to preserve the file mode, no other attributes | 
				
			||||
 * @note Note that this function takes two separate directory fds | 
				
			||||
 * @note This will use reflink/FICLONE if supported. | 
				
			||||
 */ | 
				
			||||
bool copy0(const fs::cpath &src, const fs::cpath &dst, int src_dir=AT_FDCWD, int dst_dir=AT_FDCWD) { | 
				
			||||
    struct stat st; | 
				
			||||
    if (::fstatat(src_dir, src, &st, 0)) | 
				
			||||
        return false; | 
				
			||||
 | 
				
			||||
    auto fs = open(src, O_RDONLY, src_dir); | 
				
			||||
    if (!fs) | 
				
			||||
        return false; | 
				
			||||
    auto fd = open_creat(dst, O_WRONLY, st.st_mode, dst_dir); | 
				
			||||
    if (!fd) | 
				
			||||
        return false; | 
				
			||||
 | 
				
			||||
    // Try reflink
 | 
				
			||||
#ifdef FICLONE | 
				
			||||
    int ret = ::ioctl(fd, FICLONE, (int)fs); | 
				
			||||
    if (ret != -1) | 
				
			||||
        return ret == st.st_size; | 
				
			||||
#endif | 
				
			||||
    
 | 
				
			||||
    return fcopy(fs, fd, st.st_size); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// @}
 | 
				
			||||
} // namespace ko::fd
 | 
				
			||||
@ -0,0 +1,78 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// kofd_pipe.hpp
 | 
				
			||||
// ko::fd::pipe
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// bi-directional pipe implementation
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "kofd.hpp" | 
				
			||||
 | 
				
			||||
#include <optional> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace ko::fd { | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
/**
 | 
				
			||||
 * Represents a bi-directional pair of file descriptors | 
				
			||||
 */ | 
				
			||||
class pipe { | 
				
			||||
    int rfd, wfd; | 
				
			||||
    
 | 
				
			||||
public: | 
				
			||||
    pipe(fd &&fd) : | 
				
			||||
        rfd(fd.move()), wfd(rfd) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    pipe(fd &&rfd, fd &&wfd) : | 
				
			||||
        rfd(rfd.move()), wfd(wfd.move()) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    explicit pipe(int rfd, int wfd) : | 
				
			||||
        rfd(rfd), wfd(wfd) | 
				
			||||
    {} | 
				
			||||
    
 | 
				
			||||
    ~pipe() { | 
				
			||||
        ::close(this->rfd); | 
				
			||||
        if (this->wfd != this->rfd) | 
				
			||||
            ::close(this->wfd); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // IO Functions, see namespace fd
 | 
				
			||||
    inline size_t read(char *dest, size_t size) { | 
				
			||||
        return ::ko::fd::read(this->rfd, dest, size); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    inline std::string read(size_t size) { | 
				
			||||
        return ::ko::fd::read(this->rfd, size); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    inline size_t read(char *dest, size_t size, timeval timeout) { | 
				
			||||
        return ::ko::fd::read(this->rfd, dest, size, timeout); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    inline std::string read(size_t size, timeval timeout) { | 
				
			||||
        return ::ko::fd::read(this->rfd, size, timeout); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    inline size_t write(const char *buf, size_t size) { | 
				
			||||
        return ::ko::fd::write(this->wfd, buf, size); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    inline size_t write(const std::string &s) { | 
				
			||||
        return ::ko::fd::write(this->wfd, s); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    template <typename T> | 
				
			||||
    inline size_t write_bin(const T &v) { | 
				
			||||
        return ::ko::fd::write_bin<T>(this->wfd, v); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    template <typename T> | 
				
			||||
    inline std::optional<T> read_bin() { | 
				
			||||
        return ::ko::fd::read_bin<T>(this->rfd); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
} // namespace ko::fd
 | 
				
			||||
@ -0,0 +1,110 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// kofs.hpp
 | 
				
			||||
// ko::fs
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// Misc. Filesystem functions
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include <dirent.h> | 
				
			||||
#include <unistd.h> | 
				
			||||
 | 
				
			||||
#include <string> | 
				
			||||
#include <filesystem> | 
				
			||||
 | 
				
			||||
namespace ko::fs { | 
				
			||||
using namespace std::filesystem; | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Helper struct for functions that require a c-string path | 
				
			||||
 */ | 
				
			||||
struct cpath { | 
				
			||||
    const char *path; | 
				
			||||
    
 | 
				
			||||
    inline cpath(const char *path) : path(path) {} | 
				
			||||
    inline cpath(const fs::path &path) : path(path.c_str()) {} | 
				
			||||
    inline cpath(const std::string &path) : path(path.c_str()) {} | 
				
			||||
    
 | 
				
			||||
    inline operator const char *() const { | 
				
			||||
        return path; | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
class dir_ptr { | 
				
			||||
    DIR *ptr; | 
				
			||||
    
 | 
				
			||||
public: | 
				
			||||
    dir_ptr(int fd) { | 
				
			||||
        ptr = ::fdopendir(fd); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    dir_ptr(const cpath &path) { | 
				
			||||
        ptr = ::opendir(path); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    bool operator !() { | 
				
			||||
        return ptr == nullptr; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    ~dir_ptr() { | 
				
			||||
        ::closedir(ptr); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    dirent const *readdir() { | 
				
			||||
        return ::readdir(ptr); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Iterator
 | 
				
			||||
    class iterator { | 
				
			||||
        dir_ptr &dir; | 
				
			||||
        dirent const *here = nullptr; | 
				
			||||
        bool done = false; | 
				
			||||
        int error = 0; | 
				
			||||
 | 
				
			||||
        friend class dir_ptr; | 
				
			||||
 | 
				
			||||
        iterator(dir_ptr &dir, bool done) : dir(dir), done(done) { | 
				
			||||
            ++(*this); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
    public: | 
				
			||||
        iterator &operator ++() { | 
				
			||||
            if (!done) { | 
				
			||||
                auto errno_bak = errno; | 
				
			||||
                here = dir.readdir(); | 
				
			||||
                if (here == nullptr) { | 
				
			||||
                    done = true; | 
				
			||||
                    if (errno != errno_bak) | 
				
			||||
                        error = errno; | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
            return *this; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        dirent const &operator *() { | 
				
			||||
            return *here; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        operator bool() { | 
				
			||||
            return !done; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        bool operator ==(iterator const &other) const { | 
				
			||||
            return dir.ptr == other.dir.ptr && (here == other.here || done == other.done); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        int get_errno() { | 
				
			||||
            return error; | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    iterator begin() { | 
				
			||||
        return iterator(*this, false); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    iterator end() { | 
				
			||||
        return iterator(*this, true); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
} // namespace ko::fs
 | 
				
			||||
@ -0,0 +1,241 @@ | 
				
			||||
/*
 | 
				
			||||
 * Small Linux Namespace utility header | 
				
			||||
 * (c) 2019 Taeyeon Mori <taeyeon AT oro.sodimm.me> | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "koutil.hpp" | 
				
			||||
#include "kofd.hpp" | 
				
			||||
#include "koos.hpp" | 
				
			||||
 | 
				
			||||
#include <sched.h> | 
				
			||||
#include <sys/syscall.h> | 
				
			||||
#include <sys/types.h> | 
				
			||||
#include <unistd.h> | 
				
			||||
 | 
				
			||||
#include <array> | 
				
			||||
#include <cstdlib> | 
				
			||||
#include <fstream> | 
				
			||||
#include <iostream> | 
				
			||||
#include <memory> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace ko::ns { | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * idmap namespace | 
				
			||||
 * Contains functions for setting /proc/pid/[ug]id_map | 
				
			||||
 */ | 
				
			||||
namespace idmap { | 
				
			||||
    /// An entry in [ug]id_map
 | 
				
			||||
    struct entry { | 
				
			||||
        uid_t start; | 
				
			||||
        uid_t host_start; | 
				
			||||
        unsigned long count; | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    /// Write the idmap <map> to <path>
 | 
				
			||||
    template <typename Container> | 
				
			||||
    inline bool set(fs::path path, Container map) { | 
				
			||||
        auto stream = std::ofstream(path); | 
				
			||||
        for (entry &e : map) | 
				
			||||
            stream << e.start << ' ' << e.host_start << ' ' << e.count << '\n'; | 
				
			||||
        stream.close(); | 
				
			||||
        return stream.good(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Disable setgroups() syscall for process <pid>
 | 
				
			||||
    /// This is required for unprivileged user namespaces
 | 
				
			||||
    bool disable_setgroups(pid_t pid) { | 
				
			||||
        auto stream = std::ofstream(util::str("/proc/", pid, "/setgroups")); | 
				
			||||
        stream << "deny"; | 
				
			||||
        stream.close(); | 
				
			||||
        return stream.good(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Return an idmap mapping a single ID
 | 
				
			||||
    inline constexpr std::array<entry, 1> single(uid_t id, uid_t host_id) { | 
				
			||||
        return {{ {id, host_id, 1} }}; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Get the path to the <map_type> map for <pid>
 | 
				
			||||
    /// <map_type> may be "uid" or "pid"
 | 
				
			||||
    inline fs::path path(pid_t pid, const char *map_type) { | 
				
			||||
        return util::str("/proc/", pid, "/", map_type, "_map"); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * namespace kons::mount | 
				
			||||
 * Stuff related to setting up the mount namespace | 
				
			||||
 */ | 
				
			||||
namespace mount { | 
				
			||||
    using os::mount; | 
				
			||||
    using os::bind; | 
				
			||||
 | 
				
			||||
    // Mount all the basic filesystems
 | 
				
			||||
    util::cvresult mount_core(const fs::path &root) { | 
				
			||||
        return util::cvshort() | 
				
			||||
        .ifthen("mount_root",   !os::is_mountpoint(root),   bind, root, root, 0) | 
				
			||||
        .ifthen("mount_proc",   fs::exists(root / "proc"),  mount, "proc", root / "proc", "proc", 0, nullptr) | 
				
			||||
        .ifthen("mount_sys",    fs::exists(root / "sys"),   bind, "/sys", root / "sys", MS_REC) | 
				
			||||
        .ifthen("mount_dev",    fs::exists(root / "dev"),   bind, "/dev", root / "dev", MS_REC) | 
				
			||||
        .ifthen("mount_tmp",    fs::exists(root / "tmp"),   mount, "tmp", root / "tmp", "tmpfs", 0, nullptr) | 
				
			||||
        .ifthen("mount_run",    fs::exists(root / "run"),   mount, "run", root / "run", "tmpfs", 0, nullptr); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Write-protect path to mitigate broken filesystem permissions in single-user ns
 | 
				
			||||
    util::cvresult protect_path(const fs::path &path) { | 
				
			||||
        return util::cvshort() | 
				
			||||
        .then("bind_protect", bind, path, path, MS_REC) | 
				
			||||
        .then("bind_protect_ro", bind, path, path, MS_REC|MS_REMOUNT|MS_RDONLY); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Bind in additional locations required by GUI programs
 | 
				
			||||
    /// Some of these are serious isolation breaches!
 | 
				
			||||
    /// Note that home and rundir must be relative and will be interpreted against both '/' and $root
 | 
				
			||||
    util::cvresult mount_gui(const fs::path &root, const fs::path &home, const fs::path &rundir) { | 
				
			||||
        auto path_from_env = [](const char *name, fs::path dflt, const char *prefix=nullptr) -> fs::path { | 
				
			||||
            auto var = getenv(name); | 
				
			||||
            if (var != nullptr) { | 
				
			||||
                if (prefix != nullptr) { | 
				
			||||
                    auto plen = strlen(prefix); | 
				
			||||
                    if (!strncmp(var, prefix, plen)) | 
				
			||||
                        var += plen; | 
				
			||||
                } | 
				
			||||
                if (var[0] == '/') | 
				
			||||
                    return var+1; | 
				
			||||
            } | 
				
			||||
            return dflt; | 
				
			||||
        }; | 
				
			||||
        auto path_from_env_rel = [](fs::path to, const char *name, const char *dflt) -> fs::path { | 
				
			||||
            auto var = getenv(name); | 
				
			||||
            if (var != nullptr) | 
				
			||||
                return to / var; | 
				
			||||
            return to / dflt; | 
				
			||||
        }; | 
				
			||||
        // Bind-mount various paths required to get GUI apps to communicate with system services
 | 
				
			||||
        // X11, DBus (both buses, Steam does use the system bus), PulseAudio
 | 
				
			||||
        auto frags = std::array<fs::path, 9>{ | 
				
			||||
            "tmp/.X11-unix", | 
				
			||||
            "run/dbus", | 
				
			||||
            "run/udev", // Udev database for correct ENV entries e.g. ID_INPUT_JOYSTICK markers
 | 
				
			||||
            //"etc/machine-id", // Pulseaudio will only connect with same machine-id by default. See below
 | 
				
			||||
            path_from_env("XAUTHORITY", home / ".Xauthority"), | 
				
			||||
            home / ".config/pulse/cookie", | 
				
			||||
            path_from_env("DBUS_SESSION_BUS_ADDRESS", rundir / "bus", "unix:path="), | 
				
			||||
            rundir / "pulse", | 
				
			||||
            rundir / "pipewire-0", | 
				
			||||
            path_from_env_rel(rundir, "WAYLAND_DISPLAY", "wayland-0"), | 
				
			||||
        }; | 
				
			||||
        
 | 
				
			||||
        // /tmp/.X11-unix must be owned by user or root for wlroots xwayland to work (e.g. gamescope)
 | 
				
			||||
        // behaviour can be overridden by env var KONS_BIND_X11=all
 | 
				
			||||
        if (![&frags, root]() { | 
				
			||||
            auto x11_mount_mode = getenv("KONS_BIND_X11"); | 
				
			||||
            if (x11_mount_mode && !strcasecmp("all", x11_mount_mode)) | 
				
			||||
                return true; | 
				
			||||
            auto display = getenv("DISPLAY"); | 
				
			||||
            if (!display) | 
				
			||||
                return false; | 
				
			||||
            if (display[0] == ':') | 
				
			||||
                display += 1; | 
				
			||||
            for (char *c = display; *c; c++) | 
				
			||||
                if (!isdigit(*c)) | 
				
			||||
                    return false; | 
				
			||||
            auto dirname = root / frags[0]; | 
				
			||||
            fs::create_directories(dirname); | 
				
			||||
            ::chmod(dirname.c_str(), 01777); | 
				
			||||
            auto sockname = frags[0] / util::str("X", display); | 
				
			||||
            fd::touch(root / sockname); | 
				
			||||
            frags[0] = sockname; | 
				
			||||
            return true; | 
				
			||||
        }()) { | 
				
			||||
            std::cerr << "Warn: Invalid $DISPLAY value; falling back to bind-mounting /tmp/.X11-unix whole" << std::endl; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // Pulseaudio will by default only connect to the server published in the X11 root window properties if the machine-ids match.
 | 
				
			||||
        // Either we bind-mount /etc/machine-id or we need to set PULSE_SERVER in the environment. Both are suboptimal hacks:
 | 
				
			||||
        // /etc/machine-id shoudn't be the same across two rootfs' but it might be acceptable since we're not running init.
 | 
				
			||||
        // OTOH, setting PULSE_SERVER can break with nonstandard configurations if they're not manually set in ENV. X11 publish is not enough.
 | 
				
			||||
        auto pulse = util::str("unix:/", rundir.c_str(), "/pulse/native"); | 
				
			||||
        setenv("PULSE_SERVER", pulse.c_str(), 0); // Don't overwrite, assume that there's a reason it's set. May be TCP.
 | 
				
			||||
                                                  // If custom unix socket path, it could fail either way as it may not be included above.
 | 
				
			||||
        // NOTE that exec[vlp]e() must be used to make setenv() work.
 | 
				
			||||
 | 
				
			||||
        auto sh = util::cvshort(); | 
				
			||||
        auto host_root = fs::path("/"); | 
				
			||||
        for (auto frag : frags) { | 
				
			||||
            auto hpath = host_root / frag; | 
				
			||||
 | 
				
			||||
            if (fs::exists(hpath)) { | 
				
			||||
                auto path = root / frag; | 
				
			||||
 | 
				
			||||
                if (!fs::exists(path)) { | 
				
			||||
                    if (fs::is_directory(hpath)) | 
				
			||||
                        fs::create_directories(path); | 
				
			||||
                    else { | 
				
			||||
                        fs::create_directories(path.parent_path()); | 
				
			||||
                        auto touch = std::ofstream(path); | 
				
			||||
                    } | 
				
			||||
                } | 
				
			||||
 | 
				
			||||
                if (!(sh = sh.then("mount_gui", bind, hpath, path, 0))) | 
				
			||||
                    break; | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return sh; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    /// Pivot the root to $new_root, optionally keeping the old one at $old_root.
 | 
				
			||||
    /// Note that the $old_root directory is required in the process either way.
 | 
				
			||||
    util::cvresult pivot_root(const fs::path &new_root, const fs::path &old_root, bool keep_old=true) { | 
				
			||||
        auto path = new_root / old_root; | 
				
			||||
        if (!fs::exists(path)) | 
				
			||||
            fs::create_directories(path); | 
				
			||||
 | 
				
			||||
        return util::cvshort() | 
				
			||||
            .then("pivot_root", syscall, SYS_pivot_root, new_root.c_str(), path.c_str()) | 
				
			||||
            .then("chdir_root", chdir, "/") | 
				
			||||
            .ifthen("umount_oldroot", !keep_old, umount2, old_root.c_str(), MNT_DETACH); | 
				
			||||
    } | 
				
			||||
} // namespace mount
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Unshare (at least) new single-user namespace | 
				
			||||
 * @param uid The uid inside the userns | 
				
			||||
 * @param gid The gid inside the userns | 
				
			||||
 * @param flags The unshare(2)/clone(2) flags (CLONE_NEWUSER implied) | 
				
			||||
 * @return Zero on success, -1 + errno on failure | 
				
			||||
 */ | 
				
			||||
inline int unshare_single(uid_t uid, uid_t gid, long flags) { | 
				
			||||
    auto euid = geteuid(); | 
				
			||||
    auto egid = getegid(); | 
				
			||||
    auto r = ::unshare(flags | CLONE_NEWUSER); | 
				
			||||
    if (r != 0) | 
				
			||||
        return r; | 
				
			||||
    if (!idmap::set("/proc/self/uid_map", idmap::single(uid, euid))) | 
				
			||||
        return -1; | 
				
			||||
    if (!idmap::disable_setgroups(getpid())) | 
				
			||||
        return -1; | 
				
			||||
    if (!idmap::set("/proc/self/gid_map", idmap::single(gid, egid))) | 
				
			||||
        return -1; | 
				
			||||
    return 0; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
inline int setns(const fs::cpath &path, int type, int dirfd=AT_FDCWD) { | 
				
			||||
    auto fd = ::openat(dirfd, path, O_RDONLY); | 
				
			||||
    if (fd < 0) | 
				
			||||
        return errno; | 
				
			||||
    auto res = ::setns(fd, type); | 
				
			||||
    ::close(fd); | 
				
			||||
    return res; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
} // namespace ko::ns
 | 
				
			||||
 | 
				
			||||
@ -0,0 +1,81 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// kons_clone.hpp
 | 
				
			||||
// ko::ns::clone namespace
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// Small Linux namespace utility header
 | 
				
			||||
// Clone-related functions
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "kons.hpp" | 
				
			||||
#include "koproc.hpp" | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * clone namespace | 
				
			||||
 * Useful wrappers around the clone(2) syscall | 
				
			||||
 */ | 
				
			||||
namespace ko::ns::clone { | 
				
			||||
namespace detail { | 
				
			||||
    template <typename ArgP> | 
				
			||||
    int uvclone_entry(void *arg) { | 
				
			||||
        auto [f, args, sync] = *reinterpret_cast<ArgP>(arg); | 
				
			||||
        sync.yield(); | 
				
			||||
        return std::apply(f, args); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process in a new user namespace | 
				
			||||
 * @param uidmap The uid map for the user namespace | 
				
			||||
 * @param gidmap The gid map for the user namespace | 
				
			||||
 * @param fn The function to call in the child process | 
				
			||||
 * @param stacksize The size of the process stack | 
				
			||||
 * @param flags The clone(2) flags (SIGCHLD|CLONE_VM|CLONE_NEWUSER implied) | 
				
			||||
 * @param args The function arguments | 
				
			||||
 */ | 
				
			||||
template <typename U, typename G, typename F, typename... Args> | 
				
			||||
std::pair<proc::child_ref, int> uvclone(U uidmap, G gidmap, F fn, size_t stacksize, long flags, Args... args) { | 
				
			||||
    auto [sync, sync_c] = proc::sync::make_semapair(false); | 
				
			||||
    auto data = new std::tuple{fn, std::tuple{std::forward<Args>(args)...}, sync_c}; | 
				
			||||
 | 
				
			||||
    auto proc = proc::detail::do_clone(detail::uvclone_entry<decltype(data)>, stacksize, CLONE_NEWUSER | CLONE_VM | flags, data); | 
				
			||||
    auto res = EINVAL; | 
				
			||||
 | 
				
			||||
    if (proc) { | 
				
			||||
        // Wait for child
 | 
				
			||||
        sync.wait(); | 
				
			||||
 | 
				
			||||
        // Set maps
 | 
				
			||||
        auto pid = proc.pid(); | 
				
			||||
        if (idmap::set(idmap::path(pid, "uid"), uidmap)) { | 
				
			||||
            if (idmap::disable_setgroups(pid)) { | 
				
			||||
                if (idmap::set(idmap::path(pid, "gid"), gidmap)) { | 
				
			||||
                    res = 0; | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        if (res) | 
				
			||||
            res = errno; | 
				
			||||
 | 
				
			||||
        sync.post(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    return {std::move(proc), res}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process in a new single-user user namespace | 
				
			||||
 * @param uid The uid inside the user namespace | 
				
			||||
 * @param gid The gid inside the user namespace | 
				
			||||
 * @param fn The function to call in the child process | 
				
			||||
 * @param stacksize The size of the process stack | 
				
			||||
 * @param flags The clone(2) flags (SIGCHLD|CLONE_VM|CLONE_NEWUSER implied) | 
				
			||||
 * @param args The function arguments | 
				
			||||
 */ | 
				
			||||
template <typename F, typename... Args> | 
				
			||||
inline std::pair<proc::child_ref, int> uvclone_single(uid_t uid, gid_t gid, F fn, size_t stacksize, long flags, Args... args) { | 
				
			||||
    return uvclone(idmap::single(uid, getuid()), idmap::single(gid, getgid()), fn, stacksize, flags, args...); | 
				
			||||
} | 
				
			||||
} // namespace ko::ns::clone
 | 
				
			||||
@ -0,0 +1,68 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// koos.hpp
 | 
				
			||||
// ko::os
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// Misc. OS interfaces
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include "kofs.hpp" | 
				
			||||
 | 
				
			||||
#include <mntent.h> | 
				
			||||
#include <pwd.h> | 
				
			||||
#include <sys/mount.h> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace ko::os { | 
				
			||||
/// Try to get the current user home directory
 | 
				
			||||
fs::path get_home() { | 
				
			||||
    const char *home_env = getenv("HOME"); | 
				
			||||
 | 
				
			||||
    if (home_env) | 
				
			||||
        return fs::path(home_env); | 
				
			||||
 | 
				
			||||
    auto pwd = getpwuid(getuid()); | 
				
			||||
 | 
				
			||||
    if (pwd) | 
				
			||||
        return fs::path(pwd->pw_dir); | 
				
			||||
 | 
				
			||||
    return fs::path("/"); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Mounting filesystems
 | 
				
			||||
inline int mount(const fs::cpath &src, const fs::cpath &dst, const char *type, long flags=0, void *args=nullptr) { | 
				
			||||
    auto res = ::mount(src, dst, type, flags, args); | 
				
			||||
    if (res) | 
				
			||||
        return errno; | 
				
			||||
    return 0; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
inline int bind(const fs::cpath &src, const fs::cpath &dst, long flags=0) { | 
				
			||||
    return mount(src, dst, nullptr, MS_BIND | flags, nullptr); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/// Check if a path is a mount point
 | 
				
			||||
bool is_mountpoint(const fs::cpath &path) { | 
				
			||||
    auto fp = setmntent("/proc/self/mounts", "r"); | 
				
			||||
    if (!fp) { | 
				
			||||
        perror("mntent"); | 
				
			||||
        return false; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    bool found = false; | 
				
			||||
 | 
				
			||||
    while (auto ent = getmntent(fp)) { | 
				
			||||
        if (!strcmp(path, ent->mnt_dir)) { | 
				
			||||
            found = true; | 
				
			||||
            break; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    endmntent(fp); | 
				
			||||
 | 
				
			||||
    return found; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
} // namespace ko::os
 | 
				
			||||
@ -0,0 +1,418 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// ko::util koutil.hpp
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// Managing child processes
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include <sched.h> | 
				
			||||
#include <semaphore.h> | 
				
			||||
#include <sys/types.h> | 
				
			||||
#include <sys/mman.h> | 
				
			||||
#include <sys/wait.h> | 
				
			||||
#include <unistd.h> | 
				
			||||
 | 
				
			||||
#include "kofd.hpp" | 
				
			||||
#include "kofd_pipe.hpp" | 
				
			||||
 | 
				
			||||
#include <atomic> | 
				
			||||
#include <array> | 
				
			||||
#include <functional> | 
				
			||||
#include <iostream> | 
				
			||||
#include <memory> | 
				
			||||
#include <optional> | 
				
			||||
#include <tuple> | 
				
			||||
#include <utility> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace ko::proc { | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Simple popen implementaion
 | 
				
			||||
using popen_result_t = std::pair<pid_t, std::unique_ptr<fd::pipe>>; | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process and connect it's stdin and stdout to a pipe | 
				
			||||
 * @param exec_fn An exec-style function to call in the new process. | 
				
			||||
 * @param args The arguments to exec_fn | 
				
			||||
 * @return The PID and a pipe object | 
				
			||||
 * @warning As this uses vfork(), exec_fn must actually call some kind of exec | 
				
			||||
 *          before the parent process can resume. | 
				
			||||
 */ | 
				
			||||
template <typename F, typename... Args> | 
				
			||||
inline popen_result_t popen_impl(F exec_fn, Args... exec_args) { | 
				
			||||
    // Open 2 pipes
 | 
				
			||||
    auto pipefd = std::array<std::array<int, 2>, 2>{}; | 
				
			||||
 | 
				
			||||
    if (::pipe2(pipefd[0].data(), 0)) | 
				
			||||
        return {-1, nullptr}; | 
				
			||||
    if (::pipe2(pipefd[1].data(), 0)) { | 
				
			||||
        ::close(pipefd[0][0]); | 
				
			||||
        ::close(pipefd[0][1]); | 
				
			||||
        return {-1, nullptr}; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Fork
 | 
				
			||||
    auto pid = vfork(); | 
				
			||||
    if (pid == 0) { | 
				
			||||
        // Close parent ends
 | 
				
			||||
        ::close(pipefd[0][1]); | 
				
			||||
        ::close(pipefd[1][0]); | 
				
			||||
 | 
				
			||||
        // Set up stdin and stdout
 | 
				
			||||
        if (::dup2(pipefd[0][0], 0) != 0 || ::dup2(pipefd[1][1], 1) != 1) | 
				
			||||
            _exit(-1); | 
				
			||||
 | 
				
			||||
        // Close superfluous child ends
 | 
				
			||||
        ::close(pipefd[0][0]); | 
				
			||||
        ::close(pipefd[1][1]); | 
				
			||||
 | 
				
			||||
        // exec
 | 
				
			||||
        _exit(exec_fn(exec_args...)); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Close child ends
 | 
				
			||||
    ::close(pipefd[0][0]); | 
				
			||||
    ::close(pipefd[1][1]); | 
				
			||||
 | 
				
			||||
    // Abort if fork failed
 | 
				
			||||
    if (pid < 0) { | 
				
			||||
        ::close(pipefd[0][1]); | 
				
			||||
        ::close(pipefd[1][0]); | 
				
			||||
        return {pid, nullptr}; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // return stuff
 | 
				
			||||
    return {pid, std::make_unique<fd::pipe>(pipefd[1][0], pipefd[0][1])}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process and connect it's stdin and stdout to a pipe | 
				
			||||
 * @param argv The process argv | 
				
			||||
 * @return The PID and a pipe object | 
				
			||||
 * @note argv[0] is the process image path | 
				
			||||
 */ | 
				
			||||
popen_result_t popen(const char **argv) { | 
				
			||||
    return popen_impl(::execv, const_cast<char*>(argv[0]), const_cast<char**>(argv)); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process and connect it's stdin and stdout to a pipe | 
				
			||||
 * @param argv The process argv | 
				
			||||
 * @return The PID and a pipe object | 
				
			||||
 * @note argv[0] is the process image name or path | 
				
			||||
 */ | 
				
			||||
popen_result_t popenp(const char **argv) { | 
				
			||||
    return popen_impl(::execvp, const_cast<char*>(argv[0]), const_cast<char**>(argv)); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
/// Process Synchronization
 | 
				
			||||
namespace sync { | 
				
			||||
namespace detail { | 
				
			||||
    class semaphore_pair { | 
				
			||||
        std::atomic_int refs; | 
				
			||||
        bool shared; | 
				
			||||
        sem_t sems[2]; | 
				
			||||
 | 
				
			||||
        semaphore_pair(bool shared) : | 
				
			||||
            refs(0), shared(shared) | 
				
			||||
        { | 
				
			||||
            int bshared = shared ? 1 : 0; | 
				
			||||
            sem_init(&sems[0], bshared, 0); | 
				
			||||
            sem_init(&sems[1], bshared, 0); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
    public: | 
				
			||||
        ~semaphore_pair() { | 
				
			||||
            sem_destroy(&sems[0]); | 
				
			||||
            sem_destroy(&sems[1]); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        static semaphore_pair *create(bool shared = false) { | 
				
			||||
            if (shared) { | 
				
			||||
                void *mem = mmap(nullptr, sizeof(semaphore_pair), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); | 
				
			||||
                if (mem == MAP_FAILED) | 
				
			||||
                    return nullptr; | 
				
			||||
                return new (mem) semaphore_pair(true); | 
				
			||||
            } else { | 
				
			||||
                return new semaphore_pair(false); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        semaphore_pair *retain() { | 
				
			||||
            refs++; | 
				
			||||
            return this; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        void release() { | 
				
			||||
            auto v = refs.fetch_sub(1); | 
				
			||||
            if (v == 1) { | 
				
			||||
                if (shared) { | 
				
			||||
                    this->~semaphore_pair(); | 
				
			||||
                    munmap(this, sizeof(semaphore_pair)); | 
				
			||||
                } else { | 
				
			||||
                    delete this; | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        sem_t *sem(int n) { | 
				
			||||
            return &sems[n%2]; | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Contains a set of semaphores for bidirectional synchronization | 
				
			||||
 */ | 
				
			||||
class semapair { | 
				
			||||
    detail::semaphore_pair *sems; | 
				
			||||
    int offset; | 
				
			||||
 | 
				
			||||
    semapair(detail::semaphore_pair *sems, int offset) : | 
				
			||||
        sems(sems->retain()), | 
				
			||||
        offset(offset) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    friend std::array<semapair, 2> make_semapair(bool); | 
				
			||||
 | 
				
			||||
public: | 
				
			||||
    semapair(const semapair &o) : | 
				
			||||
        sems(o.sems->retain()), | 
				
			||||
        offset(o.offset) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    semapair(semapair &&o) : | 
				
			||||
        sems(o.sems), | 
				
			||||
        offset(o.offset) | 
				
			||||
    { | 
				
			||||
        o.sems = nullptr; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    ~semapair() { | 
				
			||||
        if (sems) | 
				
			||||
            sems->release(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    inline void wait() { | 
				
			||||
        sem_wait(sems->sem(offset)); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    inline void post() { | 
				
			||||
        sem_post(sems->sem(offset+1)); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    inline void yield() { | 
				
			||||
        post(); | 
				
			||||
        wait(); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
inline std::array<semapair, 2> make_semapair(bool shared) { | 
				
			||||
    auto stuff = detail::semaphore_pair::create(shared); | 
				
			||||
    return {{ {stuff, 0}, {stuff, 1} }}; | 
				
			||||
} | 
				
			||||
} // namespace sync (ko::proc::sync)
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Clone wrappers
 | 
				
			||||
using void_callback_t = std::pair<void(*)(void *), void *>; | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Represents a cloned child process with potential cleanup | 
				
			||||
 */ | 
				
			||||
class child_ref { | 
				
			||||
    pid_t _pid = -1; | 
				
			||||
 | 
				
			||||
    std::optional<void_callback_t> cleanup = {}; | 
				
			||||
 | 
				
			||||
    bool _done = false; | 
				
			||||
    int _status = -1; | 
				
			||||
    
 | 
				
			||||
    inline void _check_clean() { | 
				
			||||
        if (cleanup) | 
				
			||||
            std::cerr << "Warning: ko::proc::child_ref with cleanup destroyed without waiting" << std::endl; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
public: | 
				
			||||
    child_ref(pid_t pid) : | 
				
			||||
        _pid(pid) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    child_ref(pid_t pid, void_callback_t cleanup_cb) : | 
				
			||||
        _pid(pid), cleanup(cleanup_cb) | 
				
			||||
    {} | 
				
			||||
 | 
				
			||||
    child_ref(child_ref &&o) : | 
				
			||||
        _pid(o._pid), cleanup(o.cleanup), | 
				
			||||
        _done(o._done), _status(o._status) | 
				
			||||
    { | 
				
			||||
        o._pid = -1; | 
				
			||||
        o.cleanup = {}; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    child_ref &operator =(child_ref &&o) { | 
				
			||||
        _check_clean(); | 
				
			||||
        _pid = o._pid; | 
				
			||||
        cleanup = std::move(o.cleanup); | 
				
			||||
        _done = o._done; | 
				
			||||
        _status = o._status; | 
				
			||||
        o._pid = -1; | 
				
			||||
        o.cleanup = {}; | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
        
 | 
				
			||||
 | 
				
			||||
    ~child_ref() { | 
				
			||||
        _check_clean(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    operator bool() { | 
				
			||||
        return _pid > 0; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int wait() { | 
				
			||||
        if (!_done) { | 
				
			||||
            waitpid(_pid, &_status, 0); | 
				
			||||
            if (cleanup) { | 
				
			||||
                auto [f, arg] = cleanup.value(); | 
				
			||||
                f(arg); | 
				
			||||
                cleanup = {}; | 
				
			||||
            } | 
				
			||||
            _done = true; | 
				
			||||
        } | 
				
			||||
        return WEXITSTATUS(_status); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    std::pair<bool, int> poll() { | 
				
			||||
        if (!_done) { | 
				
			||||
            if(waitpid(_pid, &_status, WNOHANG) == 0) | 
				
			||||
                return {false, 0}; | 
				
			||||
            if (cleanup) { | 
				
			||||
                auto [f, arg] = cleanup.value(); | 
				
			||||
                f(arg); | 
				
			||||
                cleanup = {}; | 
				
			||||
            } | 
				
			||||
            _done = true; | 
				
			||||
        } | 
				
			||||
        return {true, WEXITSTATUS(_status)}; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pid_t pid() { | 
				
			||||
        return _pid; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int status() { | 
				
			||||
        return WEXITSTATUS(_status); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    bool waited() { | 
				
			||||
        return _done; | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
namespace detail { | 
				
			||||
    // Cleanup
 | 
				
			||||
    struct cleanup_data { | 
				
			||||
        uint8_t *stack = nullptr; | 
				
			||||
        size_t stack_size; | 
				
			||||
 | 
				
			||||
        void *args_copy = nullptr; | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    template <typename ArgP> | 
				
			||||
    void cleanup(void *d) { | 
				
			||||
        auto data = reinterpret_cast<cleanup_data*>(d); | 
				
			||||
 | 
				
			||||
        if (data->args_copy) | 
				
			||||
            delete reinterpret_cast<ArgP>(data->args_copy); | 
				
			||||
 | 
				
			||||
        if (data->stack) | 
				
			||||
            munmap(data->stack, data->stack_size); | 
				
			||||
 | 
				
			||||
        delete data; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    template <typename ArgP> | 
				
			||||
    inline void_callback_t make_cleanup_cb(uint8_t *stack, size_t stack_size, ArgP data) { | 
				
			||||
        return { cleanup<ArgP>, new cleanup_data{stack, stack_size, data} }; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Entrypoints
 | 
				
			||||
    template <typename ArgP> | 
				
			||||
    int vclone_entry(void *arg) { | 
				
			||||
        // XXX Does this work with non-movable types?
 | 
				
			||||
        auto [f, args] = std::move(*reinterpret_cast<ArgP>(arg)); | 
				
			||||
        return std::apply(f, args); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Common work function
 | 
				
			||||
    template <typename D> | 
				
			||||
    inline child_ref do_clone(int(*entry)(void*), size_t stacksize, int flags, D *data) { | 
				
			||||
        // Allocate stack
 | 
				
			||||
        auto stack = reinterpret_cast<uint8_t*>( | 
				
			||||
            mmap(nullptr, stacksize, PROT_WRITE|PROT_READ, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0)); | 
				
			||||
        if (stack == MAP_FAILED) | 
				
			||||
            return {-ENOMEM}; | 
				
			||||
 | 
				
			||||
        // Clone
 | 
				
			||||
        // SIGCHLD is required for child_ref and cleanup to work.
 | 
				
			||||
        auto pid = ::clone(entry, stack + stacksize, SIGCHLD | flags, data); | 
				
			||||
 | 
				
			||||
        // Discard everything if failed
 | 
				
			||||
        if (pid < 0) { | 
				
			||||
            if (data) | 
				
			||||
                delete data; | 
				
			||||
            if (stack) | 
				
			||||
                munmap(stack, stacksize); | 
				
			||||
            return {pid}; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // Return child_ref with cleanup
 | 
				
			||||
        return {pid, make_cleanup_cb(stack, stacksize, data)}; | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process sharing the same virtual memory | 
				
			||||
 * @param fn The function to call in the new process | 
				
			||||
 * @param stacksize The size of the new process stack | 
				
			||||
 * @param flags The clone(2) flags (SIGCHLD|CLONE_VM implied) | 
				
			||||
 * @param args The function arguments | 
				
			||||
 */ | 
				
			||||
template <typename F, typename... Args> | 
				
			||||
child_ref vclone(F fn, size_t stacksize, long flags, Args... args) { | 
				
			||||
    auto data = new std::pair{fn, std::tuple{std::forward<Args>(args)...}}; | 
				
			||||
    return detail::do_clone(detail::vclone_entry<decltype(data)>, stacksize, CLONE_VM | flags, data); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a process sharing the same virtual memory with synchronization primitives | 
				
			||||
 * @param fn The function to call in the new process [int fn(ko::proc::sync::semapair, args...)] | 
				
			||||
 * @param stacksize The size of the new process stack | 
				
			||||
 * @param flags The clone(2) flags (SIGCHLD|CLONE_VM implied) | 
				
			||||
 * @param args The function arguments | 
				
			||||
 */ | 
				
			||||
template <typename F, typename... Args> | 
				
			||||
std::pair<child_ref, sync::semapair> svclone(F fn, size_t stacksize, long flags, Args... args) { | 
				
			||||
    auto [sem_a, sem_b] = sync::make_semapair(false); | 
				
			||||
    auto data = new std::pair{fn, std::tuple{sem_b, std::forward<Args>(args)...}}; | 
				
			||||
    return {detail::do_clone(detail::vclone_entry<decltype(data)>, stacksize, CLONE_VM | flags, data), sem_a}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/**
 | 
				
			||||
 * Spawn a child process and immediately execvp() a new image | 
				
			||||
 * @param argv The argument list for the new process. | 
				
			||||
 * @note argv[0] is used as the image name/path | 
				
			||||
 */ | 
				
			||||
child_ref simple_spawn(const char *const *argv) { | 
				
			||||
    auto pid = ::fork(); | 
				
			||||
    if (pid == 0) | 
				
			||||
        ::_exit(::execvp(argv[0], const_cast<char *const*>(argv))); | 
				
			||||
    return {pid}; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
} // namespace ko::proc (ko::proc)
 | 
				
			||||
@ -0,0 +1,90 @@ | 
				
			||||
// ============================================================================
 | 
				
			||||
// ko::util koutil.hpp
 | 
				
			||||
// (c) 2019 Taeyeon Mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
// ============================================================================
 | 
				
			||||
// Miscellaneous utilities
 | 
				
			||||
 | 
				
			||||
#pragma once | 
				
			||||
 | 
				
			||||
#include <cstring> | 
				
			||||
#include <sstream> | 
				
			||||
#include <string> | 
				
			||||
#include <utility> | 
				
			||||
#include <tuple> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace ko::util { | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Misc.
 | 
				
			||||
/// Build a string from fragments using ostringstream
 | 
				
			||||
template <typename... Args> | 
				
			||||
inline std::string str(Args... args) { | 
				
			||||
    auto sstream = std::ostringstream(); | 
				
			||||
    (sstream <<...<< args); | 
				
			||||
    return sstream.str(); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// ------------------------------------------------------------------
 | 
				
			||||
// Cvresult
 | 
				
			||||
/// A more verbose result type with a very terse error location indicator
 | 
				
			||||
using cvresult = std::pair<int, const char *>; | 
				
			||||
 | 
				
			||||
/// Allows short-circuiting c-style return values
 | 
				
			||||
struct cvshort { | 
				
			||||
    int _state = 0; | 
				
			||||
    const char *_where = nullptr; | 
				
			||||
 | 
				
			||||
    template <typename F, typename... Args> | 
				
			||||
    inline cvshort &then(F fn, Args... args) { | 
				
			||||
        if (_state == 0) | 
				
			||||
            std::tie(_state, _where) = fn(args...); | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    template <typename F, typename... Args> | 
				
			||||
    inline cvshort &then(const char *name, F fn, Args... args) { | 
				
			||||
        if (_state == 0) { | 
				
			||||
            _state = fn(args...); | 
				
			||||
            if (_state != 0) | 
				
			||||
                _where = name; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    template <typename F, typename... Args> | 
				
			||||
    inline cvshort &ifthen(bool cond, F fn, Args... args) { | 
				
			||||
        if (_state == 0 && cond) | 
				
			||||
            std::tie(_state, _where) = fn(args...); | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    template <typename F, typename... Args> | 
				
			||||
    inline cvshort &ifthen(const char *name, bool cond, F fn, Args... args) { | 
				
			||||
        if (_state == 0 && cond) { | 
				
			||||
            _state = fn(args...); | 
				
			||||
            if (_state != 0) | 
				
			||||
                _where = name; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return *this; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    operator bool() const { | 
				
			||||
        return _state == 0; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int state() const { | 
				
			||||
        return _state; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    const char *where() const { | 
				
			||||
        return _where; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    operator cvresult() { | 
				
			||||
        return {_state, _where}; | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
} // namespace ko::util
 | 
				
			||||
@ -0,0 +1,95 @@ | 
				
			||||
# ===================================================================
 | 
				
			||||
# Taeyeon's miscallaneous applications
 | 
				
			||||
# (c) 2019 Taeyeon Mori
 | 
				
			||||
# ===================================================================
 | 
				
			||||
PROGS = keepassxc-print steamns chome overlayns
 | 
				
			||||
PROGS_ALL = fakensudo ssh-overlay-kiosk overlayns-static
 | 
				
			||||
 | 
				
			||||
# Compiler config
 | 
				
			||||
CXX = clang++
 | 
				
			||||
CXXFLAGS = -std=c++20 -Wall
 | 
				
			||||
OPTIMIZE = -O3 -flto
 | 
				
			||||
DEBUG = -DDEBUG -g3
 | 
				
			||||
 | 
				
			||||
# Install config
 | 
				
			||||
INSTALL_PATH ?= ~/.local/bin
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
# -------------------------------------------------------------------
 | 
				
			||||
# Common targets
 | 
				
			||||
.PHONY: all most install clean | 
				
			||||
most: $(PROGS) | 
				
			||||
 | 
				
			||||
all: $(PROGS) $(PROGS_ALL) | 
				
			||||
 | 
				
			||||
install: $(PROGS) | 
				
			||||
	@echo "Installing to INSTALL_PATH = $(INSTALL_PATH)"
 | 
				
			||||
	@mkdir -p $(INSTALL_PATH)
 | 
				
			||||
	@bash -c 'for prog in $(PROGS); do test -e $$prog && echo "Install $$prog -> $(INSTALL_PATH)" && install -m755 $$prog $(INSTALL_PATH); done'
 | 
				
			||||
 | 
				
			||||
install-fake-sudo: fakensudo | 
				
			||||
	install -m755 $< /usr/local/bin/sudo
 | 
				
			||||
 | 
				
			||||
clean: | 
				
			||||
	rm $(PROGS) $(PROGS_ALL)
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
# ------------------------------------------------------------------
 | 
				
			||||
# Dependencies
 | 
				
			||||
dep_koutil			= koutil.hpp
 | 
				
			||||
flg_koutil			=
 | 
				
			||||
 | 
				
			||||
dep_kofs			= kofs.hpp
 | 
				
			||||
flg_kofs			=
 | 
				
			||||
 | 
				
			||||
dep_koos			= koos.hpp $(dep_kofs)
 | 
				
			||||
flg_koos			= $(flg_kofs)
 | 
				
			||||
 | 
				
			||||
dep_kofd			= kofd.hpp $(dep_kofs)
 | 
				
			||||
flg_kofd			= $(flg_kofs)
 | 
				
			||||
 | 
				
			||||
dep_kofd_pipe		= kofd_pipe.hpp $(dep_kofd)
 | 
				
			||||
flg_kofd_pipe		= $(flg_kofd)
 | 
				
			||||
 | 
				
			||||
dep_koproc			= koproc.hpp $(dep_kofd_pipe)
 | 
				
			||||
flg_koproc			= -pthread $(flg_kofd_pipe)
 | 
				
			||||
 | 
				
			||||
dep_kons			= kons.hpp $(dep_koutil) $(dep_kofd) $(dep_koos)
 | 
				
			||||
flg_kons			= $(flg_koutil) $(flg_kofd) $(flg_koos)
 | 
				
			||||
 | 
				
			||||
dep_kons_clone		= kons_clone.hpp $(dep_kons) $(dep_koproc)
 | 
				
			||||
flg_kons_clone		= $(flg_kons) $(flg_koproc)
 | 
				
			||||
 | 
				
			||||
dep_keepassxc		= keepassxc-browser.hpp $(dep_koproc)
 | 
				
			||||
flg_keepassxc		= $(shell pkg-config --cflags --libs libsodium jsoncpp) $(flg_koproc)
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
# -------------------------------------------------------------------
 | 
				
			||||
# Applications
 | 
				
			||||
keepassxc-print: keepassxc-print.cpp $(dep_keepassxc) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_keepassxc) -o $@ $<
 | 
				
			||||
 | 
				
			||||
steamns: steamns.cpp $(dep_kons_clone) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons_clone) -o $@ $<
 | 
				
			||||
 | 
				
			||||
MOverlay2-nsexec: MOverlay2-nsexec.cpp $(dep_kons_clone) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons_clone) -o $@ $<
 | 
				
			||||
 | 
				
			||||
chome: chome.cpp $(dep_koutil) $(dep_kofd) $(dep_koos) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_koutil) $(flg_kofd) $(flg_koos) -o $@ $<
 | 
				
			||||
 | 
				
			||||
fakensudo: fakensudo.cpp $(dep_kons) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) -o $@ $<
 | 
				
			||||
 | 
				
			||||
ssh-overlay-kiosk: ssh-overlay-kiosk.cpp $(dep_koutil) $(dep_kofd) $(dep_koos) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_koutil) $(flg_kofd) $(flg_koos) -o $@ $<
 | 
				
			||||
	@echo Setting $@ setuid root
 | 
				
			||||
	sudo chown root $@
 | 
				
			||||
	sudo chmod u+s $@
 | 
				
			||||
 | 
				
			||||
overlayns: overlayns.cpp $(dep_kons) $(dep_koproc) | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) $(flg_koproc) -o $@ $<
 | 
				
			||||
 | 
				
			||||
overlayns-static: overlayns.cpp $(dep_kons) $(dep_koproc) makefile | 
				
			||||
	$(CXX) $(CXXFLAGS) $(OPTIMIZE) $(flg_kons) $(flg_koproc) -static -fdata-sections -ffunction-sections -Wl,--gc-sections -o $@ $<
 | 
				
			||||
 | 
				
			||||
@ -0,0 +1,431 @@ | 
				
			||||
// overlayns
 | 
				
			||||
// (c) 2021 Taeyeon Mori
 | 
				
			||||
 | 
				
			||||
#include <string> | 
				
			||||
#include <string_view> | 
				
			||||
#include <vector> | 
				
			||||
#include <unordered_map> | 
				
			||||
#include <list> | 
				
			||||
#include <variant> | 
				
			||||
#include <span> | 
				
			||||
#include <algorithm> | 
				
			||||
#include <numeric> | 
				
			||||
#include <spawn.h> | 
				
			||||
 | 
				
			||||
#include "kons_clone.hpp" | 
				
			||||
 | 
				
			||||
using namespace ko; | 
				
			||||
using namespace std::literals::string_literals; | 
				
			||||
using namespace std::literals::string_view_literals; | 
				
			||||
 | 
				
			||||
static constexpr auto vers = "0.5"sv; | 
				
			||||
 | 
				
			||||
void usage(char const * prog) { | 
				
			||||
    printf("Synopsis: %s [-h] [-o ovl-spec]... [-m mnt-spec]... <command...>\n" | 
				
			||||
           "\n" | 
				
			||||
           "Run a command in it's own mount namespace\n" | 
				
			||||
           "\n" | 
				
			||||
           "Spec options:\n" | 
				
			||||
           "    -m mnt-spec     Add a mount to the namespace\n" | 
				
			||||
           "    -o ovl-spec     Add an overlay to the namespace\n" | 
				
			||||
           "\n" | 
				
			||||
           "Mount spec:\n" | 
				
			||||
           "    A mount specification takes the following format:\n" | 
				
			||||
           "      -m <fstype>,<device>,<mountpoint>[,<option>...]\n" | 
				
			||||
           "    see mount(8) for more information on options.\n" | 
				
			||||
           "    Some options may not match exactly however.\n" | 
				
			||||
           "    Shortcuts are in place for bind mounts:\n" | 
				
			||||
           "      `-m bind,/a,/b` is equivalent to `-m ,/a,/b,bind`\n" | 
				
			||||
           "      `-m rbind,/a,/b` is equivalent to `-m ,/a,/b,bind,rec`\n" | 
				
			||||
           "\n" | 
				
			||||
           "Overlay spec:\n" | 
				
			||||
           "    An overlay specification takes the following form:\n" | 
				
			||||
           "      -o <mountpoint>,<option>...\n" | 
				
			||||
           "    Avaliable options are (in addition to standard mount options):\n" | 
				
			||||
           "      lowerdir=<path>   Mandatory, see mount(8)\n" | 
				
			||||
           "      upperdir=<path>   Mandatory, see mount(8)\n" | 
				
			||||
           "      workdir=<path>    Mandatory, see mount(8)\n" | 
				
			||||
           "      shadow            Replaces lowerdir=; Use mountpoint as lowerdir\n" | 
				
			||||
           "                        and shadow it's content\n" | 
				
			||||
           "      tmp               Replaces upperdir= and workdir=;\n" | 
				
			||||
           "                        Use a (new) temporary directory for both\n" | 
				
			||||
           "      copyfrom=<path>   Copy contents of <path> to upperdir before mounting\n" | 
				
			||||
           "\n" | 
				
			||||
           "overlayns %s (c) 2021 Taeyeon Mori\n" | 
				
			||||
           "\n", | 
				
			||||
           prog, vers.data()); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
auto str_split(std::string_view s, char c) { | 
				
			||||
    size_t start = 0, next = 0; | 
				
			||||
    std::vector<std::string_view> parts; | 
				
			||||
    while ((next = s.find(c, start)) != s.npos) { | 
				
			||||
        while (next > 0 && s[next-1] == '\\' && (next < 2 || s[next-2] != '\\')) { | 
				
			||||
            if ((next = s.find(c, next+1)) == s.npos) | 
				
			||||
                break; | 
				
			||||
        } | 
				
			||||
        parts.push_back(s.substr(start, next - start)); | 
				
			||||
        start = next + 1; | 
				
			||||
    } | 
				
			||||
    parts.push_back(s.substr(start)); | 
				
			||||
    return parts; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
auto str_join(const std::span<std::string_view> &ss, char c) -> std::string { | 
				
			||||
    if (ss.empty()) | 
				
			||||
        return {}; | 
				
			||||
    auto sizes = std::vector<size_t>{}; | 
				
			||||
    sizes.reserve(ss.size()); | 
				
			||||
    std::ranges::transform(ss, std::back_inserter(sizes), [](const auto& s) {return s.size();}); | 
				
			||||
    auto size = ss.size() - 1 + std::reduce(sizes.begin(), sizes.end()); | 
				
			||||
    auto result = std::string(); | 
				
			||||
    result.reserve(size); | 
				
			||||
    result = ss[0]; | 
				
			||||
    std::for_each(ss.begin()+1, ss.end(), [&result, c](const auto &s) { | 
				
			||||
        result.push_back(c); | 
				
			||||
        result.append(s); | 
				
			||||
    }); | 
				
			||||
    return result; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
struct mount_spec { | 
				
			||||
    enum class mkdir_mode { | 
				
			||||
        never, | 
				
			||||
        maybe_this, | 
				
			||||
        maybe_all, | 
				
			||||
        require_this, | 
				
			||||
        require_all, | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    std::string_view type; | 
				
			||||
    std::string_view device; | 
				
			||||
    std::string_view mountpoint; | 
				
			||||
    uint64_t flags = 0; | 
				
			||||
    std::vector<std::string_view> args; | 
				
			||||
    mkdir_mode mkdir = mkdir_mode::never; | 
				
			||||
 | 
				
			||||
    struct parse_error {std::string_view msg;}; | 
				
			||||
 | 
				
			||||
    auto apply_options(std::span<std::string_view> opts) -> std::list<parse_error> { | 
				
			||||
        static const std::unordered_map<std::string_view, uint64_t> flagnames = { | 
				
			||||
            {"remount", MS_REMOUNT}, | 
				
			||||
            {"move", MS_MOVE}, | 
				
			||||
            {"bind", MS_BIND}, | 
				
			||||
            {"rec", MS_REC}, | 
				
			||||
            // propagation
 | 
				
			||||
            {"shared", MS_SHARED}, | 
				
			||||
            {"private", MS_PRIVATE}, | 
				
			||||
            {"unbindable", MS_UNBINDABLE}, | 
				
			||||
            {"slave", MS_SLAVE}, | 
				
			||||
            // read
 | 
				
			||||
            {"rw", 0}, | 
				
			||||
            {"ro", MS_RDONLY}, | 
				
			||||
            // atime
 | 
				
			||||
            {"noatime", MS_NOATIME}, | 
				
			||||
            {"nodiratime", MS_NODIRATIME}, | 
				
			||||
            {"relatime", MS_RELATIME}, | 
				
			||||
            {"strictatime", MS_STRICTATIME}, | 
				
			||||
            // filetypes
 | 
				
			||||
            {"nodev", MS_NODEV}, | 
				
			||||
            {"noexec", MS_NOEXEC}, | 
				
			||||
            {"nosuid", MS_NOSUID}, | 
				
			||||
            // misc
 | 
				
			||||
            {"dirsync", MS_DIRSYNC}, | 
				
			||||
            {"lazytime", MS_LAZYTIME}, | 
				
			||||
            {"silent", MS_SILENT}, | 
				
			||||
            {"synchronous", MS_SYNCHRONOUS}, | 
				
			||||
            {"mandlock", MS_MANDLOCK}, | 
				
			||||
        }; | 
				
			||||
 | 
				
			||||
        std::list<parse_error> errors; | 
				
			||||
        for (const std::string_view &opt : opts) { | 
				
			||||
            if (opt.starts_with("mkdir=")) { | 
				
			||||
                auto arg = opt.substr(6); | 
				
			||||
                if (arg == "never") { | 
				
			||||
                    mkdir = mkdir_mode::never; | 
				
			||||
                } else if (arg == "maybe") { | 
				
			||||
                    mkdir = mkdir_mode::maybe_all; | 
				
			||||
                } else if (arg == "require") { | 
				
			||||
                    mkdir = mkdir_mode::require_all; | 
				
			||||
                } else { | 
				
			||||
                    errors.push_back({"Unknown mkdir= argument"}); | 
				
			||||
                } | 
				
			||||
            } else if (auto f = flagnames.find(opt); f != flagnames.end()) { | 
				
			||||
                flags |= f->second; | 
				
			||||
            } else { | 
				
			||||
                args.push_back(opt); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return errors; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    static auto parse(std::string_view s) -> std::pair<mount_spec, std::list<parse_error>> { | 
				
			||||
        auto parts = str_split(s, ','); | 
				
			||||
 | 
				
			||||
        if (s.size() < 3) { | 
				
			||||
            std::cerr << "Incomplete mount spec: " << s << std::endl; | 
				
			||||
            return {{}, {{"Incomplete mount spec (need at least type,device,mountpoint"}}}; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        mount_spec spec = { | 
				
			||||
            .type = parts[0], | 
				
			||||
            .device = parts[1], | 
				
			||||
            .mountpoint = parts[2], | 
				
			||||
        }; | 
				
			||||
 | 
				
			||||
        if (spec.type == "bind") { | 
				
			||||
            spec.flags |= MS_BIND; | 
				
			||||
            spec.type = ""; | 
				
			||||
        } else if (spec.type == "rbind") { | 
				
			||||
            spec.flags |= MS_BIND | MS_REC; | 
				
			||||
            spec.type = ""; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        auto errors = spec.apply_options(std::span(parts).subspan(3)); | 
				
			||||
 | 
				
			||||
        return {spec, errors}; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int execute() { | 
				
			||||
        if (!fs::exists(mountpoint)) { | 
				
			||||
            if (mkdir == mkdir_mode::maybe_all || mkdir == mkdir_mode::require_all) { | 
				
			||||
                fs::create_directories(mountpoint); | 
				
			||||
            } else if (mkdir == mkdir_mode::maybe_this || mkdir == mkdir_mode::require_this) { | 
				
			||||
                fs::create_directory(mountpoint); | 
				
			||||
            } else { | 
				
			||||
                std::cerr << "Mountpoint doesn't exist: " << mountpoint; | 
				
			||||
                return 41; | 
				
			||||
            } | 
				
			||||
        } else if (mkdir == mkdir_mode::require_this || mkdir == mkdir_mode::require_all) { | 
				
			||||
            std::cerr << "Mountpoint exists but was required to be created: " << mountpoint; | 
				
			||||
            return 41; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        std::string fstype{type}, | 
				
			||||
                    dev{device}, | 
				
			||||
                    dest{mountpoint}, | 
				
			||||
                    margs = str_join(args, ','); | 
				
			||||
        
 | 
				
			||||
        //std::cerr << "Mount -t " << fstype << " " << dev << " " << dest << " -o " << margs << " -f " << flags << std::endl;
 | 
				
			||||
 | 
				
			||||
        auto res = os::mount(dev, dest, fstype.c_str(), flags, (void*)(margs.empty() ? nullptr : margs.c_str())); | 
				
			||||
        if (res) { | 
				
			||||
            std::cerr << "Failed mounting " << dev << " on " << dest << std::endl; | 
				
			||||
            perror("mount"); | 
				
			||||
            return res; | 
				
			||||
        } | 
				
			||||
        return 0; | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
struct copy_spec { | 
				
			||||
    std::string_view source; | 
				
			||||
    std::string_view dest; | 
				
			||||
 | 
				
			||||
    int execute() { | 
				
			||||
        std::error_code ec; | 
				
			||||
        fs::copy(source, dest, fs::copy_options::recursive, ec); | 
				
			||||
        if (ec.value()) | 
				
			||||
            std::cerr << "Could not copy " << source << " to " << dest << ": " << ec.message() << std::endl; | 
				
			||||
        return ec.value(); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
struct config { | 
				
			||||
    using step = std::variant<mount_spec, copy_spec>; | 
				
			||||
    std::list<step> recipe; | 
				
			||||
    std::list<fs::path> cleanup; | 
				
			||||
    char const * const * cmdline; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// Null coalescing helper
 | 
				
			||||
template <typename T> | 
				
			||||
T *nc(T *a, T *dflt) { | 
				
			||||
    return a ? a : dflt; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
std::list<std::string> strings_g; | 
				
			||||
 | 
				
			||||
auto parse_overlay_spec(std::string_view s, config &cfg) -> std::list<mount_spec::parse_error> { | 
				
			||||
    auto parts = str_split(s, ','); | 
				
			||||
 | 
				
			||||
    if (parts.size() < 1) | 
				
			||||
        return {{"Incomplete overlay spec"}}; | 
				
			||||
 | 
				
			||||
    mount_spec mspec = {"overlay", "overlay", parts[0]}; | 
				
			||||
 | 
				
			||||
    struct { | 
				
			||||
        std::string_view lowerdir; | 
				
			||||
        std::string_view upperdir; | 
				
			||||
        std::string_view workdir; | 
				
			||||
        bool tmp = false, shadow = false; | 
				
			||||
        std::string_view copy_from; | 
				
			||||
    } x; | 
				
			||||
 | 
				
			||||
    auto options = std::vector<std::string_view>{}; | 
				
			||||
    options.reserve(parts.size()); | 
				
			||||
 | 
				
			||||
    std::copy_if(parts.begin()+1, parts.end(), std::back_inserter(options), [&x](const auto &opt) { | 
				
			||||
        if (opt.starts_with("lowerdir=")) { | 
				
			||||
            x.lowerdir = opt; | 
				
			||||
        } else if (opt.starts_with("upperdir=")) { | 
				
			||||
            x.upperdir = opt; | 
				
			||||
        } else if (opt.starts_with("workdir=")) { | 
				
			||||
            x.workdir = opt; | 
				
			||||
        } else if (opt.starts_with("copyfrom=")) { | 
				
			||||
            x.copy_from = opt.substr(9); | 
				
			||||
        } else if (opt == "tmp") { | 
				
			||||
            x.tmp = true; | 
				
			||||
        } else if (opt == "shadow") { | 
				
			||||
            x.shadow = true; | 
				
			||||
        } else { | 
				
			||||
            return true; | 
				
			||||
        } | 
				
			||||
        return false; | 
				
			||||
    }); | 
				
			||||
 | 
				
			||||
    static constexpr auto lowerdir_opt = "lowerdir="sv; | 
				
			||||
    if (x.shadow) { | 
				
			||||
        // lowerdir == mountpoint
 | 
				
			||||
        auto& s = strings_g.emplace_back(); | 
				
			||||
        s.reserve(x.lowerdir.empty() ? lowerdir_opt.size() + mspec.mountpoint.size() : x.lowerdir.size() + mspec.mountpoint.size()); | 
				
			||||
        s = lowerdir_opt; | 
				
			||||
        s += mspec.mountpoint; | 
				
			||||
        if (!x.lowerdir.empty()) { | 
				
			||||
            s += ":"; | 
				
			||||
            s += x.lowerdir.substr(lowerdir_opt.size()); | 
				
			||||
        } | 
				
			||||
        x.lowerdir = s; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    static constexpr auto upperdir_opt = "upperdir="sv; | 
				
			||||
    static constexpr auto upperdir_name = "/upper"sv; | 
				
			||||
    static constexpr auto workdir_opt = "workdir="sv; | 
				
			||||
    static constexpr auto workdir_name = "/work"sv; | 
				
			||||
    if (x.tmp) { | 
				
			||||
        auto tmpdir = std::string{nc((const char*)getenv("TMPDIR"), "/tmp")}; | 
				
			||||
        tmpdir.append("/overlayns-XXXXXX"sv); | 
				
			||||
        if (!mkdtemp(tmpdir.data())) { | 
				
			||||
            return {{"Could not create temporary directory for 'tmp' overlay option"sv}}; | 
				
			||||
        } | 
				
			||||
        auto& upperdir = strings_g.emplace_back(); | 
				
			||||
        upperdir.reserve(upperdir_opt.size() + tmpdir.size() + upperdir_name.size()); | 
				
			||||
        upperdir = upperdir_opt; | 
				
			||||
        upperdir += tmpdir; | 
				
			||||
        upperdir += upperdir_name; | 
				
			||||
        x.upperdir = upperdir; | 
				
			||||
        fs::create_directory(x.upperdir.substr(upperdir_opt.size())); | 
				
			||||
        auto& workdir = strings_g.emplace_back(); | 
				
			||||
        workdir.reserve(workdir_opt.size() + tmpdir.size() + workdir_name.size()); | 
				
			||||
        workdir = workdir_opt; | 
				
			||||
        workdir += tmpdir; | 
				
			||||
        workdir += workdir_name; | 
				
			||||
        x.workdir = workdir; | 
				
			||||
        fs::create_directory(x.workdir.substr(workdir_opt.size())); | 
				
			||||
        cfg.cleanup.emplace_back(tmpdir); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    std::list<mount_spec::parse_error> errors; | 
				
			||||
 | 
				
			||||
    if (x.lowerdir.empty()) { | 
				
			||||
        errors.push_back({"Missing lowerdir option"sv}); | 
				
			||||
    } else { | 
				
			||||
        mspec.args.push_back(x.lowerdir); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (x.upperdir.empty() != x.workdir.empty()) { | 
				
			||||
        errors.push_back({"Must specify upperdir and workdir both or neither"sv}); | 
				
			||||
    } else if (!x.upperdir.empty()) { | 
				
			||||
        mspec.args.push_back(x.upperdir); | 
				
			||||
        mspec.args.push_back(x.workdir); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (!errors.empty()) { | 
				
			||||
        return errors; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (!x.copy_from.empty()) { | 
				
			||||
        cfg.recipe.emplace_back(copy_spec{x.copy_from, x.upperdir.substr(upperdir_opt.size())}); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    cfg.recipe.emplace_back(mspec); | 
				
			||||
 | 
				
			||||
    return errors; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
int main(int argc, char*const* argv) { | 
				
			||||
    config cfg; | 
				
			||||
 | 
				
			||||
    // Commandline parsing
 | 
				
			||||
    constexpr auto argspec = "+ho:m:"; | 
				
			||||
 | 
				
			||||
    for (auto opt = ::getopt(argc, argv, argspec); opt != -1; opt = ::getopt(argc, argv, argspec)) { | 
				
			||||
        if (opt == 'h' || opt == '?') { | 
				
			||||
            usage(argv[0]); | 
				
			||||
            return opt == '?' ? 1 : 0; | 
				
			||||
        } else if (opt == 'o') { | 
				
			||||
            auto err = parse_overlay_spec(::optarg, cfg); | 
				
			||||
            if (!err.empty()) { | 
				
			||||
                std::cerr << "Error parsing overlay spec: " << ::optarg << std::endl; | 
				
			||||
                for (const auto &e : err) { | 
				
			||||
                    std::cerr << "  " << e.msg << std::endl; | 
				
			||||
                } | 
				
			||||
                return 33; | 
				
			||||
            } | 
				
			||||
        } else if (opt == 'm') { | 
				
			||||
            auto [spec, err] = mount_spec::parse(::optarg); | 
				
			||||
            if (!err.empty()) { | 
				
			||||
                std::cerr << "Error parsing mount spec: " << ::optarg << std::endl; | 
				
			||||
                for (const auto &e : err) { | 
				
			||||
                    std::cerr << "  " << e.msg << std::endl; | 
				
			||||
                } | 
				
			||||
                return 33; | 
				
			||||
            } else { | 
				
			||||
                cfg.recipe.push_back({spec}); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
    cfg.cmdline = &argv[::optind]; | 
				
			||||
 | 
				
			||||
    if (!cfg.cmdline[0]) { | 
				
			||||
        std::cerr << "Missing child commandline" << std::endl; | 
				
			||||
        return 22; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Unshare
 | 
				
			||||
    uid_t uid = getuid(); | 
				
			||||
    gid_t gid = getgid(); | 
				
			||||
 | 
				
			||||
    auto [child, ret] = ns::clone::uvclone_single(uid, gid, [&cfg](){ | 
				
			||||
        // Execute recipe
 | 
				
			||||
        for (auto &step : cfg.recipe) { | 
				
			||||
            int res = 0; | 
				
			||||
            std::visit([&res](auto &spec) { | 
				
			||||
                res = spec.execute(); | 
				
			||||
            }, step); | 
				
			||||
            if (res) | 
				
			||||
                return res; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        return ::execvp(cfg.cmdline[0], const_cast<char*const*>(cfg.cmdline)); | 
				
			||||
    }, 102400, CLONE_NEWNS); | 
				
			||||
 | 
				
			||||
    if (ret) | 
				
			||||
        return ret; | 
				
			||||
 | 
				
			||||
    // free memory
 | 
				
			||||
    //cfg.recipe.clear();
 | 
				
			||||
    //strings_g.clear();
 | 
				
			||||
    
 | 
				
			||||
    // execute child
 | 
				
			||||
    ret = child.wait(); | 
				
			||||
 | 
				
			||||
    std::ranges::for_each(cfg.cleanup, [](const auto& p) {fs::remove_all(p);}); | 
				
			||||
 | 
				
			||||
    return ret; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,192 @@ | 
				
			||||
// (c) 2020 Taeyeon Mori
 | 
				
			||||
// 
 | 
				
			||||
 | 
				
			||||
#include "koutil.hpp" | 
				
			||||
#include "kofs.hpp" | 
				
			||||
#include "kofd.hpp" | 
				
			||||
#include "koos.hpp" | 
				
			||||
 | 
				
			||||
#include <cstdlib> | 
				
			||||
#include <sstream> | 
				
			||||
#include <iostream> | 
				
			||||
#include <filesystem> | 
				
			||||
 | 
				
			||||
#include <unistd.h> | 
				
			||||
#include <pwd.h> | 
				
			||||
#include <mntent.h> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
struct params { | 
				
			||||
    std::filesystem::path motd; | 
				
			||||
    bool ro = true; | 
				
			||||
    bool protect = true; | 
				
			||||
    char *const *argv = nullptr; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
void usage(const char *prog) { | 
				
			||||
    std::cout << "Usage: " << prog << " [-m MOTD] [ARGV...]" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "Options:" << std::endl | 
				
			||||
              << "    -m MOTD     Specify a file to be displayed on login" << std::endl | 
				
			||||
              << "    ARGV        Specify the shell executable and arguments" << std::endl | 
				
			||||
              << "                By default, the shell from /etc/passwd is used with argument -l" << std::endl | 
				
			||||
              ; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
params parse_args(int argc, char **argv) { | 
				
			||||
    params p{}; | 
				
			||||
 | 
				
			||||
    constexpr auto spec = "+hm:"; | 
				
			||||
 | 
				
			||||
    while (true) { | 
				
			||||
        auto opt = getopt(argc, const_cast<char *const *>(argv), spec); | 
				
			||||
 | 
				
			||||
        if (opt == -1) | 
				
			||||
            break; | 
				
			||||
        else if (opt == '?' || opt == 'h') { | 
				
			||||
            usage(argv[0]); | 
				
			||||
            exit(opt == 'h' ? 0 : 1); | 
				
			||||
        } else if (opt == 'm') { | 
				
			||||
            p.motd = ::optarg; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (argc > ::optind) | 
				
			||||
        p.argv = const_cast<char *const *>(&argv[::optind]); | 
				
			||||
    
 | 
				
			||||
    return p; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Helpers
 | 
				
			||||
int xerror(const char *desc) { | 
				
			||||
    perror(desc); | 
				
			||||
    return -errno; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die(int r, const char *msg) { | 
				
			||||
    std::cerr << msg << std::endl; | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die_errno(int r, const char *msg) { | 
				
			||||
    perror(msg); | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
struct mntent_context { | 
				
			||||
    FILE *mounts; | 
				
			||||
 | 
				
			||||
    mntent_context(char const *fname) { | 
				
			||||
        mounts = setmntent(fname, "r"); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    operator bool() { | 
				
			||||
        return mounts != nullptr; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    ~mntent_context() { | 
				
			||||
        if (mounts != nullptr) | 
				
			||||
            endmntent(mounts); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    mntent *next() { | 
				
			||||
        return getmntent(mounts); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
int ro_all_mounts() { | 
				
			||||
    // Change all current mounts to readonly
 | 
				
			||||
    auto mounts = mntent_context("/proc/mounts"); | 
				
			||||
    if (!mounts) | 
				
			||||
        return 1; | 
				
			||||
    
 | 
				
			||||
    mntent *ent; | 
				
			||||
    while ((ent = mounts.next()) != nullptr) { | 
				
			||||
        if (ko::os::bind(ent->mnt_dir, ent->mnt_dir, MS_REMOUNT|MS_RDONLY)) | 
				
			||||
            return 1; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    return 0; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
int protect_self() { | 
				
			||||
    // Hide self by by bind-mounting /dev/zero on top. Make it harder to exploit any vulns, just in case
 | 
				
			||||
    // Though this does give away the name of the executable...
 | 
				
			||||
    auto path = ko::fd::readlink("/proc/self/exe"); | 
				
			||||
    if (path.empty()) | 
				
			||||
        return 1; | 
				
			||||
    return ko::os::bind("/dev/null", path); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
int main(int argc, char **argv) { | 
				
			||||
    params p = parse_args(argc, argv); | 
				
			||||
    uid_t ruid, euid, suid; | 
				
			||||
    gid_t rgid, egid, sgid; | 
				
			||||
    passwd *passwd; | 
				
			||||
    std::string options; | 
				
			||||
    const char *default_shell_argv[] = {nullptr, "-l", nullptr}; // gets executable name from user passwd record
 | 
				
			||||
 | 
				
			||||
    // Use shell from passwd if no command is given in argv
 | 
				
			||||
    if (p.argv == nullptr) | 
				
			||||
        p.argv = const_cast<char *const*>(default_shell_argv); | 
				
			||||
 | 
				
			||||
    auto [e, eloc] = ko::util::cvshort() | 
				
			||||
        .then("getresuid",          getresuid, &ruid, &euid, &suid) | 
				
			||||
        .then("getresgid",          getresgid, &rgid, &egid, &sgid) | 
				
			||||
        .then("getpwuid",           [ruid,euid,&passwd]() { | 
				
			||||
            // Check root perms
 | 
				
			||||
            if (euid != 0) | 
				
			||||
                die(3, "Must be suid root"); | 
				
			||||
            // Retrieve user info
 | 
				
			||||
            errno = 0; | 
				
			||||
            passwd = getpwuid(ruid); | 
				
			||||
            if (errno != 0) | 
				
			||||
                return 5; | 
				
			||||
            else if (passwd == nullptr) | 
				
			||||
                die(4, "Calling user ID not known to system"); | 
				
			||||
            return 0; | 
				
			||||
        }) | 
				
			||||
        .then("setegid",            ::setegid, 0) | 
				
			||||
        .then("unshare",            ::unshare, CLONE_NEWNS) | 
				
			||||
        .then("make ns slave",      ko::os::mount, "", "/", "", MS_REC|MS_SLAVE, nullptr) | 
				
			||||
        .ifthen("make fs readonly", p.ro, ro_all_mounts) | 
				
			||||
        .ifthen("protect self",     p.protect, protect_self) | 
				
			||||
        .then("mount tmp",          ko::os::mount, "tmpfs", "/tmp", "tmpfs", MS_NOEXEC|MS_NODEV|MS_NOSUID, nullptr) | 
				
			||||
        .then([ruid,rgid,suid,&options,passwd,&default_shell_argv]() -> ko::util::cvresult { | 
				
			||||
            // Create directories
 | 
				
			||||
            auto d = ko::fd::opendir("/tmp"); | 
				
			||||
            auto r = ko::util::cvshort() | 
				
			||||
                .then("fchown tmp", ::fchown, (int)d, ruid, rgid) | 
				
			||||
                .then("setegid",    ::setegid, rgid) | 
				
			||||
                .then("seteuid",    ::seteuid, ruid) | 
				
			||||
                .then("mkdir .home",  ko::fd::mkdir, ".home", 0750, (int)d) | 
				
			||||
                .then("mkdir work",   ko::fd::mkdir, ".home/work", 0750, (int)d) | 
				
			||||
                .then("mkdir top",    ko::fd::mkdir, ".home/top", 0750, (int)d) | 
				
			||||
                .then("seteuid root", ::seteuid, suid); | 
				
			||||
            if (r) { | 
				
			||||
                // Build option string
 | 
				
			||||
                options = ko::util::str("lowerdir=", passwd->pw_dir, ",upperdir=/tmp/.home/top,workdir=/tmp/.home/work"); | 
				
			||||
                // Use shell from passwd
 | 
				
			||||
                default_shell_argv[0] = passwd->pw_shell; | 
				
			||||
            } | 
				
			||||
            return r; | 
				
			||||
        }) | 
				
			||||
        .then("mount overlay",      ko::os::mount, "overlay", passwd->pw_dir, "overlay", 0, (void*)options.c_str()) | 
				
			||||
        .ifthen("show motd",        !p.motd.empty(), [&p]() { | 
				
			||||
            auto f = ko::fd::open(p.motd, O_RDONLY); | 
				
			||||
            if (!f) | 
				
			||||
                return 1; | 
				
			||||
            struct stat st; | 
				
			||||
            if (::fstat(f, &st)) | 
				
			||||
                return 1; | 
				
			||||
            return ko::fd::fcopy(f, STDOUT_FILENO, st.st_size) ? 0 : 1; | 
				
			||||
        }) | 
				
			||||
        .then("chdir home",         ::chdir, passwd->pw_dir) | 
				
			||||
        .then("drop gid",           ::setresgid, rgid, rgid, rgid) | 
				
			||||
        .then("drop uid",           ::setresuid, ruid, ruid, ruid) | 
				
			||||
        .then("exec",               ::execvp, p.argv[0], p.argv); | 
				
			||||
 | 
				
			||||
    perror(eloc); | 
				
			||||
    return e; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,591 @@ | 
				
			||||
// Isolate steam in a namespace
 | 
				
			||||
// (c) 2019 Taeyeon mori <taeyeon at oro.sodimm.me>
 | 
				
			||||
 | 
				
			||||
#include "kons_clone.hpp" | 
				
			||||
 | 
				
			||||
#include <cstdlib> | 
				
			||||
#include <sstream> | 
				
			||||
#include <iostream> | 
				
			||||
#include <fstream> | 
				
			||||
#include <filesystem> | 
				
			||||
 | 
				
			||||
#include <unistd.h> | 
				
			||||
#include <sys/signalfd.h> | 
				
			||||
 | 
				
			||||
 | 
				
			||||
namespace fs = std::filesystem; | 
				
			||||
 | 
				
			||||
// TODO: XDG_RUNTIME_DIR etc
 | 
				
			||||
 | 
				
			||||
constexpr auto ROOT_DIR = ".local/steam"; | 
				
			||||
constexpr auto DEFAULT_CMD = (const char*[]){"/bin/bash", nullptr}; | 
				
			||||
constexpr auto STEAM_USER = "steamuser"; | 
				
			||||
 | 
				
			||||
// Helpers
 | 
				
			||||
int xerror(const char *desc) { | 
				
			||||
    perror(desc); | 
				
			||||
    return -errno; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die(int r, const char *msg) { | 
				
			||||
    std::cerr << msg << std::endl; | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
[[noreturn]] void die_errno(int r, const char *msg) { | 
				
			||||
    perror(msg); | 
				
			||||
    exit(r); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// Report to parent process
 | 
				
			||||
template <typename T> | 
				
			||||
class proc_future { | 
				
			||||
    sem_t ready; | 
				
			||||
    T value; | 
				
			||||
    
 | 
				
			||||
    proc_future(int shared) { | 
				
			||||
        sem_init(&ready, shared, 0); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    ~proc_future() { | 
				
			||||
        sem_destroy(&ready); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
public: | 
				
			||||
    T wait() { | 
				
			||||
        sem_wait(&ready); | 
				
			||||
        return value; | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    void post(const T &v) { | 
				
			||||
        value = v; | 
				
			||||
        sem_post(&ready); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    static proc_future<T> *create() { | 
				
			||||
        auto shm = mmap(nullptr, sizeof(proc_future<T>), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0); | 
				
			||||
        if (shm == MAP_FAILED) | 
				
			||||
            return nullptr; | 
				
			||||
        return new (shm) proc_future<T>(1); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // in shared VM, unmap() must be skipped
 | 
				
			||||
    static proc_future<T> *create_private() { | 
				
			||||
        auto shm = mmap(nullptr, sizeof(proc_future<T>), PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_PRIVATE, -1, 0); | 
				
			||||
        if (shm == MAP_FAILED) | 
				
			||||
            return nullptr; | 
				
			||||
        return new (shm) proc_future<T>(0); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // Waiting process must call destroy()
 | 
				
			||||
    void destroy() { | 
				
			||||
        this->~proc_future(); | 
				
			||||
        munmap(this, sizeof(proc_future<T>)); | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // Posting process must call unmap() instead
 | 
				
			||||
    void unmap() { | 
				
			||||
        munmap(this, sizeof(proc_future<T>)); | 
				
			||||
    } | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
 | 
				
			||||
// ========================================================
 | 
				
			||||
// Namespace spawn process
 | 
				
			||||
// ========================================================
 | 
				
			||||
namespace nsproc { | 
				
			||||
    struct config { | 
				
			||||
        fs::path root_path, home_path, pwd; | 
				
			||||
        char *const *exec_argv; // must be nullptr-terminated
 | 
				
			||||
        uid_t uid, gid; | 
				
			||||
        bool mounts, gui_mounts, system_ro, keep_root, dummy_mode, pid_ns; | 
				
			||||
        std::optional<fs::path> setup_exec; | 
				
			||||
        int ns_path_fd; | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    int pid1() { | 
				
			||||
        sigset_t mask; | 
				
			||||
        sigemptyset(&mask); | 
				
			||||
        sigaddset(&mask, SIGCHLD); | 
				
			||||
 | 
				
			||||
        if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) | 
				
			||||
            return xerror("sigprocmask"); | 
				
			||||
        
 | 
				
			||||
        int sfd = signalfd(-1, &mask, 0); | 
				
			||||
        if (sfd == -1) | 
				
			||||
            return xerror("signalfd"); | 
				
			||||
        
 | 
				
			||||
        fd_set fds; | 
				
			||||
        struct timeval const tv = {.tv_sec = 60,.tv_usec = 0}; | 
				
			||||
 | 
				
			||||
        while (true) { | 
				
			||||
            FD_ZERO(&fds); | 
				
			||||
            FD_SET(sfd, &fds); | 
				
			||||
 | 
				
			||||
            struct timeval _tv = tv; | 
				
			||||
            int retval = select(sfd + 1, &fds, NULL, NULL, &_tv); | 
				
			||||
 | 
				
			||||
            if (retval < 0) | 
				
			||||
                return xerror("select"); | 
				
			||||
            else if (retval) { | 
				
			||||
                struct signalfd_siginfo si; | 
				
			||||
                int s = read(sfd, &si, sizeof(si)); | 
				
			||||
                if (s != sizeof(si)) | 
				
			||||
                    return xerror("signalfd_read"); | 
				
			||||
                if (si.ssi_signo != SIGCHLD) { | 
				
			||||
                    std::cerr << "Warn: Got signal != SIGCHLD" << std::endl; | 
				
			||||
                } | 
				
			||||
 | 
				
			||||
                // Reap children
 | 
				
			||||
                while (true) { | 
				
			||||
                    pid_t w = ::waitpid(-1, NULL, WNOHANG); | 
				
			||||
 | 
				
			||||
                    if (w == -1) { | 
				
			||||
                        if (errno == ECHILD) | 
				
			||||
                            break; | 
				
			||||
                    } | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
 | 
				
			||||
            // Check if there are still processes in namespace
 | 
				
			||||
            auto dir = ko::fs::dir_ptr("/proc"); | 
				
			||||
            int count = 0; | 
				
			||||
            for (auto ent : dir) { | 
				
			||||
                if (!isdigit(ent.d_name[0])) | 
				
			||||
                    continue; | 
				
			||||
                count++; | 
				
			||||
            } | 
				
			||||
            if (count <= 1) | 
				
			||||
                return 0; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int exec_app(const config &conf) { | 
				
			||||
        if (conf.pwd.empty()) { | 
				
			||||
            // Go to home is_directory
 | 
				
			||||
            auto home = ko::os::get_home(); | 
				
			||||
            fs::create_directories(home); | 
				
			||||
            chdir(home.c_str()); | 
				
			||||
        } else if (chdir(conf.pwd.c_str())) { | 
				
			||||
            die_errno(50, "Could not preserve working directory (Maybe -k is required?)"); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // Env.
 | 
				
			||||
        setenv("TMPDIR", "/tmp", 1); // Any subfolders may not exist
 | 
				
			||||
        setenv("PULSE_SERVER", ko::util::str("unix:/run/user/", conf.uid, "/pulse/native").c_str(), 0); | 
				
			||||
 | 
				
			||||
        // Run provided setup cmd
 | 
				
			||||
        if (conf.setup_exec) { | 
				
			||||
            const char* argv[2] = {conf.setup_exec.value().c_str(), nullptr}; | 
				
			||||
            auto proc = ko::proc::simple_spawn(argv); | 
				
			||||
            proc.wait(); | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // Drop Permissions
 | 
				
			||||
        setresgid(conf.gid, conf.gid, conf.gid); | 
				
			||||
        setresuid(conf.uid, conf.uid, conf.uid); | 
				
			||||
 | 
				
			||||
        // Exec
 | 
				
			||||
        execvpe(conf.exec_argv[0], conf.exec_argv, environ); | 
				
			||||
        return xerror("exec"); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int nsproc_create(const config &conf, proc_future<int> *report) { | 
				
			||||
        // Mount Namespace
 | 
				
			||||
        if (conf.mounts) { | 
				
			||||
            // Slightly hacky
 | 
				
			||||
            auto run_media_path = ko::util::str("/run/media/", getenv("USER")); | 
				
			||||
            auto [err, where] = ko::util::cvshort() | 
				
			||||
                // Mount base system: /, /proc, /sys, /dev, /tmp, /run
 | 
				
			||||
                .then(ko::ns::mount::mount_core, conf.root_path) | 
				
			||||
                // Mount /usr readonly because file permissions are useless in a single-uid namespace
 | 
				
			||||
                .ifthen(conf.system_ro && fs::exists(conf.root_path / "usr"), | 
				
			||||
                      ko::ns::mount::protect_path, conf.root_path / "usr") | 
				
			||||
                .ifthen(conf.system_ro && fs::exists(conf.root_path / "etc"), | 
				
			||||
                      ko::ns::mount::protect_path, conf.root_path / "etc") | 
				
			||||
                // Recursively bind in /media and /run/media/$USER for games
 | 
				
			||||
                .ifthen("bind_media", fs::exists("/media") && fs::exists(conf.root_path / "media"), | 
				
			||||
                      ko::os::bind, "/media", conf.root_path / "media", MS_REC) | 
				
			||||
                .ifthen("bind_run_media", fs::exists(run_media_path), [&conf, &run_media_path] () { | 
				
			||||
                      auto target_path =  conf.root_path / "run/media" / STEAM_USER; | 
				
			||||
                      std::error_code ec; | 
				
			||||
                      fs::create_directories(target_path, ec); | 
				
			||||
                      if (ec) | 
				
			||||
                          return 1; | 
				
			||||
                      return ko::os::bind(run_media_path, target_path, MS_REC); | 
				
			||||
                }) | 
				
			||||
                // Mount different things required by gui apps
 | 
				
			||||
                .ifthen(conf.gui_mounts, | 
				
			||||
                      ko::ns::mount::mount_gui, conf.root_path, conf.home_path.relative_path(), ko::util::str("run/user/", conf.uid)) | 
				
			||||
                // Add a dummy user to /etc/passwd
 | 
				
			||||
                .then("bind_passwd", [&conf]() { | 
				
			||||
                    auto etc_passwd = conf.root_path / "etc/passwd"; | 
				
			||||
                    auto tmp_passwd = conf.root_path / "tmp/passwd"; | 
				
			||||
 | 
				
			||||
                    if (fs::exists(etc_passwd)) { | 
				
			||||
                        fs::copy(etc_passwd, tmp_passwd); | 
				
			||||
                        auto s = std::fstream(tmp_passwd, std::fstream::out | std::fstream::app); | 
				
			||||
                        s << std::endl << STEAM_USER << ":x:" << conf.uid << ":" << conf.gid << ":Steam Container User:" << conf.home_path.native() << ":/bin/bash" << std::endl; | 
				
			||||
                        s.close(); | 
				
			||||
                        return ko::os::bind(tmp_passwd, etc_passwd); | 
				
			||||
                    } | 
				
			||||
                    return 0; | 
				
			||||
                }) | 
				
			||||
                // Finally, pivot_root
 | 
				
			||||
                .then(ko::ns::mount::pivot_root, conf.root_path, "mnt", conf.keep_root); | 
				
			||||
 | 
				
			||||
            if (err) { | 
				
			||||
                if (report) report->post(1); | 
				
			||||
                errno = err; | 
				
			||||
                return xerror(where); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        if (report) report->post(0); | 
				
			||||
        
 | 
				
			||||
        // Run Application
 | 
				
			||||
        if (!conf.dummy_mode) | 
				
			||||
            return exec_app(conf); | 
				
			||||
        else | 
				
			||||
            return pid1(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Joining Existing
 | 
				
			||||
    // Must associate pid namespace in parent process first!
 | 
				
			||||
    int nsproc_join_parent(const config &conf) { | 
				
			||||
        auto [err, where] = ko::util::cvshort() | 
				
			||||
            .then("setns_p_user", ko::ns::setns, "user", CLONE_NEWUSER, conf.ns_path_fd) | 
				
			||||
            .then("setns_p_pid", ko::ns::setns, "pid", CLONE_NEWPID, conf.ns_path_fd); | 
				
			||||
        if (err) | 
				
			||||
            return xerror(where); | 
				
			||||
        return 0; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    int nsproc_join_child(const config &conf) { | 
				
			||||
        auto [err, where] = ko::util::cvshort() | 
				
			||||
            //.then("setns_c_user", ko::ns::setns, "user", CLONE_NEWUSER, conf.ns_path_fd)
 | 
				
			||||
            .then("setns_c_mnt", ko::ns::setns, "mnt", CLONE_NEWNS, conf.ns_path_fd); | 
				
			||||
        if (err) | 
				
			||||
            return xerror(where); | 
				
			||||
 | 
				
			||||
        return exec_app(conf); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
// ========================================================
 | 
				
			||||
// Main
 | 
				
			||||
// ========================================================
 | 
				
			||||
void usage(const char *prog) { | 
				
			||||
    std::cout << "Usage:" << std::endl | 
				
			||||
              << "    " << prog << " -h" << std::endl | 
				
			||||
              << "    " << prog << " [-rMGk] [-p <path>] [-e <path>] [--] <argv...>" << std::endl | 
				
			||||
              << "    " << prog << " -c <path> [-MGk] [-p <path>] [-e <path>] [--] <argv...>" << std::endl | 
				
			||||
              << "    " << prog << " -j <path> [-e <path>] [--] <argv...>" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "General Options:" << std::endl | 
				
			||||
              << "  -h        Display this help text" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "Namespace Sharing Options:" << std::endl | 
				
			||||
              << "  -c <path> Create joinable namespace" << std::endl | 
				
			||||
              << "  -j <path> Join namespaces identified by path" << std::endl | 
				
			||||
              << "Note: Passing the single-character '-' will use '$root_path/.namespace'" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "Namespace Joining Options:" << std::endl | 
				
			||||
              << "  -p <path> The path to use for '-j-'" << std::endl | 
				
			||||
              << "  -D        Automatically spawn a instance of '" << prog << " -Dc'" << std::endl | 
				
			||||
              << "            into the background if the ns path doesn't exist." << std::endl | 
				
			||||
              << "Note: -D can be combined with most options from the NS Creation section below" << std::endl | 
				
			||||
              << "      but those options are ignored unless the ns must be created" << std::endl | 
				
			||||
              << std::endl | 
				
			||||
              << "Namespace Creation Options:" << std::endl | 
				
			||||
              << "  -r        Run in fakeroot mode (implies -W)" << std::endl | 
				
			||||
              << "  -p <path> Use custom root path" << std::endl | 
				
			||||
              << "  -M        Don't set up mouts (implies -G)" << std::endl | 
				
			||||
              << "  -G        Don't set up GUI-related mounts" << std::endl | 
				
			||||
              << "  -W        Don't make system paths read-only (/usr, /etc)" << std::endl | 
				
			||||
              << "  -k        Keep the original root filesystem at /mnt" << std::endl | 
				
			||||
              << "  -w        Preserve working directory (may require -k)" << std::endl | 
				
			||||
              << "  -e <path> Exceute a file during namespace setup" << std::endl | 
				
			||||
              << "  -D        Don't run any program, but idle to keep the namespace active." << std::endl | 
				
			||||
              << "            This also takes care of reaping Zombies if it is PID 1." << std::endl; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
struct config { | 
				
			||||
    fs::path root_path; | 
				
			||||
    const char *const *exec_argv = DEFAULT_CMD; | 
				
			||||
    bool fakeroot = false, | 
				
			||||
         mounts = true, | 
				
			||||
         gui_mounts = true, | 
				
			||||
         keep_root = false, | 
				
			||||
         keep_pwd = false, | 
				
			||||
         dummy_mode = false, | 
				
			||||
         pid_ns = true, | 
				
			||||
         ns_create = false, | 
				
			||||
         system_ro = true; | 
				
			||||
    std::optional<fs::path> ns_path, | 
				
			||||
                            ns_setup_exec; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
// Parse commandline arguments
 | 
				
			||||
// returns -1 on success, exit code otherwise
 | 
				
			||||
int parse_cmdline(config &conf, int argc, const char *const *argv) { | 
				
			||||
    constexpr auto spec = "+hp:rkwWMGe:c:j:D"; | 
				
			||||
    
 | 
				
			||||
    bool custom_root_path = false; | 
				
			||||
    std::optional<fs::path> create_path, join_path; | 
				
			||||
 | 
				
			||||
    while (true) { | 
				
			||||
        auto opt = getopt(argc, const_cast<char *const *>(argv), spec); | 
				
			||||
 | 
				
			||||
        if (opt == -1) | 
				
			||||
            break; | 
				
			||||
        else if (opt == '?' || opt == 'h') { | 
				
			||||
            usage(argv[0]); | 
				
			||||
            return opt == 'h' ? 0 : 1; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        else if (opt == 'r') { | 
				
			||||
            conf.fakeroot = true; | 
				
			||||
            conf.system_ro = false; | 
				
			||||
        } | 
				
			||||
        else if (opt == 'p') { | 
				
			||||
            conf.root_path = ::optarg; | 
				
			||||
            custom_root_path = true; | 
				
			||||
        } | 
				
			||||
        else if (opt == 'M') | 
				
			||||
            conf.mounts = false; | 
				
			||||
        else if (opt == 'G') | 
				
			||||
            conf.gui_mounts = false; | 
				
			||||
        else if (opt == 'W') | 
				
			||||
            conf.system_ro = false; | 
				
			||||
        else if (opt == 'k') | 
				
			||||
            conf.keep_root = true; | 
				
			||||
        else if (opt == 'w') | 
				
			||||
            conf.keep_pwd = true; | 
				
			||||
        else if (opt == 'e') | 
				
			||||
            conf.ns_setup_exec = ::optarg; | 
				
			||||
        else if (opt == 'c') | 
				
			||||
            create_path = ::optarg; | 
				
			||||
        else if (opt == 'j') | 
				
			||||
            join_path = ::optarg; | 
				
			||||
        else if (opt == 'D') | 
				
			||||
            conf.dummy_mode = true; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Check sanity
 | 
				
			||||
    bool good = true; | 
				
			||||
    if (join_path) { | 
				
			||||
        if (create_path) { | 
				
			||||
            std::cerr << "Error: -c and -j cannot be combined" << std::endl; | 
				
			||||
            good = false; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        // NOTE: let -p slip by to facilitate '-p<path> -j-' use-case
 | 
				
			||||
        if (!conf.dummy_mode && (!conf.mounts || !conf.gui_mounts || conf.keep_root)) { | 
				
			||||
            std::cerr << "Error: -j cannot be combined with any namespace setup options (-MGk) unless -D is given" << std::endl; | 
				
			||||
            good = false; | 
				
			||||
        } | 
				
			||||
 | 
				
			||||
        conf.ns_path = join_path; | 
				
			||||
    } | 
				
			||||
    if (create_path) { | 
				
			||||
        conf.ns_path = create_path; | 
				
			||||
        conf.ns_create = true; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (conf.ns_path) { | 
				
			||||
        // This is somewhat arbitrary but should prevent accidentally entering a fakeroot ns using -j
 | 
				
			||||
        if (conf.fakeroot) { | 
				
			||||
            std::cerr << "Error: -r cannot be combined with -c or -j" << std::endl; | 
				
			||||
            good = false; | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        // - Special default in -j and -c
 | 
				
			||||
        if (*conf.ns_path == "-") | 
				
			||||
            conf.ns_path = conf.root_path / ".namespace"; | 
				
			||||
    } else if (conf.dummy_mode) { | 
				
			||||
        std::cerr << "Error: -D must be combined with -c or -j" << std::endl; | 
				
			||||
        good = false; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (!good) { | 
				
			||||
        usage(argv[0]); | 
				
			||||
        return 5; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    // Rest is child cmnd
 | 
				
			||||
    if (argc > ::optind) | 
				
			||||
        conf.exec_argv = &argv[::optind]; | 
				
			||||
 | 
				
			||||
    return -1; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fs::path transpose_prefix(const fs::path &p, const fs::path &prefix, const fs::path &replace) { | 
				
			||||
    static const auto up = fs::path{".."}; | 
				
			||||
    auto rel = fs::relative(p, prefix); | 
				
			||||
    for (auto &c : rel) { | 
				
			||||
        if (c == up) | 
				
			||||
            return {}; | 
				
			||||
    } | 
				
			||||
    return replace / rel; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fs::path convert_path(const config &conf, const fs::path &p) { | 
				
			||||
    static const auto mounts = std::array<std::pair<fs::path,fs::path>, 2>{ | 
				
			||||
        std::pair{conf.root_path, "/"}, | 
				
			||||
        std::pair{"/media", "/media"} | 
				
			||||
    }; | 
				
			||||
    for (auto &pr : mounts) { | 
				
			||||
        auto res = transpose_prefix(p, pr.first, pr.second); | 
				
			||||
        if (!res.empty()) | 
				
			||||
            return res; | 
				
			||||
    } | 
				
			||||
    return fs::path{"/mnt"} / p; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
 | 
				
			||||
int main(int argc, char **argv) { | 
				
			||||
    auto home = ko::os::get_home(); | 
				
			||||
    auto uid = getuid(); | 
				
			||||
    auto gid = getgid(); | 
				
			||||
 | 
				
			||||
    // Set defaults
 | 
				
			||||
    auto conf = config{ | 
				
			||||
        .root_path = home / ROOT_DIR, | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    // Parse commandline
 | 
				
			||||
    auto perr = parse_cmdline(conf, argc, argv); | 
				
			||||
    if (perr != -1) | 
				
			||||
        return perr; | 
				
			||||
 | 
				
			||||
    // FIXME should lock something. Not sure what though. this can currently race
 | 
				
			||||
    if (conf.ns_path) { | 
				
			||||
        auto st = fs::symlink_status(*conf.ns_path); | 
				
			||||
        if (fs::exists(st)) { | 
				
			||||
            if (conf.ns_create) { | 
				
			||||
                std::cerr << "Error: File exists: " << *conf.ns_path << std::endl; | 
				
			||||
                return -EEXIST; | 
				
			||||
            } else { | 
				
			||||
                auto tgt = fs::status(*conf.ns_path); | 
				
			||||
                if (!fs::exists(tgt)) { | 
				
			||||
                    std::cerr << "Warning: Cleaning up stale ns link " << *conf.ns_path << " to " << fs::read_symlink(*conf.ns_path) << std::endl; | 
				
			||||
                    fs::remove(*conf.ns_path); | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } else if (!conf.ns_create && !conf.dummy_mode) { | 
				
			||||
            std::cerr << "Error: No such file: " << *conf.ns_path << std::endl; | 
				
			||||
            return -ENOENT; | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
    
 | 
				
			||||
    // Auto-create dummy instance
 | 
				
			||||
    auto parent_future = [&conf]() ->proc_future<int>* { | 
				
			||||
        if (!conf.ns_create && conf.dummy_mode && !fs::exists(*conf.ns_path)) { | 
				
			||||
            // Fork twice while communicating child pid
 | 
				
			||||
            auto f = proc_future<int>::create(); | 
				
			||||
            if (!f) | 
				
			||||
                die_errno(31, "Could not allocate future for dummy process"); | 
				
			||||
            auto vpid = ::vfork(); | 
				
			||||
            if (vpid < 0) | 
				
			||||
                die_errno(32, "Could not spawn dummy process (-Dc)"); | 
				
			||||
            else if (vpid == 0) { | 
				
			||||
                auto pid = ::fork(); | 
				
			||||
                if (pid < 0) | 
				
			||||
                    _exit(1); | 
				
			||||
                else if (pid > 0) | 
				
			||||
                    _exit(0); | 
				
			||||
                // Daemon process here
 | 
				
			||||
                // Switch to creation mode
 | 
				
			||||
                conf.ns_create = true; | 
				
			||||
                return f; | 
				
			||||
            } else { | 
				
			||||
                // Parent process here
 | 
				
			||||
                // check if second fork failed
 | 
				
			||||
                int st = 0; | 
				
			||||
                waitpid(vpid, &st, 0); | 
				
			||||
                if (WEXITSTATUS(st) != 0) | 
				
			||||
                    die(33, "Could not spawn dummy process (-Dc); double fork failed"); | 
				
			||||
                // Wait for ns creation
 | 
				
			||||
                auto pid = f->wait(); | 
				
			||||
                f->destroy(); | 
				
			||||
                if (pid < 0) | 
				
			||||
                    die(34, "Could not spawn dummy process (-Dc); reported failure"); | 
				
			||||
                conf.ns_path = ko::util::str("/proc/", pid, "/ns"); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        return nullptr; | 
				
			||||
    }(); | 
				
			||||
 | 
				
			||||
    ko::fd::fd ns_path_fd = conf.ns_path ? ko::fd::opendir(*conf.ns_path) : ko::fd::fd(-1); | 
				
			||||
 | 
				
			||||
    // Create nsproc config
 | 
				
			||||
    auto nsconf = nsproc::config{ | 
				
			||||
        .root_path = conf.root_path, | 
				
			||||
        .home_path = home, | 
				
			||||
        .pwd = conf.keep_pwd ? convert_path(conf, fs::current_path()) : fs::path{}, | 
				
			||||
        .exec_argv = const_cast<char *const *>(conf.exec_argv), | 
				
			||||
        .uid = conf.fakeroot ? 0 : uid, | 
				
			||||
        .gid = conf.fakeroot ? 0 : gid, | 
				
			||||
        .mounts = conf.mounts, | 
				
			||||
        .gui_mounts = conf.gui_mounts, | 
				
			||||
        .system_ro = conf.system_ro, | 
				
			||||
        .keep_root = conf.keep_root, | 
				
			||||
        .dummy_mode = conf.dummy_mode, | 
				
			||||
        .pid_ns = conf.pid_ns, | 
				
			||||
        .setup_exec = conf.ns_setup_exec, | 
				
			||||
        .ns_path_fd = ns_path_fd | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    constexpr auto stacksize = 1024*1024; | 
				
			||||
 | 
				
			||||
    // clone
 | 
				
			||||
    auto ns_future = conf.ns_create ? proc_future<int>::create_private() : nullptr; | 
				
			||||
    auto [proc, res] = conf.ns_path && !conf.ns_create ? | 
				
			||||
        [&nsconf]() -> std::pair<ko::proc::child_ref, int> { | 
				
			||||
            int e = nsproc::nsproc_join_parent(nsconf); | 
				
			||||
            if (e) return {-1, e}; | 
				
			||||
            auto child = ko::proc::vclone(nsproc::nsproc_join_child, stacksize, 0, nsconf); | 
				
			||||
            return {std::move(child), 0}; | 
				
			||||
        }() : | 
				
			||||
        ko::ns::clone::uvclone_single(nsconf.uid, nsconf.gid, nsproc::nsproc_create, stacksize, CLONE_NEWNS|CLONE_NEWPID, nsconf, ns_future); | 
				
			||||
 | 
				
			||||
    if (proc) { | 
				
			||||
        // Child should handle signals and then return
 | 
				
			||||
        static int _pid = proc.pid(); | 
				
			||||
        signal(SIGINT, SIG_IGN); // assume sent to whole session
 | 
				
			||||
        signal(SIGTERM, [](int sig){ | 
				
			||||
            kill(_pid, sig); | 
				
			||||
        }); | 
				
			||||
 | 
				
			||||
        // Create ns_reference
 | 
				
			||||
        if (conf.ns_create) { | 
				
			||||
            if (ns_future) { | 
				
			||||
                // TODO consider the return value?
 | 
				
			||||
                ns_future->wait(); | 
				
			||||
                ns_future->destroy(); | 
				
			||||
            } | 
				
			||||
            // TODO move this out so it's independent of ns_create. But create_symlink can throw which would
 | 
				
			||||
            // lead to a locked-up parent.
 | 
				
			||||
            if (parent_future) { | 
				
			||||
                parent_future->post(_pid); | 
				
			||||
                parent_future->unmap(); | 
				
			||||
            } | 
				
			||||
            fs::create_directory_symlink(ko::util::str("/proc/", proc.pid(), "/ns"), *conf.ns_path); | 
				
			||||
        } | 
				
			||||
        
 | 
				
			||||
        // Wait for child
 | 
				
			||||
        res = proc.wait(); | 
				
			||||
 | 
				
			||||
        // Clean up ns path
 | 
				
			||||
        if (conf.ns_create) | 
				
			||||
            fs::remove(*conf.ns_path); | 
				
			||||
 | 
				
			||||
        return res; | 
				
			||||
    } else { | 
				
			||||
        if (parent_future) | 
				
			||||
            parent_future->post(-1); | 
				
			||||
        return proc.pid(); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,11 @@ | 
				
			||||
{ | 
				
			||||
	"folders": [ | 
				
			||||
		{ | 
				
			||||
			"path": "." | 
				
			||||
		} | 
				
			||||
	], | 
				
			||||
	"settings": { | 
				
			||||
		"C_Cpp.default.cppStandard": "c++20", | 
				
			||||
		"C_Cpp.default.cStandard": "c17" | 
				
			||||
	} | 
				
			||||
} | 
				
			||||
					Loading…
					
					
				
		Reference in new issue