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