/* * Copyright (C) 2014-2021 Firejail Authors * * This file is part of firejail project * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "firejail.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef O_PATH #define O_PATH 010000000 #endif // Parse the DISPLAY environment variable and return a display number. // Returns -1 if DISPLAY is not set, or is set to anything other than :ddd. int x11_display(void) { const char *display_str = env_get("DISPLAY"); char *endp; unsigned long display; if (!display_str) { if (arg_debug) fputs("DISPLAY is not set\n", stderr); return -1; } if (display_str[0] != ':' || display_str[1] < '0' || display_str[1] > '9') { if (arg_debug) fprintf(stderr, "unsupported DISPLAY form '%s'\n", display_str); return -1; } errno = 0; display = strtoul(display_str+1, &endp, 10); // handling DISPLAY=:0 and also :0.0 if (endp == display_str+1 || (*endp != '\0' && *endp != '.')) { if (arg_debug) fprintf(stderr, "unsupported DISPLAY form '%s'\n", display_str); return -1; } if (errno || display > (unsigned long)INT_MAX) { if (arg_debug) fprintf(stderr, "display number %s is outside the valid range\n", display_str+1); return -1; } if (arg_debug) fprintf(stderr, "DISPLAY=%s parsed as %lu\n", display_str, display); return (int)display; } #ifdef HAVE_X11 // check for X11 abstract sockets static int x11_abstract_sockets_present(void) { EUID_ROOT(); // grsecurity fix FILE *fp = fopen("/proc/net/unix", "re"); if (!fp) errExit("fopen"); EUID_USER(); char *linebuf = 0; size_t bufsz = 0; int found = 0; errno = 0; for (;;) { if (getline(&linebuf, &bufsz, fp) == -1) { if (errno) errExit("getline"); break; } // The last space-separated field in 'linebuf' is the // pathname of the socket. Abstract sockets' pathnames // all begin with '@/', normal ones begin with '/'. char *p = strrchr(linebuf, ' '); if (!p) { fputs("error parsing /proc/net/unix\n", stderr); exit(1); } if (strncmp(p+1, "@/tmp/.X11-unix/", 16) == 0) { found = 1; break; } } free(linebuf); fclose(fp); return found; } // Choose a random, unallocated display number. This has an inherent // and unavoidable TOCTOU race, since we cannot create either the // socket or a lockfile ourselves. static int random_display_number(void) { int display; int found = 0; int i; struct sockaddr_un sa; // The -1 here is because we need space to inject a // leading nul byte. int sun_pathmax = (int)(sizeof sa.sun_path - 1); assert((size_t)sun_pathmax == sizeof sa.sun_path - 1); int sun_pathlen; int sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) errExit("socket"); for (i = 0; i < 100; i++) { display = rand() % (X11_DISPLAY_END - X11_DISPLAY_START) + X11_DISPLAY_START; // The display number might be claimed by a server listening // in _either_ the normal or the abstract namespace; they // don't necessarily do both. The easiest way to check is // to try to connect, both ways. memset(&sa, 0, sizeof sa); sa.sun_family = AF_UNIX; sun_pathlen = snprintf(sa.sun_path, sun_pathmax, "/tmp/.X11-unix/X%d", display); if (sun_pathlen >= sun_pathmax) { fprintf(stderr, "sun_path too small for display :%d" " (only %d bytes usable)\n", display, sun_pathmax); exit(1); } if (connect(sockfd, (struct sockaddr *)&sa, offsetof(struct sockaddr_un, sun_path) + sun_pathlen + 1) == 0) { close(sockfd); sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) errExit("socket"); continue; } if (errno != ECONNREFUSED && errno != ENOENT) errExit("connect"); // Name not claimed in the normal namespace; now try it // in the abstract namespace. Note that abstract-namespace // names are NOT nul-terminated; they extend to the length // specified as the third argument to 'connect'. memmove(sa.sun_path + 1, sa.sun_path, sun_pathlen + 1); sa.sun_path[0] = '\0'; if (connect(sockfd, (struct sockaddr *)&sa, offsetof(struct sockaddr_un, sun_path) + 1 + sun_pathlen) == 0) { close(sockfd); sockfd = socket(AF_UNIX, SOCK_STREAM, 0); if (sockfd == -1) errExit("socket"); continue; } if (errno != ECONNREFUSED && errno != ENOENT) errExit("connect"); // This display number is unclaimed. Of course, it could // be claimed before we get around to doing it... found = 1; break; } close(sockfd); if (!found) { fputs("Error: cannot find an unallocated X11 display number, " "exiting...\n", stderr); exit(1); } return display; } #endif #ifdef HAVE_X11 void x11_start_xvfb(int argc, char **argv) { EUID_ASSERT(); int i; pid_t jail = 0; pid_t server = 0; env_store_name_val("FIREJAIL_X11", "yes", SETENV); // never try to run X servers as root!!! if (getuid() == 0) { fprintf(stderr, "Error: X11 sandboxing is not available when running as root\n"); exit(1); } drop_privs(0); // check xvfb if (!program_in_path("Xvfb")) { fprintf(stderr, "\nError: Xvfb program was not found in /usr/bin directory, please install it:\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xvfb\n"); fprintf(stderr, " Arch: sudo pacman -S xorg-server-xvfb\n"); fprintf(stderr, " Fedora: sudo dnf install xorg-x11-server-Xvfb\n"); exit(0); } int display = random_display_number(); char *display_str; if (asprintf(&display_str, ":%d", display) == -1) errExit("asprintf"); assert(xvfb_screen); char *server_argv[256] = { // rest initialized to NULL "Xvfb", display_str, "-screen", "0", xvfb_screen }; unsigned pos = 0; while (server_argv[pos] != NULL) pos++; assert(xvfb_extra_params); // should be "" if empty // parse xvfb_extra_params // very basic quoting support char *temp = strdup(xvfb_extra_params); if (*xvfb_extra_params != '\0') { if (!temp) errExit("strdup"); bool dquote = false; bool squote = false; for (i = 0; i < (int) strlen(xvfb_extra_params); i++) { if (temp[i] == '\"') { dquote = !dquote; // replace closing quote by \0 if (dquote) temp[i] = '\0'; } if (temp[i] == '\'') { squote = !squote; // replace closing quote by \0 if (squote) temp[i] = '\0'; } if (!dquote && !squote && temp[i] == ' ') temp[i] = '\0'; if (dquote && squote) { fprintf(stderr, "Error: mixed quoting found while parsing xvfb_extra_params\n"); exit(1); } } if (dquote) { fprintf(stderr, "Error: unclosed quote found while parsing xvfb_extra_params\n"); exit(1); } server_argv[pos++] = temp; for (i = 0; i < (int) strlen(xvfb_extra_params)-1; i++) { if (pos >= (sizeof(server_argv)/sizeof(*server_argv)) - 2) { fprintf(stderr, "Error: arg count limit exceeded while parsing xvfb_extra_params\n"); exit(1); } if (temp[i] == '\0' && (temp[i+1] == '\"' || temp[i+1] == '\'')) server_argv[pos++] = temp + i + 2; else if (temp[i] == '\0' && temp[i+1] != '\0') server_argv[pos++] = temp + i + 1; } } server_argv[pos++] = NULL; assert(pos < (sizeof(server_argv)/sizeof(*server_argv))); // no overrun assert(server_argv[pos-1] == NULL); // last element is null if (arg_debug) { size_t i = 0; printf("\n*** Starting xvfb server:"); while (server_argv[i]!=NULL) { printf(" \"%s\"", server_argv[i]); i++; } printf(" ***\n\n"); } // remove --x11 arg char *jail_argv[argc+2]; int j = 0; for (i = 0; i < argc; i++) { if (strncmp(argv[i], "--x11", 5) == 0) continue; jail_argv[j] = argv[i]; j++; } jail_argv[j] = NULL; assert(j < argc+2); // no overrun if (arg_debug) { size_t i = 0; printf("\n*** Starting xvfb client:"); while (jail_argv[i]!=NULL) { printf(" \"%s\"", jail_argv[i]); i++; } printf(" ***\n\n"); } server = fork(); if (server < 0) errExit("fork"); if (server == 0) { if (arg_debug) printf("Starting xvfb...\n"); // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(server_argv[0], server_argv); perror("execvp"); _exit(1); } if (arg_debug) printf("xvfb server pid %d\n", server); // check X11 socket char *fname; if (asprintf(&fname, "/tmp/.X11-unix/X%d", display) == -1) errExit("asprintf"); int n = 0; // wait for x11 server to start while (++n < 10) { sleep(1); if (access(fname, F_OK) == 0) break; }; if (n == 10) { fprintf(stderr, "Error: failed to start xvfb\n"); exit(1); } free(fname); assert(display_str); env_store_name_val("DISPLAY", display_str, SETENV); // run attach command jail = fork(); if (jail < 0) errExit("fork"); if (jail == 0) { fmessage("\n*** Attaching to Xvfb display %d ***\n\n", display); // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(jail_argv[0], jail_argv); perror("execvp"); _exit(1); } // cleanup free(display_str); free(temp); // wait for either server or jail termination pid_t pid = wait(NULL); // see which process terminated and kill other if (pid == server) { kill(jail, SIGTERM); } else if (pid == jail) { kill(server, SIGTERM); } // without this closing Xephyr window may mess your terminal: // "monitoring" process will release terminal before // jail process ends and releases terminal wait(NULL); // fulneral exit(0); } static char *extract_setting(int argc, char **argv, const char *argument) { int i; int len = strlen(argument); for (i = 1; i < argc; i++) { if (strncmp(argv[i], argument, len) == 0) { return argv[i] + len; } // detect end of firejail params if (strcmp(argv[i], "--") == 0) break; if (strncmp(argv[i], "--", 2) != 0) break; } return NULL; } //$ Xephyr -ac -br -noreset -screen 800x600 :22 & //$ DISPLAY=:22 firejail --net=eth0 --blacklist=/tmp/.X11-unix/x0 firefox void x11_start_xephyr(int argc, char **argv) { EUID_ASSERT(); int i; pid_t jail = 0; pid_t server = 0; // default xephyr screen can be overwritten by a --xephyr-screen= command line option char *newscreen = extract_setting(argc, argv, "--xephyr-screen="); if (newscreen) xephyr_screen = newscreen; env_store_name_val("FIREJAIL_X11", "yes", SETENV); // unfortunately, xephyr does a number of weird things when started by root user!!! if (getuid() == 0) { fprintf(stderr, "Error: X11 sandboxing is not available when running as root\n"); exit(1); } drop_privs(0); // check xephyr if (!program_in_path("Xephyr")) { fprintf(stderr, "\nError: Xephyr program was not found in /usr/bin directory, please install it:\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xserver-xephyr\n"); fprintf(stderr, " Arch: sudo pacman -S xorg-server-xephyr\n"); fprintf(stderr, " Fedora: sudo dnf install xorg-x11-server-Xephyr\n"); exit(0); } int display = random_display_number(); char *display_str; if (asprintf(&display_str, ":%d", display) == -1) errExit("asprintf"); assert(xephyr_screen); char *server_argv[256] = { // rest initialized to NULL "Xephyr", "-ac", "-br", "-noreset", "-screen", xephyr_screen }; unsigned pos = 0; while (server_argv[pos] != NULL) pos++; if (checkcfg(CFG_XEPHYR_WINDOW_TITLE)) { server_argv[pos++] = "-title"; server_argv[pos++] = "firejail x11 sandbox"; } assert(xephyr_extra_params); // should be "" if empty // parse xephyr_extra_params // very basic quoting support char *temp = strdup(xephyr_extra_params); if (*xephyr_extra_params != '\0') { if (!temp) errExit("strdup"); bool dquote = false; bool squote = false; for (i = 0; i < (int) strlen(xephyr_extra_params); i++) { if (temp[i] == '\"') { dquote = !dquote; // replace closing quote by \0 if (dquote) temp[i] = '\0'; } if (temp[i] == '\'') { squote = !squote; // replace closing quote by \0 if (squote) temp[i] = '\0'; } if (!dquote && !squote && temp[i] == ' ') temp[i] = '\0'; if (dquote && squote) { fprintf(stderr, "Error: mixed quoting found while parsing xephyr_extra_params\n"); exit(1); } } if (dquote) { fprintf(stderr, "Error: unclosed quote found while parsing xephyr_extra_params\n"); exit(1); } server_argv[pos++] = temp; for (i = 0; i < (int) strlen(xephyr_extra_params)-1; i++) { if (pos >= (sizeof(server_argv)/sizeof(*server_argv)) - 2) { fprintf(stderr, "Error: arg count limit exceeded while parsing xephyr_extra_params\n"); exit(1); } if (temp[i] == '\0' && (temp[i+1] == '\"' || temp[i+1] == '\'')) { server_argv[pos++] = temp + i + 2; } else if (temp[i] == '\0' && temp[i+1] != '\0') { server_argv[pos++] = temp + i + 1; } } } server_argv[pos++] = display_str; server_argv[pos++] = NULL; // no overrun assert(pos < (sizeof(server_argv)/sizeof(*server_argv))); assert(server_argv[pos-1] == NULL); // last element is null { size_t i = 0; printf("\n*** Starting xephyr server:"); while (server_argv[i]!=NULL) { printf(" \"%s\"", server_argv[i]); i++; } printf(" ***\n\n"); } // remove --x11 arg char *jail_argv[argc+2]; int j = 0; for (i = 0; i < argc; i++) { if (strncmp(argv[i], "--x11", 5) == 0) continue; jail_argv[j] = argv[i]; j++; } jail_argv[j] = NULL; assert(j < argc+2); // no overrun if (arg_debug) { size_t i = 0; printf("*** Starting xephyr client:"); while (jail_argv[i]!=NULL) { printf(" \"%s\"", jail_argv[i]); i++; } printf(" ***\n\n"); } server = fork(); if (server < 0) errExit("fork"); if (server == 0) { if (arg_debug) printf("Starting xephyr...\n"); // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(server_argv[0], server_argv); perror("execvp"); _exit(1); } if (arg_debug) printf("xephyr server pid %d\n", server); // check X11 socket char *fname; if (asprintf(&fname, "/tmp/.X11-unix/X%d", display) == -1) errExit("asprintf"); int n = 0; // wait for x11 server to start while (++n < 10) { sleep(1); if (access(fname, F_OK) == 0) break; }; if (n == 10) { fprintf(stderr, "Error: failed to start xephyr\n"); exit(1); } free(fname); assert(display_str); env_store_name_val("DISPLAY", display_str, SETENV); // run attach command jail = fork(); if (jail < 0) errExit("fork"); if (jail == 0) { if (!arg_quiet) printf("\n*** Attaching to Xephyr display %d ***\n\n", display); // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(getenv("LD_PRELOAD") == NULL); assert(env_get("LD_PRELOAD") == NULL); execvp(jail_argv[0], jail_argv); perror("execvp"); _exit(1); } // cleanup free(display_str); free(temp); // wait for either server or jail termination pid_t pid = wait(NULL); // see which process terminated and kill other if (pid == server) { kill(jail, SIGTERM); } else if (pid == jail) { kill(server, SIGTERM); } // without this closing Xephyr window may mess your terminal: // "monitoring" process will release terminal before // jail process ends and releases terminal wait(NULL); // fulneral exit(0); } // this function returns the string that will appear in the window title when xpra is being used // this string may include one of these items: // * the "--name" argument, if specified // * the basename portion of the "--private" directory, if specified // note: the malloc() is leaking, but this is a small string allocated one time only during startup, so don't care static char * get_title_arg_str() { char * title_arg_str = NULL; const char * title_start = "--title=firejail x11 sandbox"; const char * title_sep = " "; // use the "--name" argument if it was explicitly specified if ((cfg.name != NULL) && (strlen(cfg.name) > 0)) { title_arg_str = malloc(strlen(title_start) + strlen(title_sep) + strlen(cfg.name) + 1); if (title_arg_str == NULL) { fprintf(stderr, "Error: malloc() failed to allocate memory\n"); exit(1); } strcpy(title_arg_str, title_start); strcat(title_arg_str, title_sep); strcat(title_arg_str, cfg.name); } // use the "--private" argument if it was explicitly specified else if ((cfg.home_private != NULL) && (strlen(cfg.home_private) > 0)) { const char * base_out = gnu_basename(cfg.home_private); title_arg_str = malloc(strlen(title_start) + strlen(title_sep) + strlen(base_out) + 1); if (title_arg_str == NULL) { fprintf(stderr, "Error: malloc() failed to allocate memory\n"); exit(1); } strcpy(title_arg_str, title_start); strcat(title_arg_str, title_sep); strcat(title_arg_str, base_out); } // default else { title_arg_str = malloc(strlen(title_start) + 1); if (title_arg_str == NULL) { fprintf(stderr, "Error: malloc() failed to allocate memory\n"); exit(1); } strcpy(title_arg_str, title_start); } return title_arg_str; } static void __attribute__((noreturn)) x11_start_xpra_old(int argc, char **argv, int display, char *display_str) { EUID_ASSERT(); int i; pid_t client = 0; pid_t server = 0; // build the start command char *server_argv[256] = { // rest initialized to NULL "xpra", "start", display_str, "--no-daemon", }; unsigned pos = 0; while (server_argv[pos] != NULL) pos++; assert(xpra_extra_params); // should be "" if empty // parse xephyr_extra_params // very basic quoting support char *temp = strdup(xpra_extra_params); if (*xpra_extra_params != '\0') { if (!temp) errExit("strdup"); bool dquote = false; bool squote = false; for (i = 0; i < (int) strlen(xpra_extra_params); i++) { if (temp[i] == '\"') { dquote = !dquote; // replace closing quote by \0 if (dquote) temp[i] = '\0'; } if (temp[i] == '\'') { squote = !squote; // replace closing quote by \0 if (squote) temp[i] = '\0'; } if (!dquote && !squote && temp[i] == ' ') temp[i] = '\0'; if (dquote && squote) { fprintf(stderr, "Error: mixed quoting found while parsing xpra_extra_params\n"); exit(1); } } if (dquote) { fprintf(stderr, "Error: unclosed quote found while parsing xpra_extra_params\n"); exit(1); } server_argv[pos++] = temp; for (i = 0; i < (int) strlen(xpra_extra_params)-1; i++) { if (pos >= (sizeof(server_argv)/sizeof(*server_argv)) - 2) { fprintf(stderr, "Error: arg count limit exceeded while parsing xpra_extra_params\n"); exit(1); } if (temp[i] == '\0' && (temp[i+1] == '\"' || temp[i+1] == '\'')) { server_argv[pos++] = temp + i + 2; } else if (temp[i] == '\0' && temp[i+1] != '\0') { server_argv[pos++] = temp + i + 1; } } } server_argv[pos++] = NULL; // no overrun assert(pos < (sizeof(server_argv)/sizeof(*server_argv))); assert(server_argv[pos-1] == NULL); // last element is null if (arg_debug) { size_t i = 0; printf("\n*** Starting xpra server: "); while (server_argv[i]!=NULL) { printf(" \"%s\"", server_argv[i]); i++; } printf(" ***\n\n"); } int fd_null = -1; if (arg_quiet) { fd_null = open("/dev/null", O_RDWR); if (fd_null == -1) errExit("open"); } // start server = fork(); if (server < 0) errExit("fork"); if (server == 0) { if (arg_debug) printf("Starting xpra...\n"); if (arg_quiet && fd_null != -1) { dup2(fd_null,0); dup2(fd_null,1); dup2(fd_null,2); } // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(getenv("LD_PRELOAD") == NULL); assert(env_get("LD_PRELOAD") == NULL); execvp(server_argv[0], server_argv); perror("execvp"); _exit(1); } // add a small delay, on some systems it takes some time for the server to start sleep(5); // check X11 socket char *fname; if (asprintf(&fname, "/tmp/.X11-unix/X%d", display) == -1) errExit("asprintf"); int n = 0; // wait for x11 server to start while (++n < 10) { sleep(1); if (access(fname, F_OK) == 0) break; } if (n == 10) { fprintf(stderr, "Error: failed to start xpra\n"); exit(1); } free(fname); // build attach command char * title_arg_str = get_title_arg_str(); char *attach_argv[] = { "xpra", title_arg_str, "attach", display_str, NULL }; // run attach command client = fork(); if (client < 0) errExit("fork"); if (client == 0) { if (arg_quiet && fd_null != -1) { dup2(fd_null,0); dup2(fd_null,1); dup2(fd_null,2); } fmessage("\n*** Attaching to xpra display %d ***\n\n", display); // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(attach_argv[0], attach_argv); perror("execvp"); _exit(1); } assert(display_str); env_store_name_val("DISPLAY", display_str, SETENV); // build jail command char *firejail_argv[argc+2]; pos = 0; for (i = 0; i < argc; i++) { if (strncmp(argv[i], "--x11", 5) == 0) continue; firejail_argv[pos] = argv[i]; pos++; } firejail_argv[pos] = NULL; assert((int) pos < (argc+2)); assert(!firejail_argv[pos]); // start jail pid_t jail = fork(); if (jail < 0) errExit("fork"); if (jail == 0) { // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); // restore original environment variables env_apply_all(); if (firejail_argv[0]) // shut up llvm scan-build execvp(firejail_argv[0], firejail_argv); perror("execvp"); exit(1); } fmessage("Xpra server pid %d, xpra client pid %d, jail %d\n", server, client, jail); sleep(1); // adding a delay in order to let the server start // wait for jail or server to end while (1) { pid_t pid = wait(NULL); if (pid == jail) { char *stop_argv[] = { "xpra", "stop", display_str, NULL }; pid_t stop = fork(); if (stop < 0) errExit("fork"); if (stop == 0) { if (arg_quiet && fd_null != -1) { dup2(fd_null,0); dup2(fd_null,1); dup2(fd_null,2); } // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(stop_argv[0], stop_argv); perror("execvp"); _exit(1); } // wait for xpra server to stop, 10 seconds limit while (++n < 10) { sleep(1); pid = waitpid(server, NULL, WNOHANG); if (pid == server) break; } if (arg_debug) { if (n == 10) printf("failed to stop xpra server gracefully\n"); else printf("xpra server successfully stopped in %d secs\n", n); } // kill xpra server and xpra client kill(client, SIGTERM); kill(server, SIGTERM); exit(0); } else if (pid == server) { // kill firejail process kill(jail, SIGTERM); // kill xpra client (should die with server, but...) kill(client, SIGTERM); exit(0); } } } static void __attribute__((noreturn)) x11_start_xpra_new(int argc, char **argv, char *display_str) { EUID_ASSERT(); int i; pid_t server = 0; // build the start command char *server_argv[256] = { // rest initialized to NULL "xpra", "start", display_str, "--daemon=no", "--attach=yes", "--exit-with-children=yes" }; unsigned spos = 0; unsigned fpos = 0; while (server_argv[spos] != NULL) spos++; // build jail command char *firejail_argv[argc+2]; size_t total_length = 0; for (i = 0; i < argc; i++) { if (strncmp(argv[i], "--x11", 5) == 0) continue; firejail_argv[fpos] = argv[i]; fpos++; total_length += strlen(argv[i]); } char *start_child_prefix = "--start-child="; char *start_child; start_child = malloc(total_length + strlen(start_child_prefix) + fpos + 2); if (start_child == NULL) { fprintf(stderr, "Error: unable to allocate start_child to assemble command\n"); exit(1); } strcpy(start_child,start_child_prefix); for(i = 0; (unsigned) i < fpos; i++) { strcat(start_child,firejail_argv[i]); if((unsigned) i != fpos - 1) strcat(start_child," "); } server_argv[spos++] = start_child; server_argv[spos++] = NULL; firejail_argv[fpos] = NULL; assert(xpra_extra_params); // should be "" if empty // parse xephyr_extra_params // very basic quoting support char *temp = strdup(xpra_extra_params); if (*xpra_extra_params != '\0') { if (!temp) errExit("strdup"); bool dquote = false; bool squote = false; for (i = 0; i < (int) strlen(xpra_extra_params); i++) { if (temp[i] == '\"') { dquote = !dquote; // replace closing quote by \0 if (dquote) temp[i] = '\0'; } if (temp[i] == '\'') { squote = !squote; // replace closing quote by \0 if (squote) temp[i] = '\0'; } if (!dquote && !squote && temp[i] == ' ') temp[i] = '\0'; if (dquote && squote) { fprintf(stderr, "Error: mixed quoting found while parsing xpra_extra_params\n"); exit(1); } } if (dquote) { fprintf(stderr, "Error: unclosed quote found while parsing xpra_extra_params\n"); exit(1); } server_argv[spos++] = temp; for (i = 0; i < (int) strlen(xpra_extra_params)-1; i++) { if (spos >= (sizeof(server_argv)/sizeof(*server_argv)) - 2) { fprintf(stderr, "Error: arg count limit exceeded while parsing xpra_extra_params\n"); exit(1); } if (temp[i] == '\0' && (temp[i+1] == '\"' || temp[i+1] == '\'')) { server_argv[spos++] = temp + i + 2; } else if (temp[i] == '\0' && temp[i+1] != '\0') { server_argv[spos++] = temp + i + 1; } } } server_argv[spos++] = NULL; assert((int) fpos < (argc+2)); assert(!firejail_argv[fpos]); // no overrun assert(spos < (sizeof(server_argv)/sizeof(*server_argv))); assert(server_argv[spos-1] == NULL); // last element is null if (arg_debug) { size_t i = 0; printf("\n*** Starting xpra server: "); while (server_argv[i]!=NULL) { printf(" \"%s\"", server_argv[i]); i++; } printf(" ***\n\n"); } int fd_null = -1; if (arg_quiet) { fd_null = open("/dev/null", O_RDWR); if (fd_null == -1) errExit("open"); } // start server = fork(); if (server < 0) errExit("fork"); if (server == 0) { if (arg_debug) printf("Starting xpra...\n"); if (arg_quiet && fd_null != -1) { dup2(fd_null,0); dup2(fd_null,1); dup2(fd_null,2); } // restore original environment variables env_apply_all(); // running without privileges - see drop_privs call above assert(env_get("LD_PRELOAD") == NULL); assert(getenv("LD_PRELOAD") == NULL); execvp(server_argv[0], server_argv); perror("execvp"); _exit(1); } // wait for server to end while (1) { pid_t pid = wait(NULL); if (pid == server) { free(start_child); exit(0); } } } void x11_start_xpra(int argc, char **argv) { EUID_ASSERT(); env_store_name_val("FIREJAIL_X11", "yes", SETENV); // unfortunately, xpra does a number of weird things when started by root user!!! if (getuid() == 0) { fprintf(stderr, "Error: X11 sandboxing is not available when running as root\n"); exit(1); } drop_privs(0); // check xpra if (!program_in_path("xpra")) { fprintf(stderr, "\nError: Xpra program was not found in /usr/bin directory, please install it:\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xpra\n"); fprintf(stderr, " Arch: sudo pacman -S xpra\n"); fprintf(stderr, " Fedora: sudo dnf install xpra\n"); exit(0); } int display = random_display_number(); char *display_str; if (asprintf(&display_str, ":%d", display) == -1) errExit("asprintf"); if (checkcfg(CFG_XPRA_ATTACH)) x11_start_xpra_new(argc, argv, display_str); else x11_start_xpra_old(argc, argv, display, display_str); } void x11_start(int argc, char **argv) { EUID_ASSERT(); // unfortunately, xpra does a number of weird things when started by root user!!! if (getuid() == 0) { fprintf(stderr, "Error: X11 sandboxing is not available when running as root\n"); exit(1); } // check xpra if (program_in_path("xpra")) x11_start_xpra(argc, argv); else if (program_in_path("Xephyr")) x11_start_xephyr(argc, argv); else { fprintf(stderr, "\nError: Xpra or Xephyr not found in /usr/bin directory, please install one of them:\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xpra\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xserver-xephyr\n"); fprintf(stderr, " Arch: sudo pacman -S xpra\n"); fprintf(stderr, " Arch: sudo pacman -S xorg-server-xephyr\n"); fprintf(stderr, " Fedora: sudo dnf install xpra\n"); fprintf(stderr, " Fedora: sudo dnf install xorg-x11-server-Xephyr\n"); exit(0); } } #endif void x11_xorg(void) { #ifdef HAVE_X11 // get DISPLAY env const char *display = env_get("DISPLAY"); if (!display) { fputs("Error: --x11=xorg requires an 'outer' X11 server to use.\n", stderr); exit(1); } // check xauth utility is present in the system struct stat s; if (stat("/usr/bin/xauth", &s) == -1) { fprintf(stderr, "Error: xauth utility not found in /usr/bin. Please install it:\n"); fprintf(stderr, " Debian/Ubuntu/Mint: sudo apt-get install xauth\n"); fprintf(stderr, " Arch: sudo pacman -S xorg-xauth\n"); fprintf(stderr, " Fedora: sudo dnf install xorg-x11-xauth\n"); exit(1); } if ((s.st_uid != 0 && s.st_gid != 0) || (s.st_mode & S_IWOTH)) { fprintf(stderr, "Error: invalid /usr/bin/xauth executable\n"); exit(1); } if (s.st_size > 1024 * 1024) { fprintf(stderr, "Error: /usr/bin/xauth executable is too large\n"); exit(1); } // copy /usr/bin/xauth in the sandbox and set mode to 0711 // users are not able to trace the running xauth this way if (arg_debug) printf("Copying /usr/bin/xauth to %s\n", RUN_XAUTH_FILE); if (copy_file("/usr/bin/xauth", RUN_XAUTH_FILE, 0, 0, 0711)) { fprintf(stderr, "Error: cannot copy /usr/bin/xauth executable\n"); exit(1); } fmessage("Generating a new .Xauthority file\n"); mkdir_attr(RUN_XAUTHORITY_SEC_DIR, 0700, getuid(), getgid()); // create new Xauthority file in RUN_XAUTHORITY_SEC_DIR char tmpfname[] = RUN_XAUTHORITY_SEC_DIR "/.Xauth-XXXXXX"; int fd = mkstemp(tmpfname); if (fd == -1) { fprintf(stderr, "Error: cannot create .Xauthority file\n"); exit(1); } if (fchown(fd, getuid(), getgid()) == -1) errExit("chown"); close(fd); // run xauth if (arg_debug) sbox_run(SBOX_USER | SBOX_CAPS_NONE | SBOX_SECCOMP, 8, RUN_XAUTH_FILE, "-v", "-f", tmpfname, "generate", display, "MIT-MAGIC-COOKIE-1", "untrusted"); else sbox_run(SBOX_USER | SBOX_CAPS_NONE | SBOX_SECCOMP, 7, RUN_XAUTH_FILE, "-f", tmpfname, "generate", display, "MIT-MAGIC-COOKIE-1", "untrusted"); // remove xauth copy unlink(RUN_XAUTH_FILE); // ensure there is already a file ~/.Xauthority, so that bind-mount below will work. char *dest; if (asprintf(&dest, "%s/.Xauthority", cfg.homedir) == -1) errExit("asprintf"); if (access(dest, F_OK) == -1) { touch_file_as_user(dest, 0600); if (access(dest, F_OK) == -1) { fprintf(stderr, "Error: cannot create %s\n", dest); exit(1); } } // get a file descriptor for ~/.Xauthority int dst = safer_openat(-1, dest, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (dst == -1) errExit("safer_openat"); // check if the actual mount destination is a user owned regular file if (fstat(dst, &s) == -1) errExit("fstat"); if (!S_ISREG(s.st_mode) || s.st_uid != getuid()) { if (S_ISLNK(s.st_mode)) fprintf(stderr, "Error: .Xauthority is a symbolic link\n"); else fprintf(stderr, "Error: .Xauthority is not a user owned regular file\n"); exit(1); } // preserve a read-only mount struct statvfs vfs; if (fstatvfs(dst, &vfs) == -1) errExit("fstatvfs"); if ((vfs.f_flag & MS_RDONLY) == MS_RDONLY) fs_remount(RUN_XAUTHORITY_SEC_DIR, MOUNT_READONLY, 0); // always mounting the new Xauthority file noexec,nodev,nosuid fs_remount(RUN_XAUTHORITY_SEC_DIR, MOUNT_NOEXEC, 0); // get a file descriptor for the new Xauthority file int src = safer_openat(-1, tmpfname, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (src == -1) errExit("safer_openat"); if (fstat(src, &s) == -1) errExit("fstat"); if (!S_ISREG(s.st_mode)) { errno = EPERM; errExit("mounting Xauthority file"); } // mount via the link in /proc/self/fd if (arg_debug) printf("Mounting %s on %s\n", tmpfname, dest); if (bind_mount_by_fd(src, dst)) { fprintf(stderr, "Error: cannot mount the new .Xauthority file\n"); exit(1); } // check /proc/self/mountinfo to confirm the mount is ok MountData *mptr = get_last_mount(); if (strcmp(mptr->dir, dest) != 0 || strcmp(mptr->fstype, "tmpfs") != 0) errLogExit("invalid .Xauthority mount"); close(src); close(dst); ASSERT_PERMS(dest, getuid(), getgid(), 0600); // blacklist user .Xauthority file if it is not masked already const char *envar = env_get("XAUTHORITY"); if (envar) { char *rp = realpath_as_user(envar); if (rp) { if (strcmp(rp, dest) != 0) disable_file_or_dir(rp); free(rp); } } // set environment variable env_store_name_val("XAUTHORITY", dest, SETENV); free(dest); // mask RUN_XAUTHORITY_SEC_DIR if (mount("tmpfs", RUN_XAUTHORITY_SEC_DIR, "tmpfs", MS_NOSUID | MS_NODEV | MS_STRICTATIME, "mode=755,gid=0") < 0) errExit("mounting tmpfs"); fs_logger2("tmpfs", RUN_XAUTHORITY_SEC_DIR); #endif } void fs_x11(void) { #ifdef HAVE_X11 int display = x11_display(); if (display <= 0) return; struct stat s1, s2; if (stat("/tmp", &s1) != 0 || lstat("/tmp/.X11-unix", &s2) != 0) return; if ((s1.st_mode & S_ISVTX) != S_ISVTX) { fwarning("cannot mask X11 sockets: sticky bit not set on /tmp directory\n"); return; } if (s2.st_uid != 0) { fwarning("cannot mask X11 sockets: /tmp/.X11-unix not owned by root user\n"); return; } // the mount source is under control of the user, so be careful and // mount without following symbolic links, using a file descriptor char *x11file; if (asprintf(&x11file, "/tmp/.X11-unix/X%d", display) == -1) errExit("asprintf"); int src = open(x11file, O_PATH|O_NOFOLLOW|O_CLOEXEC); if (src < 0) { free(x11file); return; } struct stat s3; if (fstat(src, &s3) < 0) errExit("fstat"); if (!S_ISSOCK(s3.st_mode)) { close(src); free(x11file); return; } if (arg_debug || arg_debug_whitelists) fprintf(stderr, "Masking all X11 sockets except %s\n", x11file); // This directory must be mode 1777 if (mount("tmpfs", "/tmp/.X11-unix", "tmpfs", MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_STRICTATIME, "mode=1777,uid=0,gid=0") < 0) errExit("mounting tmpfs on /tmp/.X11-unix"); fs_logger("tmpfs /tmp/.X11-unix"); // create an empty root-owned file which will have the desired socket bind-mounted over it int dst = open(x11file, O_RDONLY|O_CREAT|O_EXCL|O_CLOEXEC, S_IRUSR | S_IWUSR); if (dst < 0) errExit("open"); if (bind_mount_by_fd(src, dst)) errExit("mount bind"); close(src); close(dst); fs_logger2("whitelist", x11file); free(x11file); #endif } void x11_block(void) { #ifdef HAVE_X11 // check abstract socket presence and network namespace options if ((!arg_nonetwork && !arg_netns && !cfg.bridge0.configured && !cfg.interface0.configured) && x11_abstract_sockets_present()) { fprintf(stderr, "ERROR: --x11=none specified, but abstract X11 socket still accessible.\n" "Additional setup required. To block abstract X11 socket you can either:\n" " * use network namespace in firejail (--net=none, --net=...)\n" " * add \"-nolisten local\" to xserver options\n" " (eg. to your display manager config, or /etc/X11/xinit/xserverrc)\n"); exit(1); } // blacklist sockets char *cmd = strdup("blacklist /tmp/.X11-unix"); if (!cmd) errExit("strdup"); profile_check_line(cmd, 0, NULL); profile_add(cmd); // blacklist .Xauthority cmd = strdup("blacklist ${HOME}/.Xauthority"); if (!cmd) errExit("strdup"); profile_check_line(cmd, 0, NULL); profile_add(cmd); const char *xauthority = env_get("XAUTHORITY"); if (xauthority) { char *line; if (asprintf(&line, "blacklist %s", xauthority) == -1) errExit("asprintf"); profile_check_line(line, 0, NULL); profile_add(line); } // clear environment env_store("DISPLAY", RMENV); env_store("XAUTHORITY", RMENV); #endif }