├── .github └── workflows │ └── tests.yml ├── LICENSE ├── Makefile ├── README.md ├── debian ├── changelog ├── compat ├── control ├── install ├── manpages └── rules ├── jchroot.8 └── jchroot.c /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | - cron: 0 7 1 * * 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: | 14 | make 15 | - name: Test 16 | run: | 17 | ./jchroot --help 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The license below applies to most, but not all content in this project. 2 | Files with different licensing and authorship terms are marked as such. 3 | That information must be considered when ensuring licensing compliance. 4 | 5 | ISC License 6 | 7 | Copyright (c) 2011, Vincent Bernat 8 | 9 | Permission to use, copy, modify, and/or distribute this software for any 10 | purpose with or without fee is hereby granted, provided that the above 11 | copyright notice and this permission notice appear in all copies. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 14 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 15 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 16 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 17 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 18 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 19 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-Werror -Wall -ansi -g -O2 2 | LDFLAGS= 3 | EXEC=jchroot 4 | 5 | all: $(EXEC) 6 | 7 | jchroot: jchroot.o 8 | $(CC) -o $@ $^ $(LDFLAGS) 9 | 10 | clean: 11 | rm -f *.o $(EXEC) 12 | 13 | 14 | .PHONY: clean 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jchroot: a chroot with more isolation 2 | ===================================== 3 | 4 | Recent Linux kernels are now able to provide a new PID namespace to a 5 | newly created process. The process becomes PID 1 in its own namespace 6 | and all processes created in this namespace will be killed when the 7 | first process terminates. This allows to reliably kill any process 8 | started by the first process, even when they double fork. It also 9 | ensures a better isolation. 10 | 11 | The same applies for mount points and IPC. If you combine those three 12 | namespaces with a standard chroot, you get a chroot on steroids. You 13 | can launch any (non malicious) process in this chroot, it won't 14 | interfere with your main system and everybody will be killed when you 15 | exit the shell. Any filesystem that was mounted will also be unmounted 16 | automatically. 17 | 18 | This is what jchroot does: 19 | 20 | 0. Setup user/group mappings. 21 | 1. provide a new PID/IPC/mount/UTS namespace 22 | 2. mount anything you want 23 | 3. set hostname if needed 24 | 4. chroot to your target 25 | 5. drop privileges if needed 26 | 6. execute your command 27 | 28 | After your command has been executed, any process started by the 29 | execution of this command will be killed, any IPC will be freed, any 30 | mount point will be unmounted. All clean! 31 | 32 | See also [schroot][1] and [lxc][2]. `schroot` is not yet able to do 33 | this, but this is planned. See [bug #637870][3]. `lxc` should be able 34 | to do this but seems targeted at more complex situations... If you use 35 | systemd, look at `nspawn` or `systemd-nspawn` which does almost the 36 | same thing than jchroot. You could also use `unshare` (with `chroot`) 37 | from `util-linux` package. 38 | 39 | [1]: http://packages.qa.debian.org/s/schroot.html 40 | [2]: http://lxc.sourceforge.net/ 41 | [3]: http://bugs.debian.org/637870 42 | 43 | Security note 44 | ------------- 45 | 46 | It should be noted that a privileged process inside jchroot may be 47 | able to escape unless its privileges are reduced. For example, it 48 | could fiddle with `/dev/kmem` or mount any filesystem after creating 49 | the appropriate node. 50 | 51 | If you seek a complex isolation, you are better off with [lxc][2] 52 | which bundles many security mechanisms. 53 | 54 | You may want to use user namespaces to increase the security of the 55 | chroot: 56 | 57 | ./jchroot -U -u 0 -g 0 -M "0 $(id -u) 1" -G "0 $(id -g) 1" /path/to/chroot cmd 58 | 59 | Installation & use 60 | ------------------ 61 | 62 | Just use `make` to get `jchroot`. Then `jchroot --help` to get help. 63 | 64 | Related projects 65 | ---------------- 66 | 67 | [ChrootX](https://github.com/Spiritdude/ChrootX) is a wrapper around 68 | `chroot`/`jchroot`. 69 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | jchroot (1.1-1) UNRELEASED; urgency=medium 2 | 3 | * New upstream release. 4 | 5 | -- Vincent Bernat Tue, 11 May 2021 23:10:12 +0200 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 9 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: jchroot 2 | Maintainer: Vincent Bernat 3 | Priority: optional 4 | Section: admin 5 | Build-Depends: debhelper (>= 9) 6 | 7 | Package: jchroot 8 | Architecture: any 9 | Depends: ${misc:Depends}, ${shlibs:Depends} 10 | Description: chroot with more isolation 11 | jchroot uses namespaces to run commands with better isolation than 12 | with chroot. This is what jchroot does: 13 | . 14 | 0. Setup user/group mappings. 15 | 1. provide a new PID/IPC/mount/UTS namespace 16 | 2. mount anything you want 17 | 3. set hostname if needed 18 | 4. chroot to your target 19 | 5. drop privileges if needed 20 | 6. execute your command 21 | . 22 | After your command has been executed, any process started by the 23 | execution of this command will be killed, any IPC will be freed, any 24 | mount point will be unmounted. All clean! 25 | -------------------------------------------------------------------------------- /debian/install: -------------------------------------------------------------------------------- 1 | jchroot usr/sbin 2 | -------------------------------------------------------------------------------- /debian/manpages: -------------------------------------------------------------------------------- 1 | jchroot.8 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | %: 4 | dh $@ 5 | -------------------------------------------------------------------------------- /jchroot.8: -------------------------------------------------------------------------------- 1 | .\" Copyright (c) 2011 Vincent Bernat 2 | .\" 3 | .\" Permission to use, copy, modify, and/or distribute this software for any 4 | .\" purpose with or without fee is hereby granted, provided that the above 5 | .\" copyright notice and this permission notice appear in all copies. 6 | .\" 7 | .\" THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | .\" WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | .\" MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | .\" ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | .\" WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | .\" ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | .\" OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | .\" 15 | .Dd $Mdocdate: August 17 2011 $ 16 | .Dt JCHROOT 8 17 | .Os 18 | .Sh NAME 19 | .Nm jchroot 20 | .Nd chroot with more isolation 21 | .Sh SYNOPSIS 22 | .Nm 23 | .Op Fl h | Fl -help 24 | .Op Fl U | Fl -new-user-ns 25 | .Op Fl N | Fl -new-network-ns 26 | .Op Fl u | Fl -user Ar user 27 | .Op Fl g | Fl -group Ar group 28 | .Op Fl f | Fl -fstab Ar fstab 29 | .Op Fl n | Fl -hostname Ar hostname 30 | .Op Fl M | Fl -uid-map Ar map 31 | .Op Fl G | Fl -gid-map Ar map 32 | .Op Fl p | Fl -pidfile Ar pidfile 33 | .Op Fl e | Fl -env Ar name=value 34 | .Op Fl c | Fl -chdir Ar directory 35 | .Op Fl b | Fl -background 36 | .Ar target 37 | .Op -- 38 | .Ar command 39 | .Sh DESCRIPTION 40 | .Nm 41 | provides a chroot to run a command or a shell with more isolation, 42 | thanks to features introduced in recent Linux kernels (2.6.24 or 43 | later). Like 44 | .Xr chroot 8 , 45 | .Nm 46 | will change the root directory to the specified 47 | .Ar target 48 | before running the provided 49 | .Ar command 50 | but it will also provide a new PID namespace, a new mount namespace 51 | and a new IPC namespace to ensure better isolation. 52 | .Pp 53 | Moreover, when the command exits, the kernel will automatically kill 54 | any process that was started by the command, umount any filesystems 55 | mounted by 56 | .Nm 57 | or the commmand and cleanup the IPC namespace. Therefore, most daemons 58 | can be run safely from the chroot: they will be killed on exit. 59 | .Pp 60 | The options are as follows: 61 | .Bl -tag -width Ds 62 | .It Fl N | Fl -new-network-ns 63 | Switch to a new network namespace. The program will be run in a fresh 64 | network namespace. Unless additional actions are taken, this means 65 | that the program won't get any network access. 66 | .It Fl U | Fl -new-user-ns 67 | Switch to a new user namespace. It allows 68 | .Nm 69 | to be run without being root. It requires a recent kernel (3.8+) with 70 | support for user namespace. 71 | .It Fl M | Fl -uid-map Ar mapping 72 | Use the provided user-mapping in the new user namespace. This option 73 | should not be used without The 74 | .Fl U 75 | option. The mapping is explained in 76 | .Xr user_namespaces 7 77 | manual page. A mapping is a record with the ID range start inside the 78 | namespace, the ID range start outside the namespace and the length of 79 | the range. 80 | .It Fl G | Fl -gid-map Ar mapping 81 | Use the provided group-mapping in the new user namespace. See 82 | .Fl M 83 | for more information. 84 | .It Fl u | Fl -user Ar user 85 | Specify user to use after chrooting. This can be specified as a user 86 | name or an UID. In case of a user name, the primary group of the user 87 | is also used unless another group is specified. 88 | .It Fl g | Fl -group Ar group 89 | Specify primary group to use after chrooting. This can be specified 90 | as a group name or a GID. 91 | .It Fl f | Fl -fstab Ar fstab 92 | Specify a file location in the 93 | .Xr fstab 5 94 | format containing mount points relative to 95 | .Ar target 96 | to mount inside the chroot. Here is an example of such a file: 97 | .Bd -literal 98 | proc /proc proc defaults 0 0 99 | sys /sys sysfs defaults 0 0 100 | /home /home none bind,rw 0 0 101 | /dev/pts /dev/pts none bind,rw 0 0 102 | /var/run /var/run tmpfs rw,nosuid,noexec,mode=755 0 0 103 | 104 | /etc/resolv.conf /etc/resolv.conf none bind,ro 0 0 105 | .Ed 106 | .It Fl n | Fl -hostname Ar name 107 | Specify a hostname for the chroot. This enables UTS namespace. 108 | .It Fl p | Fl -pidfile Ar file 109 | Write PID of child process to file. 110 | .It Fl e | Fl -env Ar name=value 111 | Set an environment variable. This option can be specified more than once. 112 | .It Fl c | Fl -chdir Ar directory 113 | Change to the specified directory after entering the chroot. 114 | .It Fl b | Fl -background 115 | Allow processes started by 116 | .Nm 117 | to continue in the background. Otherwise, they will be killed abruptly 118 | with a SIGKILL signal once 119 | .Nm 120 | terminates. Due to the use of a PID namespace, the process running as 121 | PID 1 in the chroot will only handle SIGKILL and SIGSTOP from an 122 | ancestor, and all other signals need a specific handler (no default 123 | handler). Therefore, when using this option and using Ctrl-C to stop a 124 | process, the process may continue the background instead of 125 | terminating. Note this option does not put the command in background 126 | by itself. 127 | .It Fl h | Fl -help 128 | Get help. 129 | .El 130 | .Sh SEE ALSO 131 | .Xr chroot 8 , 132 | .Xr clone 2 , 133 | .Xr lxc 7 134 | .Sh AUTHORS 135 | .An -nosplit 136 | The 137 | .Nm 138 | program was written by 139 | .An Vincent Bernat Aq bernat@luffy.cx . 140 | -------------------------------------------------------------------------------- /jchroot.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Vincent Bernat 3 | * 4 | * Permission to use, copy, modify, and/or distribute this software for any 5 | * purpose with or without fee is hereby granted, provided that the above 6 | * copyright notice and this permission notice appear in all copies. 7 | * 8 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 9 | * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 10 | * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 11 | * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 12 | * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 13 | * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 | * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 | */ 16 | 17 | #define _GNU_SOURCE 18 | #define _DEFAULT_SOURCE 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | #ifndef PATH_MAX 39 | # define PATH_MAX 4096 40 | #endif 41 | 42 | struct config { 43 | int pipe_fd[2]; 44 | int userns; 45 | int netns; 46 | int background; 47 | uid_t user; 48 | gid_t group; 49 | char *fstab; 50 | char *hostname; 51 | char *target; 52 | char *const *command; 53 | const char *uid_map; 54 | const char *gid_map; 55 | const char *pid_file; 56 | const char *chdir_to; 57 | }; 58 | 59 | const char *progname; 60 | static void usage(int code) { 61 | fprintf(stderr, 62 | "Usage: %s [OPTIONS] TARGET [--] COMMAND\n" 63 | "\n" 64 | "Available options:\n" 65 | " -U | --new-user-ns Use a new user namespace\n" 66 | " -N | --new-network-ns Use a new network namespace\n" 67 | " -u USER | --user=USER Specify user to use after chroot\n" 68 | " -g USER | --group=USER Specify group to use after chroot\n" 69 | " -f FSTAB | --fstab=FSTAB Specify a fstab(5) file\n" 70 | " -n NAME | --hostname=NAME Specify a hostname\n" 71 | " -M MAP | --uid-map=MAP Comma-separated list of UID mappings\n" 72 | " -G MAP | --gid-map=MAP Comma-separated list of GID mappings\n" 73 | " -p FILE | --pidfile=FILE Write PID of child process to file\n" 74 | " -e N=V | --env=NAME=VALUE Set an environment variable\n" 75 | " -c DIR | --chdir=DIR Change directory inside the chroot\n" 76 | " -b | --background Allow launched command to continue in background\n", 77 | progname); 78 | exit(code); 79 | } 80 | 81 | /* Step 7: Execute command */ 82 | static int step7(struct config *config) { 83 | if (config->chdir_to != NULL && chdir(config->chdir_to)) { 84 | fprintf(stderr, "unable to change directory: %m\n"); 85 | return EXIT_FAILURE; 86 | } 87 | if (!config->background) { 88 | /* Cannot use SIGTERM, it would be ignored. Killing PID 1 will 89 | * also kill other processes. */ 90 | prctl(PR_SET_PDEATHSIG, SIGKILL, 0, 0, 0, 0); 91 | } 92 | if (execvp(config->command[0], config->command) == -1) { 93 | int i = 1; 94 | fprintf(stderr, "unable to execute '%s", config->command[0]); 95 | while (config->command[i]) fprintf(stderr, " %s", config->command[i++]); 96 | fprintf(stderr, "': %m\n"); 97 | return errno; 98 | } 99 | return 0; /* No real return... */ 100 | } 101 | 102 | /* Step 6: Drop (or increase) privileges */ 103 | static int step6(struct config *config) { 104 | if (config->group != (gid_t) -1 && setgid(config->group)) { 105 | fprintf(stderr, "unable to change to GID %d: %m\n", config->group); 106 | return EXIT_FAILURE; 107 | } 108 | if (setgroups(0, NULL)) { 109 | /* This may fail on some recent kernels. See 110 | * https://lwn.net/Articles/626665/ for the rationale. */ 111 | if (!config->userns) { 112 | fprintf(stderr, "unable to drop additional groups: %m\n"); 113 | return EXIT_FAILURE; 114 | } 115 | } 116 | if (config->user != (uid_t) -1 && setuid(config->user)) { 117 | fprintf(stderr, "unable to change to UID %d: %m\n", config->user); 118 | return EXIT_FAILURE; 119 | } 120 | #ifdef PR_SET_NO_NEW_PRIVS 121 | if (config->group != (gid_t) -1 || config->user != (uid_t) -1) { 122 | prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0); 123 | } 124 | #endif 125 | return step7(config); 126 | } 127 | 128 | /* Step 5: Chroot with pivot_root */ 129 | static int step5(struct config *config) { 130 | char *template = NULL; 131 | char *p = NULL; 132 | if (mount(config->target, config->target, "bind", MS_BIND|MS_REC, "") == -1) { 133 | fprintf(stderr, "unable to turn new root into mountpoint: %m\n"); 134 | return EXIT_FAILURE; 135 | } 136 | if (asprintf(&template, "%s/.pivotrootXXXXXX", config->target) == -1) { 137 | fprintf(stderr, "unable to allocate template directory: %m\n"); 138 | return EXIT_FAILURE; 139 | } 140 | if (mkdtemp(template) == NULL) { 141 | fprintf(stderr, "unable to create temporary directory for pivot root: %m\n"); 142 | free(template); 143 | return EXIT_FAILURE; 144 | } 145 | if (syscall(__NR_pivot_root, config->target, template) == -1) { 146 | fprintf(stderr, "unable to pivot root to %s: %m\n", config->target); 147 | rmdir(template); 148 | free(template); 149 | return EXIT_FAILURE; 150 | } 151 | if (chdir("/")) { 152 | fprintf(stderr, "unable to go into chroot: %m\n"); 153 | /* We should cleanup the mount and the temporary directory, but we 154 | * have pivoted and we won't are likely to still use the old 155 | * mount... */ 156 | free(template); 157 | return EXIT_FAILURE; 158 | } 159 | p = template; 160 | p += strlen(config->target); 161 | if (umount2(p, MNT_DETACH) == -1) { 162 | fprintf(stderr, "unable to umount old root: %m\n"); 163 | /* Again, cannot really clean... */ 164 | free(template); 165 | return EXIT_FAILURE; 166 | } 167 | if (rmdir(p) == -1) { 168 | fprintf(stderr, "unable to remove directory for old root: %m\n"); 169 | /* ... */ 170 | free(template); 171 | return EXIT_FAILURE; 172 | } 173 | return step6(config); 174 | } 175 | 176 | /* Step 4: Set hostname */ 177 | static int step4(struct config *config) { 178 | if (config->hostname && 179 | sethostname(config->hostname, strlen(config->hostname))) { 180 | fprintf(stderr, "unable to change hostname to '%s': %m\n", 181 | config->hostname); 182 | } 183 | return step5(config); 184 | } 185 | 186 | struct mount_opt { 187 | char *name; 188 | int clear; 189 | int flag; 190 | }; 191 | 192 | static struct mount_opt mount_opt[] = { 193 | { "defaults", 0, 0 }, 194 | { "ro", 0, MS_RDONLY }, 195 | { "rw", 1, MS_RDONLY }, 196 | { "suid", 1, MS_NOSUID }, 197 | { "nosuid", 0, MS_NOSUID }, 198 | { "dev", 1, MS_NODEV }, 199 | { "nodev", 0, MS_NODEV }, 200 | { "exec", 1, MS_NOEXEC }, 201 | { "noexec", 0, MS_NOEXEC }, 202 | { "sync", 0, MS_SYNCHRONOUS }, 203 | { "async", 1, MS_SYNCHRONOUS }, 204 | { "atime", 1, MS_NOATIME }, 205 | { "noatime", 0, MS_NOATIME }, 206 | { "diratime", 1, MS_NODIRATIME }, 207 | { "nodiratime", 0, MS_NODIRATIME }, 208 | { "bind", 0, MS_BIND }, 209 | { NULL, 0, 0 }, 210 | }; 211 | 212 | /* Step 3: Mount anything needed */ 213 | static int step3(void *arg) { 214 | struct config *config = arg; 215 | 216 | /* First, wait for the parent to be ready */ 217 | char ch; 218 | if (read(config->pipe_fd[0], &ch, 1) != 0) { 219 | fprintf(stderr, "unable to synchronize with parent: %m\n"); 220 | return EXIT_FAILURE; 221 | } 222 | 223 | close(config->pipe_fd[0]); 224 | /* Make sure we have no handles shared with parent anymore, 225 | * these might be used to break out of the chroot */ 226 | unshare(CLONE_FILES); 227 | 228 | if (mount("", "/", "", MS_PRIVATE | MS_REC, "") == -1) { 229 | fprintf(stderr, "unable to make current root private: %m\n"); 230 | return EXIT_FAILURE; 231 | } 232 | if (config->fstab) { 233 | struct mntent *mntent; 234 | char path[256]; 235 | FILE *file; 236 | 237 | file = setmntent(config->fstab, "r"); 238 | if (!file) { 239 | fprintf(stderr, "unable to open '%s': %m\n", config->fstab); 240 | return EXIT_FAILURE; 241 | } 242 | 243 | while ((mntent = getmntent(file))) { 244 | /* We need to parse mnt_opts */ 245 | unsigned long mntflags = 0; 246 | char *mntopts = strdup(mntent->mnt_opts); 247 | char *mntdata = malloc(strlen(mntent->mnt_opts) + 1); 248 | if (!mntdata || !mntopts) { 249 | fprintf(stderr, "unable to allocate memory\n"); 250 | free(mntopts); 251 | free(mntdata); 252 | return EXIT_FAILURE; 253 | } 254 | *mntdata = 0; 255 | 256 | char *opt = NULL; 257 | struct mount_opt *mo; 258 | 259 | for (opt = strtok(mntopts, ","); opt != NULL; 260 | opt = strtok(NULL, ",")) { 261 | /* Is `opt` a known option? */ 262 | for (mo = &mount_opt[0]; 263 | mo->name != NULL; mo++) { 264 | if (!strncmp(opt, mo->name, strlen(mo->name))) { 265 | if (mo->clear) 266 | mntflags &= ~mo->flag; 267 | else 268 | mntflags |= mo->flag; 269 | break; 270 | } 271 | } 272 | if (!mo->name) { 273 | /* `opt` is not know, append it to `mntdata` */ 274 | if (strlen(mntdata)) strcat(mntdata, ","); 275 | strcat(mntdata, opt); 276 | } 277 | } 278 | free(mntopts); 279 | 280 | /* Mount! */ 281 | if (snprintf(path, sizeof(path), "%s%s", 282 | config->target, mntent->mnt_dir) >= sizeof(path)) { 283 | fprintf(stderr, "path too long: %s\n", mntent->mnt_dir); 284 | free(mntdata); 285 | return EXIT_FAILURE; 286 | } 287 | if ((mount(mntent->mnt_fsname, path, mntent->mnt_type, 288 | mntflags & ~MS_REMOUNT, mntdata)) || 289 | /* With MS_BIND, we need to remount to enable some options like "ro" */ 290 | (((mntflags & MS_REMOUNT) || (mntflags & MS_BIND)) && 291 | (mount(mntent->mnt_fsname, path, mntent->mnt_type, 292 | mntflags | MS_REMOUNT, mntdata)))) { 293 | fprintf(stderr, "unable to mount '%s' on '%s': %m\n", 294 | mntent->mnt_fsname, mntent->mnt_dir); 295 | free(mntdata); 296 | return EXIT_FAILURE; 297 | } 298 | free(mntdata); 299 | } 300 | } 301 | return step4(config); 302 | } 303 | 304 | static void step2_update_map(const char *map, char *map_file) { 305 | int fd, j; 306 | ssize_t map_len; 307 | char *mapping = strdup(map); 308 | 309 | map_len = strlen(mapping); 310 | for (j = 0; j < map_len; j++) 311 | if (mapping[j] == ',') 312 | mapping[j] = '\n'; 313 | 314 | fd = open(map_file, O_RDWR); 315 | if (fd == -1) { 316 | fprintf(stderr, "unable to open %s: %m\n", map_file); 317 | exit(EXIT_FAILURE); 318 | } 319 | 320 | if (write(fd, mapping, map_len) != map_len) { 321 | fprintf(stderr, "unable to write to %s: %m\n", map_file); 322 | exit(EXIT_FAILURE); 323 | } 324 | 325 | close(fd); 326 | free(mapping); 327 | } 328 | 329 | /* Step 2: setup user mappings */ 330 | static void step2(struct config *config, pid_t pid) { 331 | char map_path[PATH_MAX]; 332 | if (config->uid_map != NULL) { 333 | snprintf(map_path, PATH_MAX, "/proc/%ld/uid_map", (long) pid); 334 | step2_update_map(config->uid_map, map_path); 335 | } 336 | if (config->gid_map != NULL) { 337 | snprintf(map_path, PATH_MAX, "/proc/%ld/gid_map", (long) pid); 338 | step2_update_map(config->gid_map, map_path); 339 | } 340 | close(config->pipe_fd[1]); /* Sync with child */ 341 | } 342 | 343 | /* Step 1: create a new PID/IPC/NS/UTS namespace */ 344 | static int step1(struct config *config) { 345 | int ret; 346 | pid_t pid; 347 | 348 | long stack_size = sysconf(_SC_PAGESIZE); 349 | void *stack = alloca(stack_size) + stack_size; 350 | int flags = CLONE_NEWPID | CLONE_NEWIPC | CLONE_NEWNS; 351 | 352 | if (pipe(config->pipe_fd) == -1) { 353 | fprintf(stderr, "failed to create a pipe: %m\n"); 354 | return EXIT_FAILURE; 355 | } 356 | 357 | FILE *pid_file = NULL; 358 | 359 | if (config->pid_file) { 360 | pid_file = fopen(config->pid_file, "w"); 361 | if (pid_file == NULL) { 362 | fprintf(stderr, "failed to open pid file %s for writing: %m\n", 363 | config->pid_file); 364 | return EXIT_FAILURE; 365 | } 366 | } 367 | 368 | if (config->hostname) flags |= CLONE_NEWUTS; 369 | if (config->userns) flags |= CLONE_NEWUSER; 370 | if (config->netns) flags |= CLONE_NEWNET; 371 | pid = clone(step3, 372 | stack, 373 | SIGCHLD | flags | CLONE_FILES, 374 | config); 375 | if (pid < 0) { 376 | fprintf(stderr, "failed to clone: %m\n"); 377 | if (config->pid_file) fclose(pid_file); 378 | return EXIT_FAILURE; 379 | } 380 | 381 | if (config->pid_file) { 382 | if (fprintf(pid_file, "%u", pid) < 0) { 383 | fprintf(stderr, "failed to write PID (%u) to file %s: %m\n", 384 | pid, config->pid_file); 385 | fclose(pid_file); 386 | return EXIT_FAILURE; 387 | } 388 | fclose(pid_file); 389 | } 390 | 391 | step2(config, pid); 392 | 393 | while (waitpid(pid, &ret, 0) < 0 && errno == EINTR) 394 | continue; 395 | return WIFEXITED(ret)?WEXITSTATUS(ret):EXIT_FAILURE; 396 | } 397 | 398 | int main(int argc, char * argv[]) { 399 | struct config config; 400 | memset(&config, 0, sizeof(struct config)); 401 | config.user = config.group = -1; 402 | progname = argv[0]; 403 | 404 | int c; 405 | while (1) { 406 | int option_index = 0; 407 | static struct option long_options[] = { 408 | { "new-user-ns", no_argument, 0, 'U' }, 409 | { "new-network-ns", no_argument, 0, 'N' }, 410 | { "user", required_argument, 0, 'u' }, 411 | { "group", required_argument, 0, 'g' }, 412 | { "fstab", required_argument, 0, 'f' }, 413 | { "hostname", required_argument, 0, 'n' }, 414 | { "uid-map", required_argument, 0, 'M' }, 415 | { "gid-map", required_argument, 0, 'G' }, 416 | { "pidfile", required_argument, 0, 'p' }, 417 | { "env", required_argument, 0, 'e' }, 418 | { "chdir", required_argument, 0, 'c' }, 419 | { "background", no_argument, 0, 'b' }, 420 | { "help", no_argument, 0, 'h' }, 421 | { 0, 0, 0, 0 } 422 | }; 423 | 424 | c = getopt_long(argc, argv, "hNUu:g:f:n:M:G:p:e:c:b", 425 | long_options, &option_index); 426 | if (c == -1) break; 427 | 428 | switch (c) { 429 | case 'U': 430 | config.userns = 1; 431 | break; 432 | case 'M': 433 | config.uid_map = optarg; 434 | break; 435 | case 'G': 436 | config.gid_map = optarg; 437 | break; 438 | case 'N': 439 | config.netns = 1; 440 | break; 441 | case 'b': 442 | config.background = 1; 443 | break; 444 | case 'u': 445 | if (!optarg) usage(EXIT_FAILURE); 446 | 447 | struct passwd *passwd; 448 | passwd = getpwnam(optarg); 449 | if (!passwd) { 450 | config.user = strtoul(optarg, NULL, 10); 451 | if (errno) { 452 | fprintf(stderr, "'%s' is not a valid user\n", optarg); 453 | usage(EXIT_FAILURE); 454 | } 455 | } else { 456 | config.user = passwd->pw_uid; 457 | if (config.group == (gid_t) -1) 458 | config.group = passwd->pw_gid; 459 | } 460 | break; 461 | case 'g': 462 | if (!optarg) usage(EXIT_FAILURE); 463 | 464 | struct group *group; 465 | group = getgrnam(optarg); 466 | if (!group) { 467 | config.group = strtoul(optarg, NULL, 10); 468 | if (errno) { 469 | fprintf(stderr, "'%s' is not a valid group\n", optarg); 470 | usage(EXIT_FAILURE); 471 | } 472 | } else { 473 | config.group = group->gr_gid; 474 | } 475 | break; 476 | case 'f': 477 | if (!optarg) usage(EXIT_FAILURE); 478 | config.fstab = optarg; 479 | break; 480 | case 'n': 481 | if (!optarg) usage(EXIT_FAILURE); 482 | config.hostname = optarg; 483 | break; 484 | case 'e': 485 | if (!optarg) usage(EXIT_FAILURE); 486 | if (putenv(optarg) != 0) { 487 | fprintf(stderr, "failed to set environment variable: %s\n", optarg); 488 | usage(EXIT_FAILURE); 489 | } 490 | break; 491 | case 'c': 492 | if (!optarg) usage(EXIT_FAILURE); 493 | config.chdir_to = optarg; 494 | break; 495 | case 'p': 496 | if (!optarg) usage(EXIT_FAILURE); 497 | config.pid_file = optarg; 498 | break; 499 | case 'h': 500 | usage(EXIT_SUCCESS); 501 | break; 502 | default: 503 | usage(EXIT_FAILURE); 504 | } 505 | } 506 | 507 | if (!config.userns && 508 | (config.uid_map != NULL || config.gid_map != NULL)) { 509 | fprintf(stderr, "cannot use UID/GID mapping without a user namespace\n"); 510 | usage(EXIT_FAILURE); 511 | } 512 | 513 | if (optind == argc) usage(EXIT_FAILURE); 514 | config.target = argv[optind++]; 515 | if (optind == argc) usage(EXIT_FAILURE); 516 | config.command = argv + optind; 517 | 518 | struct stat st; 519 | if (stat(config.target, &st) || !S_ISDIR(st.st_mode)) { 520 | fprintf(stderr, "'%s' is not a directory\n", config.target); 521 | return EXIT_FAILURE; 522 | } 523 | 524 | return step1(&config); 525 | } 526 | --------------------------------------------------------------------------------