/* SuSE secure compartment v1.1 (c) 2000 by Marc Heuse <marc@suse.de>
 *
 * Description:
 * Compartment provides all possibilities to securely run insecure and/or
 * untrusted programs. It provides you with all necessary options to fine
 * grain the tightening process as you need it.
 *
 * Please note that you need a Linux kernel 2.2.12 or later!
 *
 * Official releases and updates can be found on the SuSE Linux distribution
 * from version 7.0 on and ftp.suse.com plus mirror sites.
 *
 * Unofficial updates and betas can be found at http://www.suse.de/~marc
 *
 * Please contact me directly at marc@suse.de for bugs, comments or ideas.
 *
 * Thanks go to Solar Designer for comments & critics ...
 *
 */

#include <stdio.h>
#include <unistd.h>
#include <grp.h>
#include <pwd.h>
#include <grp.h>
#define _LINUX_STRING_H_
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <linux/capability.h>
#include <stdarg.h>
#include <syslog.h>
#include <sys/time.h>
#include <sys/resource.h>

#define PROGRAM_NAME	"SuSE secure compartment"
#define VERSION		"v1.1"
#define AUTHOR		"Marc Heuse <marc@suse.de>"
#define POINTER		"http://www.suse.de/~marc"
#define LOGBUF		5000

char *_env[] = { "HOME=/", "COMPARTMENT=YES", "PATH=/bin:/usr/bin:/", "" };

int cap_set_no[29] = {
  CAP_CHOWN,CAP_DAC_OVERRIDE,CAP_DAC_READ_SEARCH,CAP_FOWNER,CAP_FSETID,
  CAP_FS_MASK,CAP_KILL,CAP_SETGID,CAP_SETUID,CAP_SETPCAP,CAP_LINUX_IMMUTABLE,
  CAP_NET_BIND_SERVICE,CAP_NET_BROADCAST,CAP_NET_ADMIN,CAP_NET_RAW,CAP_IPC_LOCK,
  CAP_IPC_OWNER,CAP_SYS_MODULE,CAP_SYS_RAWIO,CAP_SYS_CHROOT,CAP_SYS_PTRACE,
  CAP_SYS_PACCT,CAP_SYS_ADMIN,CAP_SYS_BOOT,CAP_SYS_NICE,CAP_SYS_RESOURCE,
  CAP_SYS_TIME,CAP_SYS_TTY_CONFIG, 0 };
char cap_set_names[29][29] = {
  "CAP_CHOWN","CAP_DAC_OVERRIDE","CAP_DAC_READ_SEARCH","CAP_FOWNER","CAP_FSETID",
  "CAP_FS_MASK","CAP_KILL","CAP_SETGID","CAP_SETUID","CAP_SETPCAP",
  "CAP_LINUX_IMMUTABLE","CAP_NET_BIND_SERVICE","CAP_NET_BROADCAST",
  "CAP_NET_ADMIN","CAP_NET_RAW","CAP_IPC_LOCK","CAP_IPC_OWNER","CAP_SYS_MODULE",
  "CAP_SYS_RAWIO","CAP_SYS_CHROOT","CAP_SYS_PTRACE","CAP_SYS_PACCT",
  "CAP_SYS_ADMIN","CAP_SYS_BOOT","CAP_SYS_NICE","CAP_SYS_RESOURCE","CAP_SYS_TIME",
  "CAP_SYS_TTY_CONFIG", "" };

extern int capset(cap_user_header_t header, cap_user_data_t data);
extern char **environ;
int program_params, fd;
int do_chroot, do_group, do_user, do_caps, do_init, verbose, quiet;
char *chroot_path, *init_program, *prg, *user, **_argv;
long tmp;
__u32 temp, caps = 0;
uid_t set_user;
gid_t set_group;
struct passwd *pw;
struct group *gr;

