// $Id: rpm-fake.c,v 1.32 2005/07/03 12:26:28 ensc Exp $    --*- c++ -*--

// Copyright (C) 2003 Enrico Scholz <enrico.scholz@informatik.tu-chemnitz.de>
//  
// 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; version 2 of the License.
//  
// 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
//  

#ifdef HAVE_CONFIG_H
#  include <config.h>
#endif

#include "pathconfig.h"
#include "util.h"

#include <lib/vserver.h>
#include <lib/internal.h>
#include <lib_internal/sys_clone.h>

#include <sys/socket.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>
#include <asm/unistd.h>
#include <string.h>
#include <stdbool.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <sys/mount.h>
#include <linux/fs.h>
#include <sched.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/un.h>
#include <fcntl.h>
#include <pwd.h>
#include <grp.h>


  // from selinux.h
  // FIXME: add configure autodetection and include <selinux.h> directly
int rpm_execcon(unsigned int verified,
		const char *filename,
		char *const argv[], char *const envp[]);


#define ENSC_WRAPPERS_PREFIX	"rpm-fake.so: "
#define ENSC_WRAPPERS_VSERVER	1
#define ENSC_WRAPPERS_UNISTD	1
#include <wrappers.h>

#undef _POSIX_SOURCE
#include "capability-compat.h"

#ifndef CLONE_NEWNS
#  define CLONE_NEWNS	0x00020000
#endif

#define LIBNAME		"rpm-fake.so"
#define PLATFORM_FILE	"/etc/rpm/platform"

#define INIT(FILE,FUNC)	FUNC##_func = ((__typeof__(FUNC) *) (xdlsym(FILE, #FUNC)))
#define DECLARE(FUNC)	static __typeof__(FUNC) *	FUNC##_func = 0

#define DEBUG 1

#define DBG_INIT	0x0001
#define DBG_VARIABLES	0x0002
#define DBG_RESOLVER	0x0004
#define DBG_EXECV	0x0008
#define DBG_ENV		0x0010
#define DBG_VERBOSE0	0x8000
#define DBG_VERBOSE1	(0x4000 | DBG_VERBOSE0)
#define DBG_VERBOSE2	(0x2000 | DBG_VERBOSE1)

int			wrapper_exit_code = 255;

static xid_t		ctx   = VC_NOCTX;
static uint32_t		caps  = ~0;
static int		flags = 0;
static char const *	mnts  = 0;
static char const *	root  = 0;
static int		pw_sock   = -1;
static int		sync_sock = -1;
static unsigned int	debug_level = 0;

static bool		is_initialized = false;

  //DECLARE(rpm_execcon);
  //DECLARE(execv);
DECLARE(getpwnam);
DECLARE(getgrnam);
DECLARE(endpwent);
DECLARE(endgrent);

static void		initRPMFake() __attribute__((__constructor__));
static void		exitRPMFake() __attribute__((__destructor__));

static inline bool
isDbgLevel(unsigned int level)
{
  return ((debug_level&level)==level);
}

static void *
xdlsym(void *handle, const char *symbol)
{
  void	*res = dlsym(handle, symbol);
  if (res==0) {
    char const	*error = dlerror();
    Vwrite(2, symbol, strlen(symbol));
    Vwrite(2, ": ", 2);
    Vwrite(2, error, strlen(error));
    Vwrite(2, "\n", 2);

    _exit(255);
  }

  return res;
}

static void
showHelp()
{
  WRITE_MSG(1,
	    "Usage: LD_PRELOAD=" LIBNAME " <executable> <args>*\n\n"
	    LIBNAME " unterstands the following environment variables:\n"
	    "  $RPM_FAKE_RESOLVER     ...  program which does the NSS resolving (defaults\n"
	    "                              to " RESOLVER_PROG ")\n"
	    "  $RPM_FAKE_RESOLVER_UID ...  uid of the resolver program\n"
	    "  $RPM_FAKE_RESOLVER_GID ...  gid of the resolver program\n"
	    "  $RPM_FAKE_CTX          ...  vserver context which shall be used for resolver\n"
	    "                              and scriptlets\n"
	    "  $RPM_FAKE_CAP          ...  linux capability remove-mask for the context\n"
	    "  $RPM_FAKE_FLAGS        ...  vserver flags of the context\n"
	    "  $RPM_FAKE_CHROOT       ...  directory of the chroot environment\n"
	    "  $RPM_FAKE_NAMESPACE_MOUNTS\n"
	    "                          ... colon separated list of directories which will\n"
            "                              umounted before scriptlet execution\n\n"
	    "  $RPM_FAKE_HELP          ... shows this message\n"
	    "  $RPM_FAKE_VERSION       ... shows the version of this program\n\n"
	    "  $RPM_FAKE_DEBUG         ... sets the debuglevel bitmask\n\n"
	    "Please report bugs to " PACKAGE_BUGREPORT "\n");
  exit(0);
}

