/*
 * Simple mount interface for userfs.  This does permissions checking
 * and allows non-suid processes to be mounted.
 *
 * It checks the proposed mount dir, does the mount and starts up
 * the filesystem program attached to the kernel with pipes to
 * deal with the filesystem operations.  The filesystem process
 * runs as the invoking user.
 *
 * Muserfs expects the child to only ever exit properly when it
 * gets an EOF from the kernel.  Muserfs takes signals and umounts
 * the filesystem when it gets normal termination signals.
 * muserfs will also attempt to unmount the filesystem if the child
 * exists unexpectedly.  If this happens, muserfs will exit regardless
 * of whether it was successful.
 * 
 * The /etc/mtab file is also updated correctly, so that umount should
 * be able to unmount the filesystem.
 *
 * The input and output fds are inhereted by the filesystem process
 * and are passed on the command-line with the -i fd (kernel to process)
 * and -o fd (process to kernel) options.
 *
 * Usage: muserfs [-a] program mount-point [args...]
 *
 * This program is distributed under the terms of the
 * Free Software Foundation General Public Licence.
 * Copyright Jeremy Fitzhardinge 1993
 */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdarg.h>
#include <mntent.h>
#include <pwd.h>
#include <getopt.h>
#include <signal.h>
#include <fcntl.h>

#include <sys/wait.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <sys/stat.h>
#include <sys/param.h>

#include <linux/fs.h>
#include <linux/userfs_fs.h>
#include <linux/userfs_mount.h>
#include <linux/userfs_types.h>
#include <linux/userfs_fs_sb.h>

#include "io.h"

char *progname = NULL;

const char *mount_point = NULL;

int doumount(const char *);
int mounted = 0;

void sighandler(int sig)
{
  switch (sig)
    {
    case SIGINT:
    case SIGTERM:
    case SIGHUP:
    case SIGQUIT:
    case SIGCHLD:
      if (!mounted)
        break;
		
      if (!doumount(mount_point))
        fprintf(stderr, "%s: failed to unmount \"%s\"\n",
                progname, mount_point);

    default:
      return;
    }
}

static void sysfatal(const char *str, ...)
{
  va_list va;
  int er = errno;
	
  va_start(va, str);
  fprintf(stderr, "%s: ", progname);
  vfprintf(stderr, str, va);
  fprintf(stderr, ": %s\n", strerror(er));
  va_end(va);
  exit(1);
}

static void syserror(const char *str, ...)
{
  va_list va;
  int er = errno;
	
  va_start(va, str);
  fprintf(stderr, "%s: ", progname);
  vfprintf(stderr, str, va);
  fprintf(stderr, ": %s\n", strerror(er));
  va_end(va);
}

void *xmalloc(size_t s)
{
  void *m = malloc(s);

  if (m == NULL)
    sysfatal("memory allocation failure");

  return m;
}

char *fullpath(const char *p)
{
  char path[MAXPATHLEN];
  int dl;
  char *np;

  if (realpath(p, path) == NULL)
    return strdup(p);
  else
    return strdup(path);
}

char *makeopts(char **av, int narg, const char *name, int pid)
{
  int i, j, nblen;
  char *op, *cp;
  char nbuf[100];

  if (narg > 2)
    narg = 2;                   /* Only the host and the servicename
                                 belong here, because there's the passwd
                                 among the arguments */
	
  sprintf(nbuf, "username=%s,user,pid=%d,exec,nodev,nosuid,", name, pid);
  nblen = j = strlen(nbuf);
	
  for(i = 0; i < narg; i++)
    j += strlen(av[i]) + 1;

  op = xmalloc(j + 1);

  strcpy(op, nbuf);
  cp = &op[nblen];
	
  for(i = 0; i < narg; i++)
    {
      strcpy(cp, av[i]);
      cp += strlen(av[i]);
      *cp++ = ',';
    }

  cp[-1] = 0;

  return op;
}

int doumount(const char *mpoint)
{
  int kid;
  int wstat;
  int i;
  static volatile int interlock;
  static const char *umounts[] =
    {
      "/sbin/umount",
      "/etc/umount",
      "/usr/sbin/umount",
      "/bin/umount",
    };
	
  if (++interlock != 1)
    return 0;
  if (mpoint == NULL)
    return 0;
	
  switch(kid = fork())
    {
    case -1:
      perror("umount fork failed");
      return 1;
    case 0:
      setuid(geteuid());
      for(i = 0; i < sizeof(umounts)/sizeof(*umount); i++)
        execl(umounts[i], "umount", mpoint, NULL);
      sysfatal("exec of \"umount %s\" failed", mpoint);
      exit(1);
    }

  if (wait4(kid, &wstat, 0, NULL) == -1)
    sysfatal("Wait for umount failed");

  interlock--;
	
  if (WEXITSTATUS(wstat) != 0)
    return 0;
  mounted = 0;
  return 1;
}

static int prot_seq = 0;

void genhdr(up_preamble *pre, up_ops op)
{
  pre->version = UP_VERSION;
  pre->op = op;
  pre->isreq = UP_REQ;
  pre->seq = prot_seq++;
  pre->size = 0;
}

typedef unsigned char *(*encode_func)(const void *, unsigned char *);
typedef unsigned char *(*decode_func)(void *, unsigned char *);
typedef unsigned int (*sizeof_func)(const void *);

#define GEN_ENC(var)	(encode_func)(var)
#define GEN_DEC(var)	(decode_func)(var)
#define GEN_SIZ(var)	(sizeof_func)(var)

void userfs_genpkt(struct super_block *, up_preamble *, up_ops);

/* Encode and send request, receive and decode reply */
#define doop(from, to, pre, repl, st, ss, rt, rs)\
	_userfs_doop(from, to, pre, repl, \
		     GEN_ENC(encode_##st), (void *)ss, \
		     GEN_DEC(decode_##rt), (void *)rs, \
		     GEN_SIZ(sizeof_##st), GEN_SIZ(sizeof_##rt))

int _userfs_doop(int from, int to,
		 up_preamble *pre, upp_repl *repl,
		 encode_func se, void *send,
		 decode_func rd, void *recv,
		 sizeof_func sso, sizeof_func rso)
{
  int ret;
  unsigned char *p, *p0;
  int replsz = sizeof_upp_repl(repl);
	
  pre->size = (*sso)(send);

  p = p0 = xmalloc(USERFS_MAX_XFER);
	
  p = encode_up_preamble(pre, p0);
  if ((ret = fullwrite(to, p0, p-p0)) != p-p0)
    {
      free(p0);
      return -1;
    }

  if (pre->size > 0)
    {
      p = (*se)(send, p0);
      if ((ret = fullwrite(to, p0, p-p0)) != p-p0)
        {
          free(p0);
          return -1;
        }
    }

  if (fullread(from, p0, replsz) != replsz)
    {
      free(p0);
      return -1;
    }

  decode_upp_repl(repl, p0);

  if (repl->size != 0)
    {
      if (fullread(from, p0, repl->size) != repl->size)
        {
          free(p0);
          return -1;
        }
      (*rd)(recv, p0);
    }
  free(p0);
	
  return 0;
}

/*
 * Get mount flags from the filesystem
 */
int getflags(int in, int out)
{
  return 0;
}

/*
 * Check filesystem process is there and functioning by asking if
 * it supports the upp_mount operation.  If it doesn't or fails to
 * respond then the mount is aborted.
 */
int checkfs(int in, int out)
{
  up_preamble pre;
  upp_repl repl;
  int sz;
	
  genhdr(&pre, up_mount);
  pre.isreq = UP_ENQ;
	
  if (doop(in, out, &pre, &repl, void, NULL, void, NULL) != 0)
    return 0;

  if (repl.version != UP_VERSION || repl.op != up_mount ||
      repl.seq != 0 || repl.isreq != UP_REPL ||
      repl.size != 0 || repl.errno != 0)
    return 0;
	
  return 1;
}

int domount(int infd, int outfd, const char *mpoint, char *prog,
	    char **args, int nargs, int kid, int flags)
{
  struct userfs_mount mnt;
  struct mntent ment;
  FILE *mtab;
  struct passwd *pwd;
  char *mdev;
  int fd;
	
  if ((pwd = getpwuid(getuid())) == NULL)
    sysfatal("can't get password file entry for uid %d",
             getuid());
	
  mnt.tokern = infd;
  mnt.fromkern = outfd;
  mnt.version = USERFS_VERSION;
  mnt.magic = USERFS_SUPER_MAGIC;
  mnt.seq = prot_seq;
	
  if (mount(NULL, mpoint, "userfs",
            MS_MGC_VAL|MS_NODEV|MS_NOSUID|flags, &mnt) == -1)
    {
      syserror("mount of %s on %s failed",
               prog, mpoint);
      close(infd);
      close(outfd);
      return 1;
    }

  close(infd);
  close(outfd);

  mdev = xmalloc(strlen(prog) + 7);
  sprintf(mdev, "%s", prog);
	
  ment.mnt_fsname = mdev;
  ment.mnt_dir = fullpath(mpoint);
  ment.mnt_type = "userfs";
  ment.mnt_opts = makeopts(args, nargs, pwd->pw_name, kid);
  ment.mnt_freq = 0;
  ment.mnt_passno= 0;

  mount_point = ment.mnt_dir;
	
  if ((fd = open(MOUNTED"~", O_RDWR|O_CREAT|O_EXCL, 0600)) == -1)
    {
      syserror("Can't get "MOUNTED"~ lock file");
      return 1;
    }
  close(fd);
	
  if ((mtab = setmntent(MOUNTED, "a+")) == NULL)
    {
      syserror("Can't open " MOUNTED);
      return 1;
    }

  if (addmntent(mtab, &ment) == 1)
    {
      syserror("Can't write mount entry");
      return 1;
    }
  if (fchmod(fileno(mtab), 0644) == -1)
    {
      syserror("Can't set perms on "MOUNTED);
      return 1;
    }
  endmntent(mtab);

  if (unlink(MOUNTED"~") == -1)
    {
      syserror("Can't remove "MOUNTED"~");
      return 1;
    }
  free(mdev);
  mounted = 1;
  return 0;
}

int mountok(struct stat *st)
{
  if (!S_ISDIR(st->st_mode))
    {
      errno = ENOTDIR;
      return -1;
    }
	
  if (getuid() != 0 &&
      (getuid() != st->st_uid ||
       (st->st_mode & S_IRWXU) != S_IRWXU)
      )
    {
      errno = EPERM;
      return -1;
    }

  return 0;
}

int main(int argc, char *argv[])
{
  struct stat st;
  int kid, i, ch;
  int p1[2], p2[2];
  char **args;
  char b1[10], b2[10];
  int async = 0;
  int ret = 0;
	
  progname = argv[0];
  argv += 1;
  argc -= 1;

  if (geteuid() != 0 || getegid() != getgid()) {
    fprintf(stderr, "%s: must be installed suid root\n",
            progname);
    exit(1);
  }

  if ((argc > 0) && (strcmp(argv[0], "-a") == 0)) {
    async = 1;
    argv += 1;
    argc -= 1;
  }
  if (argc < 2) {
    fprintf(stderr,
            "Usage: %s [-a] program mount-point [args...]\n",
            progname);
    exit(1);
  }

#if 1
  if (async && getuid() != 0) {
    fprintf(stderr,
            "%s: async (-a) ignored: you can't unmount if you arn't root\n",
            progname);
    async = 0;
  }
#endif
	
  if (stat(argv[1], &st) == -1)
    sysfatal("Stat of %s failed", argv[1]);

  if (mountok(&st) == -1)
    sysfatal("can't mount on %s", argv[1]);

  if (pipe(p1) == -1 || pipe(p2) == -1)
    sysfatal("can't create pipe");

  for(i = 1; i < NSIG; i++) {
    struct sigaction sa;
    sigset_t ss;

    sigemptyset(&ss);
		
    sa.sa_handler = sighandler;
    sa.sa_flags = SA_NOMASK|SA_RESTART;
    sa.sa_mask = ss;
		
    if (i != SIGKILL && i != SIGSTOP)
      sigaction(i, &sa, NULL);
  }
  signal(SIGCHLD, SIG_DFL);
	
  switch(kid = fork()) {
  case -1:                      /* fail */
    sysfatal("fork failed");

  case 0:			/* child */
    break;

  default:                      /* parent */
    close(p1[1]);
    close(p2[0]);

    mounted = 0;

    if (checkfs(p1[0], p2[1])) {
      int flags = getflags(p1[0], p2[1]);

      ret = domount(p1[0], p2[1], argv[1], argv[0], 
                    &argv[2], argc-2, kid, flags);
    }
    else
      fprintf(stderr,
              "%s: filesystem failed to respond correctly.\n",
              progname);

    while (!async) {
      int wstat;
			
      if (wait4(kid, &wstat, 0, NULL) == -1)
        fprintf(stderr, "%s: wait for pid %d failed: %s\n",
                progname, kid, strerror(errno));
      else
        if (!WIFEXITED(wstat))
          continue;
			
      if (mounted && !doumount(mount_point) != 0)
        fprintf(stderr, "%s: Failed to umount filesystem\n",
                progname);
      break;
    }
    exit(ret);
  }

  /* child */
  close(p1[0]);
  close(p2[1]);
	
  if (setreuid(-1, getuid()) == -1)
    sysfatal("Can't drop suid permissions");

  {
    static int isigs[] = { SIGKILL, SIGTERM, SIGINT };
    for(i = 0; i < sizeof(isigs)/sizeof(*isigs); i++) {
      struct sigaction sa;
      sigset_t ss;

      sigemptyset(&ss);
		
      sa.sa_handler = SIG_IGN;
      sa.sa_flags = SA_NOMASK|SA_RESTART;
      sa.sa_mask = ss;
		
      sigaction(isigs[i], &sa, NULL);
    }
  }
  args = (char **)xmalloc(sizeof(char *)* (argc + 6));

  sprintf(b1, "%d", p2[0]);
  sprintf(b2, "%d", p1[1]);
	
  args[0] = argv[0];
  args[1] = "-i";
  args[2] = b1;
  args[3] = "-o";
  args[4] = b2;
  args[5] = "-m";
  args[6] = fullpath(argv[1]);

  for(i = 0; i <= argc-3; i++)
    args[i+7] = argv[i+2];
  args[i+7] = NULL;
	
  execvp(args[0], args);

  sysfatal("exec of %s failed", args[0]);
  exit(1);
}