void help() {
    fprintf(stderr, "%s %s %s %s\n\n", PROGRAM_NAME, VERSION, AUTHOR, POINTER);
    fprintf(stderr, "Syntax: %s [options] /full/path/to/program\n", prg);
    fprintf(stderr, "Options:
\t --chroot path\t chroot to path
\t --user user\t change uid to this user
\t --group group\t change gid to this group
\t --init program\t execute this program/script before doing anything
\t --cap capset\t set capset name. This option can be used several times.
\t --verbose\t be verbose
\t --quiet\t do no logging (to syslog)
\t --fork\t\t fork (if everything is fine)
\nHints: always try to chroot; use --user&group if possible; chroot and chown all
files to another user than root if you use capabilties. Read the README file!
\nKnown capset names: none");
    tmp = 0;
    while(strlen(cap_set_names[tmp])>0)
        fprintf(stderr," %s",cap_set_names[tmp++]);
    fprintf(stderr, " (see linux/capability.h for more information)\n");
    exit(-1);
}

void print_msg(const char *format, ...) {
    va_list arg;
    char *log;
    if (quiet == 0) {
        log = malloc(LOGBUF);
        va_start(arg, format);
        vfprintf(stderr, format, arg);
        vsnprintf(log, LOGBUF, format, arg);
        syslog(LOG_NOTICE, log);
        va_end(arg);
        free(log);
    }
}

void my_secure() {
    char file[10] = "/dev/null";
    int mode = O_RDWR;

    alarm(0);
    if (verbose)
        print_msg("Turned possible alarm(2)s off\n");

    environ = _env;             // set a safe and clean environment
    if (verbose)
        print_msg("Safe environment set\n");

    if ((fd = open(file, mode)) < 0) {
	file[1] = '\0';
	mode = O_RDONLY;
	if ((fd = open(file, mode)) < 0) {
	    print_msg("Can not open /dev/null nor /, no more fd's?\n");
	    exit(-1);
	}
    }
    while (fd < 2)
        fd = open(file, mode);
    close(fd);
    if (verbose)
        print_msg("Ensured that fd 0-2 are open\n");

    /* FIXME: should limits be reset or signals be blocked or ignored? */

    return;
}

void my_caps() {
    cap_user_header_t head = (cap_user_header_t) calloc(1, sizeof(cap_user_header_t));
    cap_user_data_t cap = (cap_user_data_t)   calloc(1, sizeof(cap_user_data_t));
    head->version = _LINUX_CAPABILITY_VERSION;
    head->pid = 0;
    cap->inheritable = cap->permitted = cap->effective = caps;
//    if (capget(head, cap) != 0) {
//	print_msg("Could not get capabilities.\n");
//	exit(-1);
//    }
    if (capset(head, cap) != 0) {
        print_msg("Could not set capabilities.\n");
        exit(-1);
    } else {
        if (verbose)
            print_msg("Capabilities successfully set to 0x%0x\n",cap->effective);
    }
    free(head);
    free(cap);
    return;
}
 
int main (int argc, char *argv[]) {
    struct stat st;
    int do_fork = 0;
    struct rlimit limit = {0, 0};
    unsigned long int uidrange = 65535;
/* process the parameters */
    prg = argv[0];
    if (argc < 2)
        help();
    if (strcmp(argv[1],"-h") == 0 || strcmp(argv[1],"--help") == 0)
        help();

    my_secure();
    openlog("SuSEcompartment", LOG_PID, LOG_DAEMON);
    if (sizeof(uid_t) == 4) {
        (unsigned long int) uidrange = 65535;
    } else { if (sizeof(uid_t) == 8) {
        (unsigned long int) uidrange = (unsigned long int) 2147483646; //4294967295;
     } else
        fprintf(stderr, "Warning: weird uid size: %d\n", sizeof(uid_t));
    }

    program_params = 1;
    while((argc - 1) > program_params && strncmp(argv[program_params], "--", 2) == 0) {
        if (strcmp(argv[program_params], "--chroot") == 0) {
            chroot_path = argv[++program_params];
            do_chroot = 1;
        } else {
         if (strcmp(argv[program_params], "--user") == 0) {
            if ((pw = (struct passwd *) getpwnam(argv[++program_params])) != NULL) {
                user = argv[program_params];
                set_user = pw->pw_uid;
            } else {
                tmp = strtol(argv[program_params], (char **)NULL, 10);
                if (tmp < 0 || tmp > uidrange) {
                    print_msg("--user is out of bounds (username or a value between 0 and %lu): %ld\n", uidrange, tmp);
                    exit(-1);
                }
                set_user = (uid_t) tmp;
            }
            if (verbose)
                print_msg("UID will be set to %d\n", set_user);
            do_user = 1;
         } else {
          if (strcmp(argv[program_params], "--group") == 0) {
            if ((gr = (struct group *) getgrnam(argv[++program_params])) != NULL) {
                set_group = gr->gr_gid;
            } else {
                tmp = strtol(argv[program_params], (char **)NULL, 10);
                if (tmp < 0 || tmp > uidrange) {
                    print_msg("--group is out of bounds (groupname or a value between 0 and %lu): %ld\n", uidrange, tmp);
                    exit(-1);
                }
                set_group = (gid_t) tmp;
            }
            if (verbose)
                print_msg("GID will be set to %d\n", set_group);
            do_group = 1;
          } else {
           if (strcmp(argv[program_params], "--init") == 0) {
            init_program = argv[++program_params];
            if (verbose)
                print_msg("Initial program will be %s\n", init_program);
            do_init = 1;
           } else {
            if (strcmp(argv[program_params], "--fork") == 0) {
             if (verbose)
                 print_msg("Will fork if everything is fine\n");
             do_fork = 1;
            } else {
             if (strcmp(argv[program_params], "--cap") == 0) {
	      program_params++;
	      tmp = 0;
	      temp = caps;
	    
	      if (strcmp(argv[program_params], "none") == 0 ||
	          strcmp(argv[program_params], "null") == 0) {
	          temp = caps = 0;
	          if (verbose)
	              print_msg("Capabilities all cleared\n");
	     } else {
	      while((temp == caps) && (strlen(cap_set_names[tmp]) > 0)) {
		    if (strcmp(argv[program_params], cap_set_names[tmp]) == 0) {
		        temp = cap_set_no[tmp];
		        caps |= (1<<((temp)&31));
		        if (verbose)
		            print_msg("Capabilities will be set to 0x%0x\n",caps);
	            }
	            tmp++;
	      }
	      if (caps == temp) {
		    print_msg("Unknown capset name: %s\n", argv[program_params]);
		    exit(-1);
	      }
	     }
             do_caps = 1;
        } else {
         if (strcmp(argv[program_params], "--verbose") == 0) {
            print_msg("I am in verbose mode now\n");
            verbose = 1;
         } else {
          if (strcmp(argv[program_params], "--quiet") == 0) {
              quiet = 1;
          } else {
           if (strcmp(argv[program_params], "--help") == 0) {
               help();
           } else {
               print_msg("Unknown parameter: %s\n\n",argv[program_params]);
               help();
           }
        }}}}}}}}
        program_params++;
    }

    if ((getuid() != geteuid()) || (getgid() != getegid()))
        print_msg("Warning: euid/egid and uid/gid are different, are we running suid/sgid??\nDo not do that!\n");

/* the first parameter without an -- has to be the program plus options to execute */
    if (program_params == argc) {
        print_msg("No program given to execute.\n\n");
        help();    
    }
    _argv = argv;
    _argv += program_params;

/* some further insightful deductions */
    if (do_user == 0)
        set_user = geteuid();
    if (do_group == 0)
        set_group = getegid();
    if (user == NULL) {
        if ((pw = (struct passwd *) getpwuid(set_user)) == NULL) {
            print_msg("Warning: username for uid %u is not found in /etc/passwd.\n", set_user);
        } else {
            user = (char *) pw->pw_name;
        }
    }

    if (do_caps && do_user) {
	print_msg("Warning: --user and --caps will not work together!\n");
    }

    if (do_init) {
        if (system(init_program)) {
            print_msg("Error executing init program %s\n", init_program);
            exit(1);
        } else {
            if (verbose)
                print_msg("Init program %s run successfully\n", init_program);
        }
    }

    if (do_chroot) {
        if (chroot(chroot_path)) {
            print_msg("Error chrooting to %s\n", chroot_path); 
            exit(1);
        }
        if (chdir("/") != 0) {
            print_msg("chdir to / in chroot failed\n");
            exit(1);
        }
        if (verbose)
            print_msg("Chrooted sucessfully to %s\n", chroot_path);
    }

    if (do_user || do_group || do_caps) {
//        if (user != NULL) {
            if (setgroups(0, NULL)) {
                print_msg("Error removing supplementary groups via setgroups().\n");
                exit(1);
            } else {
                if (verbose)
                    print_msg("setgroups() successfully set\n");
            }
//        } else {
//            print_msg("Can not do setgroups() because username for uid %u is not found in /etc/passwd.\n", set_user);
//        }
    }

    if (do_group) {
        if (setgid(set_group)) {
            print_msg("Error setting gid to %u\n", set_group);
            exit(1);
        } else {
            if (verbose)
                print_msg("GID successfully set to %d\n",set_group);
        }
    }

    if (do_caps)
        my_caps();

    if (do_user) {
        if (setuid(set_user)) {
            print_msg("Error setting uid to %u\n", set_user);
            exit(1);
        } else {
            if (verbose)
                print_msg("UID successfully set to %d\n", set_user);
          }
    }

    for (fd = 3; fd <= 1023; fd++) // set close_on_exec on all open fd´s > 2
        (void) fcntl(fd, F_SETFD, FD_CLOEXEC);
    if (verbose)
        print_msg("FD_CLOEXEC successfully set on all filedescriptors > 2\n");

    if (setrlimit(RLIMIT_CORE, &limit) < 0)
        print_msg("Could not set core size limit to 0 bytes\n");
    else if (verbose)
        print_msg("core size limit successfully set to 0 bytes\n");

    if (quiet == 0) {
        char *buf = malloc(LOGBUF);
        char *buff = malloc(LOGBUF);
        int i = 1;
        strcpy(buf, "Running as: ");
        if (strlen(argv[0]) > 4095) {
            print_msg("Illegal length of argv[0]: %d\n", strlen(argv[0]));
            exit(1);
        }
        strcat(buf, argv[0]); // argv[0] can not be bigger than 4096
        while (i < argc) {
	    snprintf(buff, LOGBUF, "%s %s", buf, argv[i++]);
	    strcpy(buf, buff);
        }
        syslog(LOG_NOTICE, "%s\n", buf);
        free(buf);
        free(buff);
    }
    if (stat(_argv[0], &st) < 0) {
        if (do_chroot || _argv[0][0] == '/' || _argv[0][0] == '.')
	    print_msg("Warning: Can not find %s\n", _argv[0]);
    } else {
        if (do_fork) {
	    do_fork = fork();
	    if (do_fork < 0) {
	        print_msg("Could not fork\n");
	        exit(1);
	    }
	    if (do_fork > 0)
	        return 0;	// this is the parent process
	    if (verbose)
	        print_msg("Successfully forked\n");
	}
    }
    
    execvp(_argv[0], _argv);

    if (do_chroot == 0)
	print_msg("Could not execute %s\n", _argv[0]);
    else {
        if (stat(_argv[0], &st) < 0)
	    print_msg("Could not find file: %s\n", _argv[0]);
        else
	    if (access(_argv[0], X_OK) < 0)
		print_msg("Execute bit missing, or no permissions to execute %s\n", _argv[0]);
	    else
	        print_msg("Could not properly execute %s - the chroot environment might not be
set up correctly:
Create the directories /etc and /lib in chroot_dir and run \"ldd %s\"
to see which libraries are needed. Copy these to chroot_dir/lib, then chdir to
chroot_dir and execute \"ldconfig -X -r .\"\n", _argv[0], _argv[0]);
    }

    return 1;
}