static void
showVersion()
{
  WRITE_MSG(1,
	    LIBNAME " " VERSION " -- wrapper around rpm\n"
	    "This program is part of " PACKAGE_STRING "\n\n"
	    "Copyright (C) 2003 Enrico Scholz\n"
	    VERSION_COPYRIGHT_DISCLAIMER);
  exit(0);
}

static void
unsetPreloadEnv()
{
  char			*env = getenv("LD_PRELOAD");
  char			*pos;

    // the const <-> non-const assignment is not an issue since the following
    // modifying operations will not be executed in the const-case
  env = env ? env : "";
  pos = strstr(env, LIBNAME);

  if (pos!=0) {
    char	*end_pos = pos + sizeof(LIBNAME);
    bool	is_end = (end_pos[-1]=='\0');
    char	*start_pos;

    end_pos[-1] = '\0';
    start_pos   = strrchr(env, ':');
    if (start_pos==0) start_pos = env;
    else if (!is_end) ++start_pos;

    if (is_end) *start_pos = '\0';
    else        memmove(start_pos, end_pos, strlen(end_pos)+1);
  }

#ifdef DEBUG
  if (isDbgLevel(DBG_VERBOSE1|DBG_VARIABLES)) {
    WRITE_MSG(2, "env='");
    WRITE_STR(2, env);
    WRITE_MSG(2, "'\n");
  }
#endif

  if (*env=='\0') unsetenv("LD_PRELOAD");
}

static void
clearEnv()
{
  if (isDbgLevel(DBG_ENV)) WRITE_MSG(2, "clearEnv()\n");
  
  unsetenv("RPM_FAKE_S_CONTEXT_REV");
  unsetenv("RPM_FAKE_S_CONTEXT_NR");
  unsetenv("RPM_FAKE_CTX");
  unsetenv("RPM_FAKE_FLAGS");
  unsetenv("RPM_FAKE_CHROOT");
  unsetenv("RPM_FAKE_NAMESPACE_MOUNTS");

  unsetenv("RPM_FAKE_RESOLVER_GID");
  unsetenv("RPM_FAKE_RESOLVER_UID");
  unsetenv("RPM_FAKE_RESOLVER");    
  unsetenv("RPM_FAKE_PWSOCKET");

  unsetenv("RPM_FAKE_DEBUG");

  unsetPreloadEnv();
}

static int
getDefaultEnv(char const *key, int dflt)
{
  char		*env = getenv(key);
  int		res;

  if (env==0 || env[0]=='\0') res = dflt;
  else                        res = atoi(env);

  return res;
}

  /// \returns true iff we are in ctx after leaving this function
static bool
setupContext(xid_t xid, char const **xid_str)
{
  bool		res = false;
  
  if (vc_isSupported(vcFEATURE_MIGRATE)) {
    xid_t	rc=VC_NOCTX;

    if ((xid==VC_DYNAMIC_XID || !vc_is_dynamic_xid(xid)) &&
	(rc=vc_ctx_create(xid))==VC_NOCTX &&
	errno!=EEXIST) {
      perror(ENSC_WRAPPERS_PREFIX "vc_create_context()");
      exit(255);
    }

    if (rc!=VC_NOCTX) {
      char			buf[sizeof(xid_t)*3 + 128];
      size_t			l;
      struct vc_ctx_caps	caps;
      
      strcpy(buf, "rpm-fake.so #");
      l = utilvserver_fmt_uint(buf+sizeof("rpm-fake.so #")-1, getppid());
      Evc_set_vhi_name(rc, vcVHI_CONTEXT, buf, sizeof("rpm-fake.so #")+l-1);

      caps.ccaps =  0ull;
      caps.cmask = ~0ull;
      caps.bcaps = ~vc_get_insecurebcaps();
      caps.bmask = ~0ull;
      Evc_set_ccaps(rc, &caps);
      
	// context will be activated later...

      xid = rc;
      res = true;
    }
  }

  if (xid==VC_DYNAMIC_XID)
    *xid_str = 0;
  else {
    char		buf[sizeof(xid_t)*3 + 2];
    size_t		l;
    
    l        = utilvserver_fmt_uint(buf, xid); buf[l] = '\0';
    *xid_str = strdup(buf);
  }
 
  Ewrite(3, &xid, sizeof xid);
  return res;
}

