Last active
March 10, 2021 22:25
-
-
Save RogerGee/2eb78e1139aba58c7cbf to your computer and use it in GitHub Desktop.
Runs a command in a restricted context (e.g. no network access, limited memory, chroot)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* restrict-command.c - must run as setuid program */ | |
#define _GNU_SOURCE | |
#include <stdlib.h> | |
#include <stdbool.h> | |
#include <stdio.h> | |
#include <string.h> | |
#include <errno.h> | |
#include <error.h> | |
#include <getopt.h> | |
#include <sys/wait.h> | |
#include <sys/resource.h> | |
#include <sys/stat.h> | |
#include <sched.h> | |
#include <unistd.h> | |
#include <fcntl.h> | |
/* options */ | |
static struct restrict_options | |
{ | |
bool denyNetwork; | |
struct rlimit limitMemory; | |
struct rlimit limitCPU; | |
struct rlimit limitFileCreate; | |
struct rlimit limitProcesses; | |
char chroot[4096]; | |
} globalOptions; | |
static const char* OPTSTRING = ":hnm:c:f:p:r:"; | |
static struct option LONG_OPTIONS[] = { | |
{ "help", no_argument, NULL, 0 }, | |
{ "network", no_argument, NULL, 1 }, | |
{ "memory", required_argument, NULL, 2 }, | |
{ "cpu", required_argument, NULL, 3 }, | |
{ "filesize", required_argument, NULL, 4 }, | |
{ "processes", required_argument, NULL, 5 }, | |
{ "chroot", required_argument, NULL, 6 }, | |
{ 0, 0, NULL, 0 } | |
}; | |
static void restrict_options_init(struct restrict_options* opts); | |
static void usage(const char* self); | |
static int exec_command(const char* program,char** argv); | |
static void apply_restrictions(); | |
int main(int argc,char* argv[]) | |
{ | |
int o; | |
int ret; | |
const char* progName; | |
struct stat st; | |
progName = argv[0]; | |
if (argc <= 1) { | |
usage(progName); | |
exit(EXIT_SUCCESS); | |
} | |
restrict_options_init(&globalOptions); | |
while (true) { | |
ret = getopt_long(argc,argv,OPTSTRING,LONG_OPTIONS,NULL); | |
if (ret == -1) | |
break; | |
o = optarg ? atoi(optarg) : -1; | |
if (optarg && (o <= 0 && ret != 6 && ret != 'r')) { | |
fprintf(stderr,"%s: invalid argument '%s': argument is out of range\n",progName,optarg); | |
exit(EXIT_FAILURE); | |
} | |
switch (ret) { | |
case 0: | |
case 'h': | |
usage(progName); | |
exit(EXIT_SUCCESS); | |
case 1: | |
case 'n': | |
globalOptions.denyNetwork = true; | |
break; | |
case 2: | |
case 'm': | |
globalOptions.limitMemory.rlim_cur = o; | |
globalOptions.limitMemory.rlim_max = o; | |
break; | |
case 3: | |
case 'c': | |
globalOptions.limitCPU.rlim_cur = o; | |
globalOptions.limitCPU.rlim_max = o; | |
break; | |
case 4: | |
case 'f': | |
globalOptions.limitFileCreate.rlim_cur = o; | |
globalOptions.limitFileCreate.rlim_max = o; | |
break; | |
case 5: | |
case 'p': | |
globalOptions.limitProcesses.rlim_cur = o; | |
globalOptions.limitProcesses.rlim_max = o; | |
break; | |
case 6: | |
case 'r': | |
/* copy file path to buffer; verify that the directory | |
exists and can be accessed (searched) by others (at | |
least) */ | |
strcpy(globalOptions.chroot,optarg); | |
/* stat file: if fails then probably doesn't exist */ | |
if (stat(globalOptions.chroot,&st) == -1) { | |
fprintf(stderr,"%s: error: specified chroot is invalid: %s\n",progName,strerror(errno)); | |
exit(EXIT_FAILURE); | |
} | |
/* verify is directory */ | |
if (!S_ISDIR(st.st_mode)) { | |
fprintf(stderr,"%s: error: specified chroot is not a directory\n",progName); | |
exit(EXIT_FAILURE); | |
} | |
/* make sure search access is granted to others */ | |
if ((st.st_mode & S_IXOTH) == 0) { | |
fprintf(stderr,"%s: error: specified chroot is not searchable by others\n",progName); | |
exit(EXIT_FAILURE); | |
} | |
break; | |
case ':': | |
fprintf(stderr,"%s: error: missing option argument\n",progName); | |
exit(EXIT_FAILURE); | |
default: | |
fprintf(stderr,"%s: error: unrecognized command-line option\n",progName); | |
exit(EXIT_FAILURE); | |
} | |
} | |
if (optind >= argc) { | |
fprintf(stderr,"%s: error: expected command argument\n",progName); | |
exit(EXIT_FAILURE); | |
} | |
return exec_command(argv[optind],argv+optind); | |
} | |
void restrict_options_init(struct restrict_options* opts) | |
{ | |
opts->denyNetwork = false; | |
memset(&opts->limitMemory,0,sizeof(struct rlimit)); | |
memset(&opts->limitCPU,0,sizeof(struct rlimit)); | |
memset(&opts->limitFileCreate,0,sizeof(struct rlimit)); | |
memset(&opts->limitProcesses,0,sizeof(struct rlimit)); | |
memset(&opts->chroot,0,sizeof(opts->chroot)); | |
} | |
void usage(const char* self) | |
{ | |
printf("usage: %s [options] command [arguments]\n",self); | |
printf( | |
"\nOptions\n" | |
"%-18s%40s\n" | |
"%-18s%40s\n" | |
"%-18s%40s\n" | |
"%-18s%40s\n" | |
"%-18s%40s\n" | |
"%-18s%40s\n", | |
" -n, --network", | |
"deny network access", | |
" -m, --memory N", | |
"limit memory usage to N bytes", | |
" -c, --cpu N", | |
"limit CPU time to N seconds", | |
" -f, --filesize N", | |
"limit file create size to N bytes", | |
" -p, --processes N", | |
"limit number of child processes to N", | |
" -r, --chroot PATH", | |
"chroot to PATH" ); | |
} | |
int exec_command(const char* program,char** argv) | |
{ | |
/* apply restrictions in the calling process */ | |
apply_restrictions(); | |
/* execute program to replace this process */ | |
execvp(program,argv); | |
/* control is here if an error occurred */ | |
error(0,0,"error executing '%s': %s",program,strerror(errno)); | |
return EXIT_FAILURE; | |
} | |
void apply_restrictions() | |
{ | |
int ret = -1; | |
uid_t uid; | |
if (globalOptions.denyNetwork) { | |
/* create a new network namespace to disallow network | |
connections */ | |
ret = unshare(CLONE_NEWNET); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail unshare()"); | |
} | |
if (globalOptions.limitMemory.rlim_max) { | |
/* limit the process's virtual address space; this causes | |
memory allocations to fail for a variety of different | |
mechanisms */ | |
ret = setrlimit(RLIMIT_AS,&globalOptions.limitMemory); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail setrlimit()"); | |
} | |
if (globalOptions.limitCPU.rlim_max) { | |
/* limit the process's total CPU time */ | |
ret = setrlimit(RLIMIT_CPU,&globalOptions.limitCPU); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail setrlimit()"); | |
} | |
if (globalOptions.limitFileCreate.rlim_max) { | |
/* limit the maximum file size */ | |
ret = setrlimit(RLIMIT_FSIZE,&globalOptions.limitFileCreate); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail setrlimit()"); | |
} | |
if (globalOptions.limitProcesses.rlim_max) { | |
/* limit the maximum file size */ | |
ret = setrlimit(RLIMIT_NPROC,&globalOptions.limitProcesses); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail setrlimit()"); | |
} | |
if (globalOptions.chroot[0] != 0) { | |
ret = chroot(globalOptions.chroot); | |
if (ret == -1) | |
error(EXIT_FAILURE,errno,"fail chroot()"); | |
} | |
if (ret == -1) | |
error(EXIT_FAILURE,0,"error: no restrictions were specified"); | |
/* lower our effective uid back to the real uid of this process; | |
this causes us to lose our privileged capabilities */ | |
uid = getuid(); | |
seteuid(uid); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment