├── .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)
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 |
--------------------------------------------------------------------------------