#if 0
static void
initPwSocket()
{
  char const *	sock_name = getenv("RPM_FAKE_PWSOCKET");
  if (sock_name!=0) {
    int flag;
    struct sockaddr_un	addr = {
      .sun_family = AF_UNIX,
    };

    strncpy(addr.sun_path, sock_name, sizeof(addr.sun_path)-1);
    addr.sun_path[sizeof(addr.sun_path)-1]='\0';
    
    if ((pw_sock=socket(AF_UNIX, SOCK_STREAM, 0))==-1 ||
	connect(pw_sock, (struct sockaddr *)(&addr), sizeof addr)==-1 ||
	(flag=fcntl(pw_sock, F_GETFD))==-1 ||
	fcntl(pw_sock, F_SETFD, flag | FD_CLOEXEC)==-1) {
      perror(ENSC_WRAPPERS_PREFIX "error while initializing pw-socket");
      exit(255);
    }
  }
}
#else
static void
initPwSocket()
{
  char const *	resolver = getenv("RPM_FAKE_RESOLVER");
  if (resolver==0) resolver=RESOLVER_PROG;
  
  if (resolver!=0 && *resolver!='\0') {
    int			res_sock[2];
    int			sync_pipe[2];
    pid_t		pid;
    char const *	uid=0;
    char const *	gid=0;

    uid=getenv("RPM_FAKE_RESOLVER_UID");
    gid=getenv("RPM_FAKE_RESOLVER_GID");

    if (socketpair(AF_UNIX, SOCK_STREAM, 0, res_sock)==-1 ||
	pipe(sync_pipe)==-1 ||
	fcntl(res_sock[0],  F_SETFD, FD_CLOEXEC)==-1 ||
	fcntl(sync_pipe[0], F_SETFD, FD_CLOEXEC)==-1) {
      perror(ENSC_WRAPPERS_PREFIX "failed to create/initialize resolver-socket or pipe");
      exit(255);
    }

    pid = fork();
    if (pid==-1) {
      perror(ENSC_WRAPPERS_PREFIX "fork()");
      exit(255);
    }

    if (pid==0) {
      char const	*args[20];
      char const	**ptr  = args;
      char const 	*env[] = { "HOME=/", "PATH=/bin:/usr/bin", 0 };
      char const	*xid_str;
      char		flag_str[ sizeof(flags)*3 + 2];
      char		caps_str[ sizeof(caps)*3  + 2];

      clearEnv();
      
      setsid();
      dup2(res_sock[1],  0);
      dup2(res_sock[1],  1);
      if (sync_pipe[1]!=3) {
	close(3);
	dup2(sync_pipe[1], 3);
	close(sync_pipe[1]);
      }
      close(res_sock[1]);
	/* ... *socket[0] are marked as close-on-exec ...*/

      flag_str[utilvserver_fmt_uint(flag_str, flags)] = '\0';
      caps_str[utilvserver_fmt_uint(caps_str, caps)]  = '\0';

      *ptr++ = resolver;
      *ptr++ = "-F"; *ptr++ = flag_str;
      *ptr++ = "-C"; *ptr++ = caps_str;
      if (root)  { *ptr++ = "-r"; *ptr++ = ".";   }
      if (uid)   { *ptr++ = "-u"; *ptr++ = uid;   }
      if (gid)   { *ptr++ = "-g"; *ptr++ = gid;   }

      if (root) Echdir(root);

      if (setupContext(ctx, &xid_str)) { *ptr++ = "-s"; }
      else if (xid_str)                { *ptr++ = "-c"; *ptr++ = xid_str; }
      
      *ptr++ = 0;
      execve(resolver, (char **)args, (char **)env);
      perror(ENSC_WRAPPERS_PREFIX "failed to exec resolver");
      exit(255);
    }
    else {
      uint8_t		c;

      close(res_sock[1]);
      close(sync_pipe[1]);
      pw_sock   = res_sock[0];
      sync_sock = sync_pipe[0];

      if (read(sync_sock, &ctx, sizeof ctx)!=sizeof(ctx) ||
	  read(sync_sock, &c, 1)!=1 ||
	  write(pw_sock, ".", 1)!=1 ||
	  read(pw_sock, &c,   1)!=1 ||
	  c!='.') {
	WRITE_MSG(2, ENSC_WRAPPERS_PREFIX "failed to initialize communication with resolver\n");
	exit(255);
      }

      if (wait4(pid, 0, WNOHANG,0)==-1) {
	WRITE_MSG(2, ENSC_WRAPPERS_PREFIX" unexpected initialization-error of resolver\n");
	exit(255);
      }
    }
  }
}
#endif

