├── .gitignore ├── .ycm_extra_conf.py ├── LICENSE ├── Makefile ├── README.md ├── example ├── enter ├── sandbox │ ├── .hako │ │ └── init │ ├── bin │ │ ├── busybox │ │ ├── nc │ │ └── sh │ ├── etc │ │ ├── group │ │ └── passwd │ ├── proc │ │ └── .keepme │ └── sbin │ │ └── init └── start └── src ├── hako-common.h ├── hako-enter.c ├── hako-run.c ├── optparse-help.h └── optparse.h /.gitignore: -------------------------------------------------------------------------------- 1 | /hako-run 2 | /hako-enter 3 | -------------------------------------------------------------------------------- /.ycm_extra_conf.py: -------------------------------------------------------------------------------- 1 | # This file is NOT licensed under the GPLv3, which is the license for the rest 2 | # of YouCompleteMe. 3 | # 4 | # Here's the license text for this file: 5 | # 6 | # This is free and unencumbered software released into the public domain. 7 | # 8 | # Anyone is free to copy, modify, publish, use, compile, sell, or 9 | # distribute this software, either in source code form or as a compiled 10 | # binary, for any purpose, commercial or non-commercial, and by any 11 | # means. 12 | # 13 | # In jurisdictions that recognize copyright laws, the author or authors 14 | # of this software dedicate any and all copyright interest in the 15 | # software to the public domain. We make this dedication for the benefit 16 | # of the public at large and to the detriment of our heirs and 17 | # successors. We intend this dedication to be an overt act of 18 | # relinquishment in perpetuity of all present and future rights to this 19 | # software under copyright law. 20 | # 21 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 24 | # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 25 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 26 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | # OTHER DEALINGS IN THE SOFTWARE. 28 | # 29 | # For more information, please refer to 30 | 31 | import os 32 | import ycm_core 33 | 34 | # These are the compilation flags that will be used in case there's no 35 | # compilation database set (by default, one is not set). 36 | # CHANGE THIS LIST OF FLAGS. YES, THIS IS THE DROID YOU HAVE BEEN LOOKING FOR. 37 | flags = [ 38 | '-Wall', 39 | '-Wextra', 40 | '-Werror', 41 | '-pedantic', 42 | '-pthread', 43 | '-Wno-missing-field-initializers', # {0} is nice 44 | '-Wc++98-compat', 45 | '-fexceptions', 46 | '-DNDEBUG', 47 | '-DUGC_IMPLEMENTATION', 48 | '-I', 'deps', 49 | # THIS IS IMPORTANT! Without a "-std=" flag, clang won't know which 50 | # language to use when compiling headers. So it will guess. Badly. So C++ 51 | # headers will be compiled as C headers. You don't want that so ALWAYS specify 52 | # a "-std=". 53 | # For a C project, you would set this to something like 'c99' instead of 54 | # 'c++11'. 55 | '-std=c99', 56 | # ...and the same thing goes for the magic -x option which specifies the 57 | # language that the files to be compiled are written in. This is mostly 58 | # relevant for c++ headers. 59 | # For a C project, you would set this to 'c' instead of 'c++'. 60 | '-x', 'c', 61 | ] 62 | 63 | 64 | # Set this to the absolute path to the folder (NOT the file!) containing the 65 | # compile_commands.json file to use that instead of 'flags'. See here for 66 | # more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html 67 | # 68 | # You can get CMake to generate this file for you by adding: 69 | # set( CMAKE_EXPORT_COMPILE_COMMANDS 1 ) 70 | # to your CMakeLists.txt file. 71 | # 72 | # Most projects will NOT need to set this to anything; you can just change the 73 | # 'flags' list of compilation flags. Notice that YCM itself uses that approach. 74 | compilation_database_folder = '.build' 75 | 76 | if os.path.exists( compilation_database_folder ): 77 | database = ycm_core.CompilationDatabase( compilation_database_folder ) 78 | else: 79 | database = None 80 | 81 | SOURCE_EXTENSIONS = [ '.cpp', '.cxx', '.cc', '.c', '.m', '.mm' ] 82 | 83 | def DirectoryOfThisScript(): 84 | return os.path.dirname( os.path.abspath( __file__ ) ) 85 | 86 | 87 | def MakeRelativePathsInFlagsAbsolute( flags, working_directory ): 88 | if not working_directory: 89 | return list( flags ) 90 | new_flags = [] 91 | make_next_absolute = False 92 | path_flags = [ '-isystem', '-I', '-iquote', '--sysroot=' ] 93 | for flag in flags: 94 | new_flag = flag 95 | 96 | if make_next_absolute: 97 | make_next_absolute = False 98 | if not flag.startswith( '/' ): 99 | new_flag = os.path.join( working_directory, flag ) 100 | 101 | for path_flag in path_flags: 102 | if flag == path_flag: 103 | make_next_absolute = True 104 | break 105 | 106 | if flag.startswith( path_flag ): 107 | path = flag[ len( path_flag ): ] 108 | new_flag = path_flag + os.path.join( working_directory, path ) 109 | break 110 | 111 | if new_flag: 112 | new_flags.append( new_flag ) 113 | return new_flags 114 | 115 | 116 | def IsHeaderFile( filename ): 117 | extension = os.path.splitext( filename )[ 1 ] 118 | return extension in [ '.h', '.hxx', '.hpp', '.hh' ] 119 | 120 | 121 | def GetCompilationInfoForFile( filename ): 122 | # The compilation_commands.json file generated by CMake does not have entries 123 | # for header files. So we do our best by asking the db for flags for a 124 | # corresponding source file, if any. If one exists, the flags for that file 125 | # should be good enough. 126 | if IsHeaderFile( filename ): 127 | basename = os.path.splitext( filename )[ 0 ] 128 | for extension in SOURCE_EXTENSIONS: 129 | replacement_file = basename + extension 130 | if os.path.exists( replacement_file ): 131 | compilation_info = database.GetCompilationInfoForFile( 132 | replacement_file ) 133 | if compilation_info.compiler_flags_: 134 | return compilation_info 135 | return None 136 | return database.GetCompilationInfoForFile( filename ) 137 | 138 | 139 | def FlagsForFile( filename, **kwargs ): 140 | if database: 141 | # Bear in mind that compilation_info.compiler_flags_ does NOT return a 142 | # python list, but a "list-like" StringVec object 143 | compilation_info = GetCompilationInfoForFile( filename ) 144 | if not compilation_info: 145 | return None 146 | 147 | final_flags = MakeRelativePathsInFlagsAbsolute( 148 | compilation_info.compiler_flags_, 149 | compilation_info.compiler_working_dir_ ) 150 | 151 | else: 152 | relative_to = DirectoryOfThisScript() 153 | final_flags = MakeRelativePathsInFlagsAbsolute( flags, relative_to ) 154 | 155 | return { 156 | 'flags': final_flags, 157 | 'do_cache': True 158 | } 159 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Bach Le 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS += -Wall -Wextra -pedantic -Wno-missing-field-initializers -Werror -std=c99 -O3 -g 2 | 3 | all: hako-run hako-enter 4 | 5 | clean: 6 | rm hako-* 7 | 8 | hako-%: src/hako-%.c 9 | $(CC) $(CFLAGS) -o $@ $< 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hako - A minimal sandboxing tool 2 | 3 | [![License](https://img.shields.io/badge/license-BSD-blue.svg)](LICENSE) 4 | 5 | `hako` = chroot + Linux namespace. 6 | 7 | It is created out of a need for a simple tool like `chroot` but with extra isolation. 8 | 9 | ## What it does 10 | 11 | - It generally works like `chroot` with the added benefit of isolation using Linux namespace. 12 | - It can run on a read-only filesystem. 13 | - Some rudimentary form of privilege dropping through setuid, setgid and [`PR_SET_NO_NEW_PRIVS`](https://www.kernel.org/doc/Documentation/prctl/no_new_privs.txt). 14 | 15 | ## What it does not do 16 | 17 | - Networking: use docker/runc instead or setup something with iproute2 and veth. 18 | With the `--network` switch, a sandbox can use the host's or another sandbox's network. 19 | Alternatively, Unix socket works for sandboxes in the same host too. 20 | - Seccomp: I might start a new project for this if needed. 21 | Something like `seccomp-exec [args]` would be nice. 22 | 23 | ## Build requirements 24 | 25 | - A C99 compiler (gcc/clang) 26 | - Recent Linux headers 27 | - make 28 | 29 | ## Usage 30 | 31 | ### Creating a sandbox 32 | 33 | ```sh 34 | mkdir sandbox 35 | mkdir sandbox/.hako 36 | touch sandbox/.hako/init 37 | chmod +x sandbox/.hako/init 38 | mkdir sandbox/bin 39 | touch sandbox/bin/busybox 40 | ln -s busybox sandbox/bin/sh 41 | ``` 42 | 43 | Content of `.hako/init`: 44 | 45 | ```sh 46 | #!/bin/sh -e 47 | 48 | mount -o ro,bind $(which busybox) ./bin/busybox 49 | ``` 50 | 51 | Run it with: 52 | 53 | ```sh 54 | hako-run sandbox /bin/sh 55 | ``` 56 | 57 | General syntax is: `hako-run [options] [command] [args]`. 58 | 59 | If `command` is not given, it will default to `/bin/sh`. 60 | 61 | The file `.hako/init` must be present and will be executed to initialize the sandbox. 62 | It can do things like bind mounting files from the host into the sandbox. 63 | 64 | Run `hako-run --help` for more info. 65 | 66 | ### Entering an existing sandbox 67 | 68 | Given: 69 | 70 | ```sh 71 | hako-run --pid-file sandbox.pid sandbox 72 | ``` 73 | 74 | One can enter the sandbox with: 75 | 76 | ```sh 77 | hako-enter --fork $(cat sandbox.pid) /bin/sh 78 | ``` 79 | 80 | General syntax is: `hako-enter [options] [command] [args]`. 81 | 82 | If `command` is not given, it will default to `/bin/sh`. 83 | 84 | Run `hako-enter --help` for more info. 85 | 86 | ## FAQ 87 | 88 | ### Why not docker? 89 | 90 | Docker does too many things. 91 | It also requires a daemon running. 92 | While it's possible to use it without building image, it's just annoying in general. 93 | 94 | ### Why not runc (aka: Docker, the good part)? 95 | 96 | `runc` looks good but I only need something a little more than `chroot` that runs only on Linux. 97 | I rather like the idea of simple Unix tools and [Bernstein chaining](http://www.catb.org/~esr/writings/taoup/html/ch06s06.html). 98 | If I need features like seccomp, I'd probably write a separate chain wrapper for it. 99 | 100 | ### Why not systemd-nspawn? 101 | 102 | 1. It requires glibc, according to buildroot. `hako` can be built with musl. 103 | 2. While I'm sure it can be used standalone, it comes with a bunch of dependencies from the systemd project. 104 | 3. It's systemd (jk). 105 | 106 | ### Why must the sandbox contains an empty .hako directory? 107 | 108 | [`pivot_root`](https://linux.die.net/man/8/pivot_root) requires it. 109 | It also provides access to the old root filesystem while creating the sandbox. 110 | `runc` relies on an [undocumented trick](https://github.com/opencontainers/runc/blob/593914b8bd5448a93f7c3e4902a03408b6d5c0ce/libcontainer/rootfs_linux.go#L635) but I'd rather not. 111 | 112 | ### How to build with musl? 113 | 114 | `CC='musl-gcc -static' make` 115 | 116 | ### How to use tmpfs in the container? 117 | 118 | Put this in `.hako/init`: `mount -t tmpfs tmpfs ./tmpfs`. 119 | 120 | ### How to hide .hako content? 121 | 122 | Put this in `.hako/init`: `mount -t tmpfs -o ro tmpfs .hako`. 123 | 124 | ### How to pass arguments to .hako/init? 125 | 126 | Use environment variable (e.g: `SOME_INIT_ARGS="some-args" hako-enter sandbox`). 127 | 128 | ### How to allow a sandbox to use the host's network namespace? 129 | 130 | `hako-run --network sandbox` 131 | 132 | ### How to put a sandbox in another one's network namespace? 133 | 134 | `hako-run --network=/proc/$(cat other-sandbox.pid)/net/ns sandbox` 135 | -------------------------------------------------------------------------------- /example/enter: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DIRNAME="$(readlink -f $(dirname $0))" 4 | SANDBOXDIR="${DIRNAME}/sandbox" 5 | HAKO_ENTER="$(dirname ${DIRNAME})/hako-enter" 6 | 7 | exec $HAKO_ENTER \ 8 | --user ${SUDO_UID:-$(id -u)} \ 9 | --group ${SUDO_GID:-$(id -g)} \ 10 | --fork \ 11 | $(cat /tmp/hako.pid) \ 12 | "$@" 13 | -------------------------------------------------------------------------------- /example/sandbox/.hako/init: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | mount -o ro,bind $(which busybox) ./bin/busybox 4 | mount -o ro,bind /etc/passwd ./etc/passwd 5 | mount -o ro,bind /etc/group ./etc/group 6 | mount -o ro,bind /tmp ./tmp 7 | mount -t proc proc ./proc 8 | mount -o ro -t tmpfs tmpfs .hako # Hide directory's content 9 | 10 | # Enable loopback networking 11 | ip link set lo up 12 | -------------------------------------------------------------------------------- /example/sandbox/bin/busybox: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullno1/hako/07f4d81b265d85232eecacd4675620bf4af65d4f/example/sandbox/bin/busybox -------------------------------------------------------------------------------- /example/sandbox/bin/nc: -------------------------------------------------------------------------------- 1 | busybox -------------------------------------------------------------------------------- /example/sandbox/bin/sh: -------------------------------------------------------------------------------- 1 | busybox -------------------------------------------------------------------------------- /example/sandbox/etc/group: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullno1/hako/07f4d81b265d85232eecacd4675620bf4af65d4f/example/sandbox/etc/group -------------------------------------------------------------------------------- /example/sandbox/etc/passwd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullno1/hako/07f4d81b265d85232eecacd4675620bf4af65d4f/example/sandbox/etc/passwd -------------------------------------------------------------------------------- /example/sandbox/proc/.keepme: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bullno1/hako/07f4d81b265d85232eecacd4675620bf4af65d4f/example/sandbox/proc/.keepme -------------------------------------------------------------------------------- /example/sandbox/sbin/init: -------------------------------------------------------------------------------- 1 | ../bin/busybox -------------------------------------------------------------------------------- /example/start: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | DIRNAME="$(readlink -f $(dirname $0))" 4 | SANDBOXDIR="${DIRNAME}/sandbox" 5 | HAKO_RUN="$(dirname ${DIRNAME})/hako-run" 6 | 7 | # Empty directories can't be checked into git 8 | mkdir -p ${SANDBOXDIR}/tmp ${SANDBOXDIR}/.hako 9 | 10 | exec $HAKO_RUN \ 11 | --pid-file /tmp/hako.pid \ 12 | --user ${SUDO_UID:-$(id -u)} \ 13 | --group ${SUDO_GID:-$(id -g)} \ 14 | ${SANDBOXDIR} \ 15 | "$@" 16 | -------------------------------------------------------------------------------- /src/hako-common.h: -------------------------------------------------------------------------------- 1 | #ifndef HAKO_COMMON_H 2 | #define HAKO_COMMON_H 3 | 4 | #ifndef _GNU_SOURCE 5 | #define _GNU_SOURCE 6 | #endif 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include "optparse.h" 18 | 19 | #define CASE_RUN_OPT case 'e': case 'u': case 'g': case 'c' 20 | #define RUN_CTX_OPTS \ 21 | {"env", 'e', OPTPARSE_REQUIRED}, \ 22 | {"user", 'u', OPTPARSE_REQUIRED}, \ 23 | {"group", 'g', OPTPARSE_REQUIRED}, \ 24 | {"chdir", 'c', OPTPARSE_REQUIRED} 25 | 26 | #define RUN_CTX_HELP \ 27 | "NAME=VALUE", "Set environment variable inside sandbox", \ 28 | "USER", "Run as this user", \ 29 | "GROUP", "Run as this group", \ 30 | "DIR", "Change to this directory inside sandbox" 31 | 32 | struct run_ctx_s 33 | { 34 | uid_t uid; 35 | gid_t gid; 36 | const char* work_dir; 37 | unsigned int env_len; 38 | char** env; 39 | char** command; 40 | char* default_cmd[2]; 41 | }; 42 | 43 | static bool 44 | strtonum(const char* str, long* number) 45 | { 46 | char* end; 47 | *number = strtol(str, &end, 10); 48 | return *end == '\0'; 49 | } 50 | 51 | static void 52 | init_run_ctx(struct run_ctx_s* run_ctx, int argc) 53 | { 54 | *run_ctx = (struct run_ctx_s){ 55 | .uid = (uid_t)-1, 56 | .gid = (gid_t)-1, 57 | .env = calloc(argc / 2, sizeof(char*)) 58 | }; 59 | } 60 | 61 | static void 62 | cleanup_run_ctx(struct run_ctx_s* run_ctx) 63 | { 64 | free(run_ctx->env); 65 | } 66 | 67 | static bool 68 | parse_run_option( 69 | struct run_ctx_s* run_ctx, 70 | const char* prog_name, 71 | char option, 72 | char* optarg 73 | ) 74 | { 75 | long num; 76 | switch(option) 77 | { 78 | case 'u': 79 | if(strtonum(optarg, &num) && num >= 0) 80 | { 81 | run_ctx->uid = (uid_t)num; 82 | } 83 | else 84 | { 85 | struct passwd* pwd = getpwnam(optarg); 86 | if(pwd != NULL) 87 | { 88 | run_ctx->uid = pwd->pw_uid; 89 | } 90 | else 91 | { 92 | fprintf(stderr, "%s: invalid user: %s\n", prog_name, optarg); 93 | return false; 94 | } 95 | } 96 | return true; 97 | case 'g': 98 | if(strtonum(optarg, &num) && num >= 0) 99 | { 100 | run_ctx->gid = (gid_t)num; 101 | } 102 | else 103 | { 104 | struct group* grp = getgrnam(optarg); 105 | if(grp != NULL) 106 | { 107 | run_ctx->gid = grp->gr_gid; 108 | } 109 | else 110 | { 111 | fprintf(stderr, "%s: invalid group: %s\n", prog_name, optarg); 112 | return false; 113 | } 114 | } 115 | return true; 116 | case 'e': 117 | run_ctx->env[run_ctx->env_len++] = optarg; 118 | return true; 119 | case 'c': 120 | run_ctx->work_dir = optarg; 121 | return true; 122 | default: 123 | fprintf(stderr, "%s: invalid option: %c\n", prog_name, option); 124 | return false; 125 | } 126 | } 127 | 128 | static bool 129 | drop_privileges(const struct run_ctx_s* run_ctx) 130 | { 131 | uid_t uid = run_ctx->uid; 132 | uid_t gid = run_ctx->gid; 133 | 134 | if((uid != (uid_t)-1 || gid != (gid_t)-1) && setgroups(0, NULL) == -1) 135 | { 136 | perror("setgroups(0, NULL) failed"); 137 | return false; 138 | } 139 | 140 | if(gid != (gid_t)-1 && setgid(gid) == -1) 141 | { 142 | perror("setgid() failed"); 143 | return false; 144 | } 145 | 146 | if(uid != (uid_t)-1 && setuid(uid) == -1) 147 | { 148 | perror("setuid() failed"); 149 | return false; 150 | } 151 | 152 | return true; 153 | } 154 | 155 | static const char* 156 | parse_run_command(struct run_ctx_s* run_ctx, struct optparse* options) 157 | { 158 | const char* target = options->argv[options->optind]; 159 | if(target != NULL && options->argv[options->optind + 1] != NULL) 160 | { 161 | run_ctx->command = &options->argv[options->optind + 1]; 162 | } 163 | else 164 | { 165 | run_ctx->default_cmd[0] = "/bin/sh"; 166 | run_ctx->default_cmd[1] = NULL; 167 | run_ctx->command = run_ctx->default_cmd; 168 | } 169 | 170 | return target; 171 | } 172 | 173 | static bool 174 | execute_run_ctx(const struct run_ctx_s* run_ctx) 175 | { 176 | if(!drop_privileges(run_ctx)) { return false; } 177 | 178 | if(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == -1) 179 | { 180 | perror("Could not lock privileges"); 181 | return false; 182 | } 183 | 184 | if(run_ctx->work_dir != NULL && chdir(run_ctx->work_dir) == -1) 185 | { 186 | perror("chdir() failed"); 187 | return false; 188 | } 189 | 190 | if(execve(run_ctx->command[0], run_ctx->command, run_ctx->env) == -1) 191 | { 192 | fprintf( 193 | stderr, "execve(\"%s\") failed: %s\n", 194 | run_ctx->command[0], strerror(errno) 195 | ); 196 | return false; 197 | } 198 | 199 | return true; 200 | } 201 | 202 | #endif 203 | -------------------------------------------------------------------------------- /src/hako-enter.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #define OPTPARSE_IMPLEMENTATION 15 | #define OPTPARSE_API static __attribute__((unused)) 16 | #include "optparse.h" 17 | #define OPTPARSE_HELP_IMPLEMENTATION 18 | #define OPTPARSE_HELP_API static 19 | #include "optparse-help.h" 20 | #include "hako-common.h" 21 | 22 | #define PROG_NAME "hako-enter" 23 | #define quit(code) exit_code = code; goto quit; 24 | 25 | static bool 26 | enter_sandbox(const char* pid) 27 | { 28 | bool exit_code = true; 29 | DIR* dir = NULL; 30 | char ns_dir[256]; // long enough to hold path to namespace 31 | 32 | if(snprintf(ns_dir, sizeof(ns_dir), "/proc/%s/ns", pid) > (int)sizeof(ns_dir)) 33 | { 34 | fprintf(stderr, "Invalid pid\n"); 35 | quit(false); 36 | } 37 | 38 | if(chdir(ns_dir) == -1) 39 | { 40 | fprintf(stderr, "chdir(\"%s\") failed: %s\n", ns_dir, strerror(errno)); 41 | quit(false); 42 | } 43 | 44 | dir = opendir("."); 45 | if(dir == NULL) 46 | { 47 | perror("Could not examine sandbox"); 48 | quit(false); 49 | } 50 | 51 | struct dirent* dirent; 52 | while((dirent = readdir(dir)) != NULL) 53 | { 54 | if(dirent->d_type != DT_LNK) { continue; } 55 | 56 | int ns = open(dirent->d_name, O_RDONLY); 57 | if(ns < 0) 58 | { 59 | if(errno == ENOENT) // no such namespace 60 | { 61 | continue; 62 | } 63 | else 64 | { 65 | fprintf( 66 | stderr, "Could not open %s: %s\n", 67 | dirent->d_name, strerror(errno) 68 | ); 69 | quit(false); 70 | } 71 | } 72 | else 73 | { 74 | int setns_result = setns(ns, 0); 75 | int setns_error = errno; 76 | close(ns); 77 | if(setns_result == -1) 78 | { 79 | fprintf( 80 | stderr, "Could not setns %s: %s\n", 81 | dirent->d_name, strerror(setns_error) 82 | ); 83 | 84 | if(!(strcmp(dirent->d_name, "user") == 0 85 | || strcmp(dirent->d_name, "net") == 0)) 86 | { 87 | quit(false); 88 | } 89 | } 90 | } 91 | } 92 | 93 | quit: 94 | if(dir != NULL) { closedir(dir); } 95 | 96 | return exit_code; 97 | } 98 | 99 | int 100 | main(int argc, char* argv[]) 101 | { 102 | int exit_code = EXIT_SUCCESS; 103 | 104 | struct optparse_long opts[] = { 105 | {"help", 'h', OPTPARSE_NONE}, 106 | {"fork", 'f', OPTPARSE_NONE}, 107 | RUN_CTX_OPTS, 108 | {0} 109 | }; 110 | 111 | const char* help[] = { 112 | NULL, "Print this message", 113 | NULL, "Fork a new process inside sandbox", 114 | RUN_CTX_HELP, 115 | }; 116 | 117 | const char* usage = "Usage: " PROG_NAME " [options] [command] [args]"; 118 | 119 | int option; 120 | bool fork_before_exec = false; 121 | struct optparse options; 122 | struct run_ctx_s run_ctx; 123 | 124 | init_run_ctx(&run_ctx, argc); 125 | optparse_init(&options, argv); 126 | options.permute = 0; 127 | 128 | while((option = optparse_long(&options, opts, NULL)) != -1) 129 | { 130 | switch(option) 131 | { 132 | case 'h': 133 | optparse_help(usage, opts, help); 134 | quit(EXIT_SUCCESS); 135 | break; 136 | case 'f': 137 | fork_before_exec = true; 138 | break; 139 | CASE_RUN_OPT: 140 | if(!parse_run_option(&run_ctx, PROG_NAME, option, options.optarg)) 141 | { 142 | quit(EXIT_FAILURE); 143 | } 144 | break; 145 | case '?': 146 | fprintf(stderr, PROG_NAME ": %s\n", options.errmsg); 147 | quit(EXIT_FAILURE); 148 | break; 149 | default: 150 | fprintf(stderr, "Unimplemented option\n"); 151 | quit(EXIT_FAILURE); 152 | break; 153 | } 154 | } 155 | 156 | const char* pid = parse_run_command(&run_ctx, &options); 157 | 158 | if(pid == NULL) 159 | { 160 | fprintf(stderr, PROG_NAME ": must provide sandbox PID\n"); 161 | quit(EXIT_FAILURE); 162 | } 163 | 164 | if(!enter_sandbox(pid)) { quit(EXIT_FAILURE); } 165 | 166 | if(fork_before_exec) 167 | { 168 | pid_t child = vfork(); 169 | if(child == -1) 170 | { 171 | perror("vfork() failed"); 172 | quit(EXIT_FAILURE); 173 | } 174 | else if(child == 0) // child 175 | { 176 | if(prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) 177 | { 178 | perror("Could not set parent death signal"); 179 | exit(EXIT_FAILURE); 180 | } 181 | 182 | if(!execute_run_ctx(&run_ctx)) { exit(EXIT_FAILURE); } 183 | 184 | exit(EXIT_SUCCESS);// unreachable 185 | } 186 | else // parent 187 | { 188 | if(!drop_privileges(&run_ctx)) { quit(EXIT_FAILURE); } 189 | 190 | int status; 191 | errno = 0; 192 | while(waitpid(child, &status, 0) != child && errno == EINTR) {} 193 | 194 | quit( 195 | WIFEXITED(status) ? 196 | WEXITSTATUS(status) : (128 + WTERMSIG(status)) 197 | ); 198 | } 199 | } 200 | else 201 | { 202 | if(!execute_run_ctx(&run_ctx)) { quit(EXIT_FAILURE); } 203 | } 204 | quit: 205 | cleanup_run_ctx(&run_ctx); 206 | 207 | return exit_code; 208 | } 209 | -------------------------------------------------------------------------------- /src/hako-run.c: -------------------------------------------------------------------------------- 1 | #define _GNU_SOURCE 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #define OPTPARSE_IMPLEMENTATION 14 | #define OPTPARSE_API static __attribute__((unused)) 15 | #include "optparse.h" 16 | #define OPTPARSE_HELP_IMPLEMENTATION 17 | #define OPTPARSE_HELP_API static 18 | #include "optparse-help.h" 19 | #include "hako-common.h" 20 | 21 | #define HAKO_DIR ".hako" 22 | #define PROG_NAME "hako-run" 23 | #define quit(code) exit_code = code; goto quit; 24 | 25 | struct sandbox_cfg_s 26 | { 27 | const char* sandbox_dir; 28 | const char* netns; 29 | int netns_flag; 30 | struct bindmnt_s* mounts; 31 | bool writable; 32 | struct run_ctx_s run_ctx; 33 | }; 34 | 35 | static int 36 | sandbox_entry(void* arg) 37 | { 38 | int exit_code = EXIT_SUCCESS; 39 | 40 | const struct sandbox_cfg_s* sandbox_cfg = arg; 41 | 42 | // Die with parent 43 | if(prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) 44 | { 45 | perror("Could not set parent death signal"); 46 | quit(EXIT_FAILURE); 47 | } 48 | 49 | // Prepare sandbox dir 50 | 51 | if(mount(NULL, "/", NULL, MS_PRIVATE | MS_REC, NULL) == -1) 52 | { 53 | perror("Could not make root mount private"); 54 | quit(EXIT_FAILURE); 55 | } 56 | 57 | if(mount( 58 | sandbox_cfg->sandbox_dir, sandbox_cfg->sandbox_dir, 59 | NULL, MS_BIND | MS_REC, NULL 60 | ) == -1) 61 | { 62 | perror("Could not turn sandbox into a mountpoint"); 63 | quit(EXIT_FAILURE); 64 | } 65 | 66 | if(chdir(sandbox_cfg->sandbox_dir) == -1) 67 | { 68 | perror("Could not chdir into sandbox"); 69 | quit(EXIT_FAILURE); 70 | } 71 | 72 | // Network 73 | if(sandbox_cfg->netns_flag != CLONE_NEWNET && sandbox_cfg->netns != NULL) 74 | { 75 | int netns = open(sandbox_cfg->netns, O_RDONLY); 76 | if(netns < 0) 77 | { 78 | fprintf( 79 | stderr, "Could not access %s: %s\n", 80 | sandbox_cfg->netns, strerror(errno) 81 | ); 82 | quit(EXIT_FAILURE); 83 | } 84 | 85 | int setns_result = setns(netns, CLONE_NEWNET); 86 | int setns_error = errno; 87 | close(netns); 88 | if(setns_result == -1) 89 | { 90 | fprintf(stderr, "Could not setns: %s\n", strerror(setns_error)); 91 | quit(EXIT_FAILURE); 92 | } 93 | } 94 | 95 | // Execute .hako/init 96 | pid_t init_pid = vfork(); 97 | if(init_pid < 0) 98 | { 99 | perror("vfork() failed"); 100 | quit(EXIT_FAILURE); 101 | } 102 | else if(init_pid == 0) // child 103 | { 104 | if(prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0) == -1) 105 | { 106 | perror("Could not set parent death signal"); 107 | quit(EXIT_FAILURE); 108 | } 109 | 110 | char* init_cmd[] = { HAKO_DIR "/init", NULL }; 111 | if(execv(init_cmd[0], init_cmd) == -1) 112 | { 113 | perror("Could not execute " HAKO_DIR "/init"); 114 | quit(EXIT_FAILURE); 115 | } 116 | } 117 | else // parent 118 | { 119 | int init_status; 120 | errno = 0; 121 | while(waitpid(init_pid, &init_status, 0) != init_pid && errno == EINTR) 122 | { } 123 | 124 | if(!WIFEXITED(init_status) || WEXITSTATUS(init_status) != 0) 125 | { 126 | fprintf( 127 | stderr, HAKO_DIR "/init failed with %s: %d\n", 128 | WIFEXITED(init_status) ? "status" : "signal", 129 | WIFEXITED(init_status) ? WEXITSTATUS(init_status) : WTERMSIG(init_status) 130 | ); 131 | quit(EXIT_FAILURE); 132 | } 133 | } 134 | 135 | // Finalize sandbox 136 | 137 | if(!sandbox_cfg->writable 138 | && mount(NULL, ".", NULL, MS_REMOUNT | MS_BIND | MS_RDONLY, NULL) == -1) 139 | { 140 | perror("Could not make sandbox read-only"); 141 | quit(EXIT_FAILURE); 142 | } 143 | 144 | if(syscall(__NR_pivot_root, ".", HAKO_DIR) == -1) 145 | { 146 | perror("Could not pivot root"); 147 | quit(EXIT_FAILURE); 148 | } 149 | 150 | if(chdir("/") == -1) 151 | { 152 | perror("Could not chdir into new root"); 153 | quit(EXIT_FAILURE); 154 | } 155 | 156 | if(umount2(HAKO_DIR, MNT_DETACH) == -1) 157 | { 158 | perror("Could not unmount old root"); 159 | quit(EXIT_FAILURE); 160 | } 161 | 162 | // Show time 163 | if(!execute_run_ctx(&sandbox_cfg->run_ctx)) 164 | { 165 | quit(EXIT_FAILURE); 166 | } 167 | 168 | quit: 169 | return exit_code; 170 | } 171 | 172 | int 173 | main(int argc, char* argv[]) 174 | { 175 | (void)argc; 176 | 177 | int exit_code = EXIT_SUCCESS; 178 | 179 | struct optparse_long opts[] = { 180 | {"help", 'h', OPTPARSE_NONE}, 181 | {"writable", 'W', OPTPARSE_NONE}, 182 | {"network", 'N', OPTPARSE_OPTIONAL}, 183 | {"pid-file", 'p', OPTPARSE_REQUIRED}, 184 | RUN_CTX_OPTS, 185 | {0} 186 | }; 187 | 188 | const char* help[] = { 189 | NULL, "Print this message", 190 | NULL, "Make sandbox root filesystem writable", 191 | "FILE", "Set sandbox's network namespace (default: host)", 192 | "FILE", "Write pid of sandbox to this file", 193 | RUN_CTX_HELP, 194 | }; 195 | 196 | const char* usage = "Usage: " PROG_NAME " [options] [command] [args]"; 197 | 198 | int option; 199 | const char* pid_file = NULL; 200 | struct optparse options; 201 | struct sandbox_cfg_s sandbox_cfg = { .netns_flag = CLONE_NEWNET }; 202 | init_run_ctx(&sandbox_cfg.run_ctx, argc); 203 | optparse_init(&options, argv); 204 | options.permute = 0; 205 | 206 | while((option = optparse_long(&options, opts, NULL)) != -1) 207 | { 208 | switch(option) 209 | { 210 | case 'h': 211 | optparse_help(usage, opts, help); 212 | quit(EXIT_SUCCESS); 213 | break; 214 | case 'W': 215 | sandbox_cfg.writable = true; 216 | break; 217 | case 'N': 218 | sandbox_cfg.netns = options.optarg; 219 | sandbox_cfg.netns_flag = 0; 220 | break; 221 | case 'p': 222 | pid_file = options.optarg; 223 | break; 224 | CASE_RUN_OPT: 225 | if(!parse_run_option( 226 | &sandbox_cfg.run_ctx, PROG_NAME, option, options.optarg 227 | )) 228 | { 229 | quit(EXIT_FAILURE); 230 | } 231 | break; 232 | case '?': 233 | fprintf(stderr, PROG_NAME ": %s\n", options.errmsg); 234 | quit(EXIT_FAILURE); 235 | break; 236 | default: 237 | fprintf(stderr, "Unimplemented option\n"); 238 | quit(EXIT_FAILURE); 239 | break; 240 | } 241 | } 242 | 243 | sandbox_cfg.sandbox_dir = parse_run_command(&sandbox_cfg.run_ctx, &options); 244 | 245 | if(sandbox_cfg.sandbox_dir == NULL) 246 | { 247 | fprintf(stderr, PROG_NAME ": must provide sandbox dir\n"); 248 | quit(EXIT_FAILURE); 249 | } 250 | 251 | // Create a child process in a new namespace 252 | long stack_size = sysconf(_SC_PAGESIZE); 253 | char* child_stack = alloca(stack_size); 254 | int clone_flags = 0 255 | | SIGCHLD 256 | | CLONE_VFORK // wait until child execs away 257 | | CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNS | CLONE_NEWUTS 258 | | sandbox_cfg.netns_flag; 259 | pid_t child_pid = clone( 260 | sandbox_entry, child_stack + stack_size, clone_flags, &sandbox_cfg 261 | ); 262 | if(child_pid == -1) 263 | { 264 | perror("clone() failed"); 265 | quit(EXIT_FAILURE); 266 | } 267 | 268 | if(!drop_privileges(&sandbox_cfg.run_ctx)) { quit(EXIT_FAILURE); } 269 | 270 | if(pid_file != NULL) 271 | { 272 | FILE* file = fopen(pid_file, "w"); 273 | if(file == NULL) 274 | { 275 | perror("Could not open pid file for writing"); 276 | quit(EXIT_FAILURE); 277 | } 278 | 279 | bool written = fprintf(file, "%" PRIdMAX, (intmax_t)child_pid) > 0; 280 | bool closed = fclose(file) == 0; 281 | if(!(written && closed)) 282 | { 283 | fprintf(stderr, "Could not write pid to file\n"); 284 | quit(EXIT_FAILURE); 285 | } 286 | } 287 | 288 | sigset_t set; 289 | sigfillset(&set); 290 | sigprocmask(SIG_BLOCK, &set, NULL); 291 | for(;;) 292 | { 293 | int sig, status; 294 | sigwait(&set, &sig); 295 | switch(sig) 296 | { 297 | case SIGINT: 298 | case SIGTERM: 299 | case SIGHUP: 300 | case SIGQUIT: 301 | // Kill child manualy because SIGKILL from PR_SET_PDEATHSIG can 302 | // be handled (and ignored) by child. 303 | kill(child_pid, SIGKILL); 304 | quit(128 + sig); 305 | break; 306 | case SIGCHLD: 307 | if(waitpid(child_pid, &status, WNOHANG) > 0) 308 | { 309 | quit( 310 | WIFEXITED(status) ? 311 | WEXITSTATUS(status) : (128 + WTERMSIG(status)) 312 | ); 313 | } 314 | break; 315 | } 316 | } 317 | 318 | quit: 319 | cleanup_run_ctx(&sandbox_cfg.run_ctx); 320 | 321 | return exit_code; 322 | } 323 | -------------------------------------------------------------------------------- /src/optparse-help.h: -------------------------------------------------------------------------------- 1 | #ifndef OPTPARSE_HELP_H 2 | #define OPTPARSE_HELP_H 3 | 4 | #include "optparse.h" 5 | 6 | #ifndef OPTPARSE_HELP_API 7 | # define OPTPARSE_HELP_API 8 | #endif 9 | 10 | OPTPARSE_HELP_API void 11 | optparse_help( 12 | const char* title, const struct optparse_long* opts, const char* help[] 13 | ); 14 | 15 | #ifdef OPTPARSE_HELP_IMPLEMENTATION 16 | 17 | #include 18 | #include 19 | 20 | static inline unsigned int 21 | optparse_help_opt_width_(const struct optparse_long opt, const char* param) 22 | { 23 | return 0 24 | + 2 // "-o" 25 | + (opt.longname ? 3 + strlen(opt.longname) : 0) // ",--option", 26 | + (param ? (opt.argtype == OPTPARSE_REQUIRED ? 1 : 3) + strlen(param) : 0); //"[=param]" or " param" 27 | } 28 | 29 | void 30 | optparse_help( 31 | const char* title, const struct optparse_long* opts, const char* help[] 32 | ) 33 | { 34 | printf("%s\n\nAvailable options:\n\n", title); 35 | 36 | unsigned int max_width = 0; 37 | for(unsigned int i = 0;; ++i) 38 | { 39 | struct optparse_long opt = opts[i]; 40 | if(!opt.shortname) { break; } 41 | 42 | unsigned int opt_width = optparse_help_opt_width_(opt, help[i * 2]); 43 | max_width = opt_width > max_width ? opt_width : max_width; 44 | } 45 | 46 | for(unsigned int i = 0;; ++i) 47 | { 48 | struct optparse_long opt = opts[i]; 49 | if(!opt.shortname) { break; } 50 | 51 | printf(" -%c", opt.shortname); 52 | if(opt.longname) 53 | { 54 | printf(",--%s", opt.longname); 55 | } 56 | 57 | const char* param = help[i * 2]; 58 | const char* description = help[i * 2 + 1]; 59 | 60 | switch(opt.argtype) 61 | { 62 | case OPTPARSE_NONE: 63 | break; 64 | case OPTPARSE_OPTIONAL: 65 | printf("[=%s]", param); 66 | break; 67 | case OPTPARSE_REQUIRED: 68 | printf(" %s", param); 69 | break; 70 | } 71 | printf("%*s", max_width + 4 - optparse_help_opt_width_(opt, param), ""); 72 | printf("%s\n", description); 73 | } 74 | } 75 | 76 | #endif 77 | 78 | #endif 79 | -------------------------------------------------------------------------------- /src/optparse.h: -------------------------------------------------------------------------------- 1 | /* Optparse --- portable, reentrant, embeddable, getopt-like option parser 2 | * 3 | * This is free and unencumbered software released into the public domain. 4 | * 5 | * To get the implementation, define OPTPARSE_IMPLEMENTATION. 6 | * Optionally define OPTPARSE_API to control the API's visibility 7 | * and/or linkage (static, __attribute__, __declspec). 8 | * 9 | * The POSIX getopt() option parser has three fatal flaws. These flaws 10 | * are solved by Optparse. 11 | * 12 | * 1) Parser state is stored entirely in global variables, some of 13 | * which are static and inaccessible. This means only one thread can 14 | * use getopt(). It also means it's not possible to recursively parse 15 | * nested sub-arguments while in the middle of argument parsing. 16 | * Optparse fixes this by storing all state on a local struct. 17 | * 18 | * 2) The POSIX standard provides no way to properly reset the parser. 19 | * This means for portable code that getopt() is only good for one 20 | * run, over one argv with one option string. It also means subcommand 21 | * options cannot be processed with getopt(). Most implementations 22 | * provide a method to reset the parser, but it's not portable. 23 | * Optparse provides an optparse_arg() function for stepping over 24 | * subcommands and continuing parsing of options with another option 25 | * string. The Optparse struct itself can be passed around to 26 | * subcommand handlers for additional subcommand option parsing. A 27 | * full reset can be achieved by with an additional optparse_init(). 28 | * 29 | * 3) Error messages are printed to stderr. This can be disabled with 30 | * opterr, but the messages themselves are still inaccessible. 31 | * Optparse solves this by writing an error message in its errmsg 32 | * field. The downside to Optparse is that this error message will 33 | * always be in English rather than the current locale. 34 | * 35 | * Optparse should be familiar with anyone accustomed to getopt(), and 36 | * it could be a nearly drop-in replacement. The option string is the 37 | * same and the fields have the same names as the getopt() global 38 | * variables (optarg, optind, optopt). 39 | * 40 | * Optparse also supports GNU-style long options with optparse_long(). 41 | * The interface is slightly different and simpler than getopt_long(). 42 | * 43 | * By default, argv is permuted as it is parsed, moving non-option 44 | * arguments to the end. This can be disabled by setting the `permute` 45 | * field to 0 after initialization. 46 | */ 47 | #ifndef OPTPARSE_H 48 | #define OPTPARSE_H 49 | 50 | #ifndef OPTPARSE_API 51 | # define OPTPARSE_API 52 | #endif 53 | 54 | struct optparse { 55 | char **argv; 56 | int permute; 57 | int optind; 58 | int optopt; 59 | char *optarg; 60 | char errmsg[64]; 61 | int subopt; 62 | }; 63 | 64 | enum optparse_argtype { 65 | OPTPARSE_NONE, 66 | OPTPARSE_REQUIRED, 67 | OPTPARSE_OPTIONAL 68 | }; 69 | 70 | struct optparse_long { 71 | const char *longname; 72 | int shortname; 73 | enum optparse_argtype argtype; 74 | }; 75 | 76 | /** 77 | * Initializes the parser state. 78 | */ 79 | OPTPARSE_API 80 | void optparse_init(struct optparse *options, char **argv); 81 | 82 | /** 83 | * Read the next option in the argv array. 84 | * @param optstring a getopt()-formatted option string. 85 | * @return the next option character, -1 for done, or '?' for error 86 | * 87 | * Just like getopt(), a character followed by no colons means no 88 | * argument. One colon means the option has a required argument. Two 89 | * colons means the option takes an optional argument. 90 | */ 91 | OPTPARSE_API 92 | int optparse(struct optparse *options, const char *optstring); 93 | 94 | /** 95 | * Handles GNU-style long options in addition to getopt() options. 96 | * This works a lot like GNU's getopt_long(). The last option in 97 | * longopts must be all zeros, marking the end of the array. The 98 | * longindex argument may be NULL. 99 | */ 100 | OPTPARSE_API 101 | int optparse_long(struct optparse *options, 102 | const struct optparse_long *longopts, 103 | int *longindex); 104 | 105 | /** 106 | * Used for stepping over non-option arguments. 107 | * @return the next non-option argument, or NULL for no more arguments 108 | * 109 | * Argument parsing can continue with optparse() after using this 110 | * function. That would be used to parse the options for the 111 | * subcommand returned by optparse_arg(). This function allows you to 112 | * ignore the value of optind. 113 | */ 114 | OPTPARSE_API 115 | char *optparse_arg(struct optparse *options); 116 | 117 | /* Implementation */ 118 | #ifdef OPTPARSE_IMPLEMENTATION 119 | 120 | #define OPTPARSE_MSG_INVALID "invalid option" 121 | #define OPTPARSE_MSG_MISSING "option requires an argument" 122 | #define OPTPARSE_MSG_TOOMANY "option takes no arguments" 123 | 124 | static int 125 | optparse_error(struct optparse *options, const char *msg, const char *data) 126 | { 127 | unsigned p = 0; 128 | const char *sep = " -- '"; 129 | while (*msg) 130 | options->errmsg[p++] = *msg++; 131 | while (*sep) 132 | options->errmsg[p++] = *sep++; 133 | while (p < sizeof(options->errmsg) - 2 && *data) 134 | options->errmsg[p++] = *data++; 135 | options->errmsg[p++] = '\''; 136 | options->errmsg[p++] = '\0'; 137 | return '?'; 138 | } 139 | 140 | OPTPARSE_API 141 | void 142 | optparse_init(struct optparse *options, char **argv) 143 | { 144 | options->argv = argv; 145 | options->permute = 1; 146 | options->optind = 1; 147 | options->subopt = 0; 148 | options->optarg = 0; 149 | options->errmsg[0] = '\0'; 150 | } 151 | 152 | static int 153 | optparse_is_dashdash(const char *arg) 154 | { 155 | return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] == '\0'; 156 | } 157 | 158 | static int 159 | optparse_is_shortopt(const char *arg) 160 | { 161 | return arg != 0 && arg[0] == '-' && arg[1] != '-' && arg[1] != '\0'; 162 | } 163 | 164 | static int 165 | optparse_is_longopt(const char *arg) 166 | { 167 | return arg != 0 && arg[0] == '-' && arg[1] == '-' && arg[2] != '\0'; 168 | } 169 | 170 | static void 171 | optparse_permute(struct optparse *options, int index) 172 | { 173 | char *nonoption = options->argv[index]; 174 | int i; 175 | for (i = index; i < options->optind - 1; i++) 176 | options->argv[i] = options->argv[i + 1]; 177 | options->argv[options->optind - 1] = nonoption; 178 | } 179 | 180 | static int 181 | optparse_argtype(const char *optstring, char c) 182 | { 183 | int count = OPTPARSE_NONE; 184 | if (c == ':') 185 | return -1; 186 | for (; *optstring && c != *optstring; optstring++); 187 | if (!*optstring) 188 | return -1; 189 | if (optstring[1] == ':') 190 | count += optstring[2] == ':' ? 2 : 1; 191 | return count; 192 | } 193 | 194 | OPTPARSE_API 195 | int 196 | optparse(struct optparse *options, const char *optstring) 197 | { 198 | int type; 199 | char *next; 200 | char *option = options->argv[options->optind]; 201 | options->errmsg[0] = '\0'; 202 | options->optopt = 0; 203 | options->optarg = 0; 204 | if (option == 0) { 205 | return -1; 206 | } else if (optparse_is_dashdash(option)) { 207 | options->optind++; /* consume "--" */ 208 | return -1; 209 | } else if (!optparse_is_shortopt(option)) { 210 | if (options->permute) { 211 | int index = options->optind++; 212 | int r = optparse(options, optstring); 213 | optparse_permute(options, index); 214 | options->optind--; 215 | return r; 216 | } else { 217 | return -1; 218 | } 219 | } 220 | option += options->subopt + 1; 221 | options->optopt = option[0]; 222 | type = optparse_argtype(optstring, option[0]); 223 | next = options->argv[options->optind + 1]; 224 | switch (type) { 225 | case -1: { 226 | char str[2] = {0, 0}; 227 | str[0] = option[0]; 228 | options->optind++; 229 | return optparse_error(options, OPTPARSE_MSG_INVALID, str); 230 | } 231 | case OPTPARSE_NONE: 232 | if (option[1]) { 233 | options->subopt++; 234 | } else { 235 | options->subopt = 0; 236 | options->optind++; 237 | } 238 | return option[0]; 239 | case OPTPARSE_REQUIRED: 240 | options->subopt = 0; 241 | options->optind++; 242 | if (option[1]) { 243 | options->optarg = option + 1; 244 | } else if (next != 0) { 245 | options->optarg = next; 246 | options->optind++; 247 | } else { 248 | char str[2] = {0, 0}; 249 | str[0] = option[0]; 250 | options->optarg = 0; 251 | return optparse_error(options, OPTPARSE_MSG_MISSING, str); 252 | } 253 | return option[0]; 254 | case OPTPARSE_OPTIONAL: 255 | options->subopt = 0; 256 | options->optind++; 257 | if (option[1]) 258 | options->optarg = option + 1; 259 | else 260 | options->optarg = 0; 261 | return option[0]; 262 | } 263 | return 0; 264 | } 265 | 266 | OPTPARSE_API 267 | char * 268 | optparse_arg(struct optparse *options) 269 | { 270 | char *option = options->argv[options->optind]; 271 | options->subopt = 0; 272 | if (option != 0) 273 | options->optind++; 274 | return option; 275 | } 276 | 277 | static int 278 | optparse_longopts_end(const struct optparse_long *longopts, int i) 279 | { 280 | return !longopts[i].longname && !longopts[i].shortname; 281 | } 282 | 283 | static void 284 | optparse_from_long(const struct optparse_long *longopts, char *optstring) 285 | { 286 | char *p = optstring; 287 | int i; 288 | for (i = 0; !optparse_longopts_end(longopts, i); i++) { 289 | if (longopts[i].shortname) { 290 | int a; 291 | *p++ = longopts[i].shortname; 292 | for (a = 0; a < (int)longopts[i].argtype; a++) 293 | *p++ = ':'; 294 | } 295 | } 296 | *p = '\0'; 297 | } 298 | 299 | /* Unlike strcmp(), handles options containing "=". */ 300 | static int 301 | optparse_longopts_match(const char *longname, const char *option) 302 | { 303 | const char *a = option, *n = longname; 304 | if (longname == 0) 305 | return 0; 306 | for (; *a && *n && *a != '='; a++, n++) 307 | if (*a != *n) 308 | return 0; 309 | return *n == '\0' && (*a == '\0' || *a == '='); 310 | } 311 | 312 | /* Return the part after "=", or NULL. */ 313 | static char * 314 | optparse_longopts_arg(char *option) 315 | { 316 | for (; *option && *option != '='; option++); 317 | if (*option == '=') 318 | return option + 1; 319 | else 320 | return 0; 321 | } 322 | 323 | static int 324 | optparse_long_fallback(struct optparse *options, 325 | const struct optparse_long *longopts, 326 | int *longindex) 327 | { 328 | int result; 329 | char optstring[96 * 3 + 1]; /* 96 ASCII printable characters */ 330 | optparse_from_long(longopts, optstring); 331 | result = optparse(options, optstring); 332 | if (longindex != 0) { 333 | *longindex = -1; 334 | if (result != -1) { 335 | int i; 336 | for (i = 0; !optparse_longopts_end(longopts, i); i++) 337 | if (longopts[i].shortname == options->optopt) 338 | *longindex = i; 339 | } 340 | } 341 | return result; 342 | } 343 | 344 | OPTPARSE_API 345 | int 346 | optparse_long(struct optparse *options, 347 | const struct optparse_long *longopts, 348 | int *longindex) 349 | { 350 | int i; 351 | char *option = options->argv[options->optind]; 352 | if (option == 0) { 353 | return -1; 354 | } else if (optparse_is_dashdash(option)) { 355 | options->optind++; /* consume "--" */ 356 | return -1; 357 | } else if (optparse_is_shortopt(option)) { 358 | return optparse_long_fallback(options, longopts, longindex); 359 | } else if (!optparse_is_longopt(option)) { 360 | if (options->permute) { 361 | int index = options->optind++; 362 | int r = optparse_long(options, longopts, longindex); 363 | optparse_permute(options, index); 364 | options->optind--; 365 | return r; 366 | } else { 367 | return -1; 368 | } 369 | } 370 | 371 | /* Parse as long option. */ 372 | options->errmsg[0] = '\0'; 373 | options->optopt = 0; 374 | options->optarg = 0; 375 | option += 2; /* skip "--" */ 376 | options->optind++; 377 | for (i = 0; !optparse_longopts_end(longopts, i); i++) { 378 | const char *name = longopts[i].longname; 379 | if (optparse_longopts_match(name, option)) { 380 | char *arg; 381 | if (longindex) 382 | *longindex = i; 383 | options->optopt = longopts[i].shortname; 384 | arg = optparse_longopts_arg(option); 385 | if (longopts[i].argtype == OPTPARSE_NONE && arg != 0) { 386 | return optparse_error(options, OPTPARSE_MSG_TOOMANY, name); 387 | } if (arg != 0) { 388 | options->optarg = arg; 389 | } else if (longopts[i].argtype == OPTPARSE_REQUIRED) { 390 | options->optarg = options->argv[options->optind]; 391 | if (options->optarg == 0) 392 | return optparse_error(options, OPTPARSE_MSG_MISSING, name); 393 | else 394 | options->optind++; 395 | } 396 | return options->optopt; 397 | } 398 | } 399 | return optparse_error(options, OPTPARSE_MSG_INVALID, option); 400 | } 401 | 402 | #endif /* OPTPARSE_IMPLEMENTATION */ 403 | #endif /* OPTPARSE_H */ 404 | --------------------------------------------------------------------------------