static void
reduceCapabilities()
{
  struct __user_cap_header_struct header;
  struct __user_cap_data_struct user;
  
  header.version = _LINUX_CAPABILITY_VERSION;
  header.pid     = 0;

  if (capget(&header, &user)==-1) {
    perror("capget()");
    exit(wrapper_exit_code);
  }

  user.effective   &= ~(1<<CAP_MKNOD);
  user.permitted   &= ~(1<<CAP_MKNOD);
  user.inheritable &= ~(1<<CAP_MKNOD);

  if (capset(&header, &user)==-1) {
    perror("capset()");
    exit(wrapper_exit_code);
  }
}

static void
initEnvironment()
{
  int		syscall_rev;
  int		syscall_nr;
  
  if (is_initialized) return;

  syscall_rev = getDefaultEnv("RPM_FAKE_S_CONTEXT_REV", 0);
  syscall_nr  = getDefaultEnv("RPM_FAKE_S_CONTEXT_NR",  273);
  
#ifdef VC_ENABLE_API_LEGACY
  {
    extern void vc_init_internal_legacy(int ctx_rev, int ctx_number,
					int ipv4_rev, int ipv4_number);
  
    vc_init_internal_legacy(syscall_rev, syscall_nr, 3, 274);
  }
#endif

  ctx       = getDefaultEnv("RPM_FAKE_CTX",  VC_DYNAMIC_XID);
  caps      = getDefaultEnv("RPM_FAKE_CAP",  ~0x3404040f);
  flags     = getDefaultEnv("RPM_FAKE_FLAGS", 0);
  root      = getenv("RPM_FAKE_CHROOT");
  mnts      = getenv("RPM_FAKE_NAMESPACE_MOUNTS");
  if (mnts && *mnts) mnts = strdup(mnts);
  else               mnts = 0;

#if DEBUG
  if (isDbgLevel(DBG_VERBOSE1))
    dprintf(2, "ctx=%u, caps=%016x, flags=%016x,\nroot='%s',\nmnts='%s'\n",
	    ctx, caps, flags, root, mnts);
#endif
  
  is_initialized = true;
}

static void
initSymbols()
{
    //INIT(RTLD_NEXT, rpm_execcon);
    //INIT(RTLD_NEXT, execv);
  INIT(RTLD_NEXT, getgrnam);
  INIT(RTLD_NEXT, getpwnam);
  INIT(RTLD_NEXT, endpwent);
  INIT(RTLD_NEXT, endgrent);
}

void
initRPMFake()
{
  if (getenv("RPM_FAKE_VERSION")) showVersion();
  if (getenv("RPM_FAKE_HELP"))    showHelp();
  
  debug_level = getDefaultEnv("RPM_FAKE_DEBUG", 0);

  if (isDbgLevel(DBG_INIT)) WRITE_MSG(2, ">>>>> initRPMFake <<<<<\n");
  
  reduceCapabilities();
  initSymbols();
  initEnvironment();
  initPwSocket();

#if 0
  if (isDbgLevel(DBG_VARIABLES|DBG_VERBOSE2)) {
    
  }
#endif
}

void
exitRPMFake()
{ 
  if (isDbgLevel(DBG_INIT)) WRITE_MSG(2, ">>>>> exitRPMFake <<<<<\n");
  if (pw_sock!=-1) {
    uint8_t	c;
    if (read(sync_sock, &c, 1)!=1) { /*...*/ }
    if (write(pw_sock, "Q", 1)!=1) { /*...*/ }
  }
}


  //============   the worker part   ===========


static bool
doPwStringRequest(uint32_t *result, char style, char const *name)
{
  uint32_t	len = strlen(name);
  uint8_t	code;
  uint8_t	c;

  return (TEMP_FAILURE_RETRY(read (sync_sock, &c, 1))==1 &&
	  TEMP_FAILURE_RETRY(write(pw_sock, &style, 1))==1 &&
	  TEMP_FAILURE_RETRY(write(pw_sock, &len,   sizeof len))==sizeof(len) &&
	  TEMP_FAILURE_RETRY(write(pw_sock, name,   len))==(ssize_t)(len) &&
	  TEMP_FAILURE_RETRY(read (pw_sock, &code,  sizeof code))==sizeof(code) &&
	  TEMP_FAILURE_RETRY(read (pw_sock, result, sizeof *result))==sizeof(*result) &&
	  code!=0);
}

struct passwd *
getpwnam(const char * name)
{
  if (pw_sock==-1) return getpwnam_func(name);
  else {
    uint32_t			id;
    static struct passwd	res = {
      .pw_passwd = "*",
      .pw_gid    = -1,
      .pw_gecos  = "",
      .pw_dir    = "/",
      .pw_shell  = "/bin/false"
    };

    res.pw_name = (char *)(name);
    if (!doPwStringRequest(&id, 'P', name)) return 0;
    res.pw_uid = id;
    
    return &res;
  }
}

struct group *
getgrnam(const char * name)
{
  if (pw_sock==-1) return getgrnam_func(name);
  else {
    uint32_t			id;
    static struct group		res = {
      .gr_passwd = "*",
      .gr_mem    = 0
    };

    res.gr_name = (char *)(name);
    if (!doPwStringRequest(&id, 'G', name)) return 0;
    res.gr_gid = id;

    return &res;
  }
}

void
endgrent()
{
  if (pw_sock==-1) endgrent_func();
  TEMP_FAILURE_RETRY(write(pw_sock, "Cg", 2));
}

void
endpwent()
{
  if (pw_sock==-1) endpwent_func();
  TEMP_FAILURE_RETRY(write(pw_sock, "Cp", 2));
}


static int
execvWorker(char const *path, char * const argv[], char * const envp[])
{
  int		res = -1;

  if (vc_isSupported(vcFEATURE_MIGRATE))
    res = vc_ctx_migrate(ctx);
  else {
#ifdef VC_ENABLE_API_COMPAT  
    res = vc_new_s_context(ctx,caps,flags);
#else
    WRITE_MSG(2, ENSC_WRAPPERS_PREFIX "can not change context: migrate kernel feature missing and 'compat' API disabled\n");
#endif
  }

  clearEnv();    
    
  if (res!=-1)
    res=execve(path, argv, envp);

  return res;
}

struct ExecvParams
{
    char const *	path;
    char * const *	argv;
    char * const *	envp;
    char const *	mnts;
};

static int
removeNamespaceMountsChild(struct ExecvParams const *params)
{
  char			buf[strlen(params->mnts)+1], *ptr;

  strcpy(buf, params->mnts);
  ptr = strtok(buf, ":");
  while (ptr) {
    if (umount2(ptr, 0)==-1) {
	// FIXME: What is the semantic for CLONE_NEWNS? Is it ok that mounts in
	// chroots are visible only, when chroot is on /dev/root?
	//
	// For now, ignore any errors, but future versions should handle them.

	//return -1;
    }
    ptr = strtok(0, ":");
  }

  return execvWorker(params->path, params->argv, params->envp);
}

static int
removeNamespaceMounts(char const *path,
		      char * const argv[], char * const envp[])
{
  if (mnts==0) return execvWorker(path, argv, envp);

  {
    int				status;
    pid_t			p, pid;
    struct ExecvParams		params;

    params.path = path;
    params.argv = argv;
    params.envp = envp;
    params.mnts = mnts;

      // the rpmlib signal-handler is still active; use the default one to
      // make wait4() working...
    signal(SIGCHLD, SIG_DFL);

#ifdef NDEBUG
    pid = sys_clone(CLONE_NEWNS|SIGCHLD|CLONE_VFORK, 0);
#else
    pid = sys_clone(CLONE_NEWNS|SIGCHLD, 0);
#endif

    switch (pid) {
      case -1	:  return -1;
      case 0	:  _exit(removeNamespaceMountsChild(&params));
      default	:  break;
    }
	
    while ((p=wait4(pid, &status, 0,0))==-1 &&
	   (errno==EINTR || errno==EAGAIN)) ;

    if (p==-1)   return -1;

    if (WIFEXITED(status))   _exit(WEXITSTATUS(status));
    if (WIFSIGNALED(status)) kill(getpid(), WTERMSIG(status));

    return -1;
  }
}


int
execv(char const *path, char * const argv[])
{
  extern char **environ;

  if (isDbgLevel(DBG_EXECV)) {
    WRITE_MSG(2, "execv('");
    WRITE_STR(2, path);
    WRITE_MSG(2, "', ...)\n");
  }

  return removeNamespaceMounts(path, argv, environ);
}

int
rpm_execcon(unsigned int UNUSED verified,
	    const char *filename,
	    char *const argv[], char *const envp[])
{
  if (isDbgLevel(DBG_EXECV)) {
    WRITE_MSG(2, "rpm_execcon(..., '");
    WRITE_STR(2, filename);
    WRITE_MSG(2, "', ...)\n");
  }

  return removeNamespaceMounts(filename, argv, envp);
}

int
is_selinux_enabled()
{
  return 0;
}
