Edit

IABSD.fr/src/usr.bin/file/file.c

Branch :

  • Show log

    Commit

  • Author : mestre
    Date : 2019-11-30 14:01:45
    Hash : 083d4b5c
    Message : After fork(2) the pledge(2) in the parent proc can be reduced to "stdio rpath sendfd" so that it can call {l,}stat/open and sendfd for imsg_* in order to send fds to the child proc which is already pledged by recvfd to receive them OK brynet@ deraadt@

  • usr.bin/file/file.c
  • /* $OpenBSD: file.c,v 1.69 2019/11/30 14:01:45 mestre Exp $ */
    
    /*
     * Copyright (c) 2015 Nicholas Marriott <nicm@openbsd.org>
     *
     * Permission to use, copy, modify, and distribute this software for any
     * purpose with or without fee is hereby granted, provided that the above
     * copyright notice and this permission notice appear in all copies.
     *
     * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
     * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
     * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     * WHATSOEVER RESULTING FROM LOSS OF MIND, USE, DATA OR PROFITS, WHETHER
     * IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING
     * OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     */
    
    #include <sys/types.h>
    #include <sys/ioctl.h>
    #include <sys/mman.h>
    #include <sys/queue.h>
    #include <sys/socket.h>
    #include <sys/stat.h>
    #include <sys/uio.h>
    #include <sys/wait.h>
    
    #include <err.h>
    #include <errno.h>
    #include <fcntl.h>
    #include <getopt.h>
    #include <imsg.h>
    #include <libgen.h>
    #include <limits.h>
    #include <pwd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <time.h>
    #include <unistd.h>
    
    #include "file.h"
    #include "magic.h"
    #include "xmalloc.h"
    
    struct input_msg {
    	int		idx;
    
    	struct stat	sb;
    	int		error;
    
    	char		link_path[PATH_MAX];
    	int		link_error;
    	int		link_target;
    };
    
    struct input_ack {
    	int		idx;
    };
    
    struct input_file {
    	struct magic		*m;
    	struct input_msg	*msg;
    
    	const char		*path;
    	int			 fd;
    
    	void			*base;
    	size_t			 size;
    	int			 mapped;
    	char			*result;
    };
    
    extern char	*__progname;
    
    __dead void	 usage(void);
    
    static int	 prepare_message(struct input_msg *, int, const char *);
    static void	 send_message(struct imsgbuf *, void *, size_t, int);
    static int	 read_message(struct imsgbuf *, struct imsg *, pid_t);
    
    static void	 read_link(struct input_msg *, const char *);
    
    static __dead void child(int, pid_t, int, char **);
    
    static void	 test_file(struct input_file *, size_t);
    
    static int	 try_stat(struct input_file *);
    static int	 try_empty(struct input_file *);
    static int	 try_access(struct input_file *);
    static int	 try_text(struct input_file *);
    static int	 try_magic(struct input_file *);
    static int	 try_unknown(struct input_file *);
    
    static int	 bflag;
    static int	 cflag;
    static int	 iflag;
    static int	 Lflag;
    static int	 sflag;
    static int	 Wflag;
    
    static char	*magicpath;
    static FILE	*magicfp;
    
    static struct option longopts[] = {
    	{ "brief",       no_argument, NULL, 'b' },
    	{ "dereference", no_argument, NULL, 'L' },
    	{ "mime",        no_argument, NULL, 'i' },
    	{ "mime-type",   no_argument, NULL, 'i' },
    	{ NULL,          0,           NULL, 0   }
    };
    
    __dead void
    usage(void)
    {
    	fprintf(stderr, "usage: %s [-bchiLsW] file ...\n", __progname);
    	exit(1);
    }
    
    int
    main(int argc, char **argv)
    {
    	int			 opt, pair[2], fd, idx;
    	char			*home;
    	struct passwd		*pw;
    	struct imsgbuf		 ibuf;
    	struct imsg		 imsg;
    	struct input_msg	 msg;
    	struct input_ack	*ack;
    	pid_t			 pid, parent;
    
    	tzset();
    
    	for (;;) {
    		opt = getopt_long(argc, argv, "bchiLsW", longopts, NULL);
    		if (opt == -1)
    			break;
    		switch (opt) {
    		case 'b':
    			bflag = 1;
    			break;
    		case 'c':
    			cflag = 1;
    			break;
    		case 'h':
    			Lflag = 0;
    			break;
    		case 'i':
    			iflag = 1;
    			break;
    		case 'L':
    			Lflag = 1;
    			break;
    		case 's':
    			sflag = 1;
    			break;
    		case 'W':
    			Wflag = 1;
    			break;
    		default:
    			usage();
    		}
    	}
    	argc -= optind;
    	argv += optind;
    	if (cflag) {
    		if (argc != 0)
    			usage();
    	} else if (argc == 0)
    		usage();
    
    	if (pledge("stdio rpath getpw recvfd sendfd id proc", NULL) == -1)
    		err(1, "pledge");
    
    	magicfp = NULL;
    	if (geteuid() != 0 && !issetugid()) {
    		home = getenv("HOME");
    		if (home == NULL || *home == '\0') {
    			pw = getpwuid(getuid());
    			if (pw != NULL)
    				home = pw->pw_dir;
    			else
    				home = NULL;
    		}
    		if (home != NULL) {
    			xasprintf(&magicpath, "%s/.magic", home);
    			magicfp = fopen(magicpath, "r");
    			if (magicfp == NULL)
    				free(magicpath);
    		}
    	}
    	if (magicfp == NULL) {
    		magicpath = xstrdup("/etc/magic");
    		magicfp = fopen(magicpath, "r");
    	}
    	if (magicfp == NULL)
    		err(1, "%s", magicpath);
    
    	parent = getpid();
    	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, pair) != 0)
    		err(1, "socketpair");
    	switch (pid = fork()) {
    	case -1:
    		err(1, "fork");
    	case 0:
    		close(pair[0]);
    		child(pair[1], parent, argc, argv);
    	}
    	close(pair[1]);
    
    	if (pledge("stdio rpath sendfd", NULL) == -1)
    		err(1, "pledge");
    
    	fclose(magicfp);
    	magicfp = NULL;
    
    	if (cflag)
    		goto wait_for_child;
    
    	imsg_init(&ibuf, pair[0]);
    	for (idx = 0; idx < argc; idx++) {
    		fd = prepare_message(&msg, idx, argv[idx]);
    		send_message(&ibuf, &msg, sizeof msg, fd);
    
    		if (read_message(&ibuf, &imsg, pid) == 0)
    			break;
    		if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof *ack)
    			errx(1, "message too small");
    		ack = imsg.data;
    		if (ack->idx != idx)
    			errx(1, "index not expected");
    		imsg_free(&imsg);
    	}
    
    wait_for_child:
    	close(pair[0]);
    	while (wait(NULL) == -1 && errno != ECHILD) {
    		if (errno != EINTR)
    			err(1, "wait");
    	}
    	_exit(0); /* let the child flush */
    }
    
    static int
    prepare_message(struct input_msg *msg, int idx, const char *path)
    {
    	int	fd, mode, error;
    
    	memset(msg, 0, sizeof *msg);
    	msg->idx = idx;
    
    	if (strcmp(path, "-") == 0) {
    		if (fstat(STDIN_FILENO, &msg->sb) == -1) {
    			msg->error = errno;
    			return (-1);
    		}
    		return (STDIN_FILENO);
    	}
    
    	if (Lflag)
    		error = stat(path, &msg->sb);
    	else
    		error = lstat(path, &msg->sb);
    	if (error == -1) {
    		msg->error = errno;
    		return (-1);
    	}
    
    	/*
    	 * pledge(2) doesn't let us pass directory file descriptors around -
    	 * but in fact we don't need them, so just don't open directories or
    	 * symlinks (which could be to directories).
    	 */
    	mode = msg->sb.st_mode;
    	if (!S_ISDIR(mode) && !S_ISLNK(mode)) {
    		fd = open(path, O_RDONLY|O_NONBLOCK);
    		if (fd == -1 && (errno == ENFILE || errno == EMFILE))
    			err(1, "open");
    	} else
    		fd = -1;
    	if (S_ISLNK(mode))
    		read_link(msg, path);
    	return (fd);
    
    }
    
    static void
    send_message(struct imsgbuf *ibuf, void *msg, size_t msglen, int fd)
    {
    	if (imsg_compose(ibuf, -1, -1, 0, fd, msg, msglen) != 1)
    		err(1, "imsg_compose");
    	if (imsg_flush(ibuf) != 0)
    		err(1, "imsg_flush");
    }
    
    static int
    read_message(struct imsgbuf *ibuf, struct imsg *imsg, pid_t from)
    {
    	int	n;
    
    	while ((n = imsg_read(ibuf)) == -1 && errno == EAGAIN)
    		/* nothing */ ;
    	if (n == -1)
    		err(1, "imsg_read");
    	if (n == 0)
    		return (0);
    
    	if ((n = imsg_get(ibuf, imsg)) == -1)
    		err(1, "imsg_get");
    	if (n == 0)
    		return (0);
    
    	if ((pid_t)imsg->hdr.pid != from)
    		errx(1, "PIDs don't match");
    
    	return (n);
    
    }
    
    static void
    read_link(struct input_msg *msg, const char *path)
    {
    	struct stat	 sb;
    	char		 lpath[PATH_MAX];
    	char		*copy, *root;
    	int		 used;
    	ssize_t		 size;
    
    	size = readlink(path, lpath, sizeof lpath - 1);
    	if (size == -1) {
    		msg->link_error = errno;
    		return;
    	}
    	lpath[size] = '\0';
    
    	if (*lpath == '/')
    		strlcpy(msg->link_path, lpath, sizeof msg->link_path);
    	else {
    		copy = xstrdup(path);
    
    		root = dirname(copy);
    		if (*root == '\0' || strcmp(root, ".") == 0 ||
    		    strcmp (root, "/") == 0)
    			strlcpy(msg->link_path, lpath, sizeof msg->link_path);
    		else {
    			used = snprintf(msg->link_path, sizeof msg->link_path,
    			    "%s/%s", root, lpath);
    			if (used < 0 || (size_t)used >= sizeof msg->link_path) {
    				msg->link_error = ENAMETOOLONG;
    				free(copy);
    				return;
    			}
    		}
    
    		free(copy);
    	}
    
    	if (!Lflag && stat(path, &sb) == -1)
    		msg->link_target = errno;
    }
    
    static __dead void
    child(int fd, pid_t parent, int argc, char **argv)
    {
    	struct passwd		*pw;
    	struct magic		*m;
    	struct imsgbuf		 ibuf;
    	struct imsg		 imsg;
    	struct input_msg	*msg;
    	struct input_ack	 ack;
    	struct input_file	 inf;
    	int			 i, idx;
    	size_t			 len, width = 0;
    
    	if (pledge("stdio getpw recvfd id", NULL) == -1)
    		err(1, "pledge");
    
    	if (geteuid() == 0) {
    		pw = getpwnam(FILE_USER);
    		if (pw == NULL)
    			errx(1, "unknown user %s", FILE_USER);
    		if (setgroups(1, &pw->pw_gid) != 0)
    			err(1, "setgroups");
    		if (setresgid(pw->pw_gid, pw->pw_gid, pw->pw_gid) != 0)
    			err(1, "setresgid");
    		if (setresuid(pw->pw_uid, pw->pw_uid, pw->pw_uid) != 0)
    			err(1, "setresuid");
    	}
    
    	if (pledge("stdio recvfd", NULL) == -1)
    		err(1, "pledge");
    
    	m = magic_load(magicfp, magicpath, cflag || Wflag);
    	if (cflag) {
    		magic_dump(m);
    		exit(0);
    	}
    
    	for (i = 0; i < argc; i++) {
    		len = strlen(argv[i]) + 1;
    		if (len > width)
    			width = len;
    	}
    
    	imsg_init(&ibuf, fd);
    	for (;;) {
    		if (read_message(&ibuf, &imsg, parent) == 0)
    			break;
    		if (imsg.hdr.len != IMSG_HEADER_SIZE + sizeof *msg)
    			errx(1, "message too small");
    		msg = imsg.data;
    
    		idx = msg->idx;
    		if (idx < 0 || idx >= argc)
    			errx(1, "index out of range");
    
    		memset(&inf, 0, sizeof inf);
    		inf.m = m;
    		inf.msg = msg;
    
    		inf.path = argv[idx];
    		inf.fd = imsg.fd;
    
    		test_file(&inf, width);
    
    		if (imsg.fd != -1)
    			close(imsg.fd);
    		imsg_free(&imsg);
    
    		ack.idx = idx;
    		send_message(&ibuf, &ack, sizeof ack, -1);
    	}
    	exit(0);
    }
    
    static void *
    fill_buffer(int fd, size_t size, size_t *used)
    {
    	static void	*buffer;
    	ssize_t		 got;
    	size_t		 left;
    	void		*next;
    
    	if (buffer == NULL)
    		buffer = xmalloc(FILE_READ_SIZE);
    
    	next = buffer;
    	left = size;
    	while (left != 0) {
    		got = read(fd, next, left);
    		if (got == -1) {
    			if (errno == EINTR)
    				continue;
    			return (NULL);
    		}
    		if (got == 0)
    			break;
    		next = (char *)next + got;
    		left -= got;
    	}
    	*used = size - left;
    	return (buffer);
    }
    
    static int
    load_file(struct input_file *inf)
    {
    	size_t	used;
    
    	if (inf->msg->sb.st_size == 0 && S_ISREG(inf->msg->sb.st_mode))
    		return (0); /* empty file */
    	if (inf->msg->sb.st_size == 0 || inf->msg->sb.st_size > FILE_READ_SIZE)
    		inf->size = FILE_READ_SIZE;
    	else
    		inf->size = inf->msg->sb.st_size;
    
    	if (!S_ISREG(inf->msg->sb.st_mode))
    		goto try_read;
    
    	inf->base = mmap(NULL, inf->size, PROT_READ, MAP_PRIVATE, inf->fd, 0);
    	if (inf->base == MAP_FAILED)
    		goto try_read;
    	inf->mapped = 1;
    	return (0);
    
    try_read:
    	inf->base = fill_buffer(inf->fd, inf->size, &used);
    	if (inf->base == NULL) {
    		xasprintf(&inf->result, "cannot read '%s' (%s)", inf->path,
    		    strerror(errno));
    		return (1);
    	}
    	inf->size = used;
    	return (0);
    }
    
    static int
    try_stat(struct input_file *inf)
    {
    	if (inf->msg->error != 0) {
    		xasprintf(&inf->result, "cannot stat '%s' (%s)", inf->path,
    		    strerror(inf->msg->error));
    		return (1);
    	}
    	if (sflag || strcmp(inf->path, "-") == 0) {
    		switch (inf->msg->sb.st_mode & S_IFMT) {
    		case S_IFIFO:
    			if (strcmp(inf->path, "-") != 0)
    				break;
    		case S_IFBLK:
    		case S_IFCHR:
    		case S_IFREG:
    			return (0);
    		}
    	}
    
    	if (iflag && (inf->msg->sb.st_mode & S_IFMT) != S_IFREG) {
    		xasprintf(&inf->result, "application/x-not-regular-file");
    		return (1);
    	}
    
    	switch (inf->msg->sb.st_mode & S_IFMT) {
    	case S_IFDIR:
    		xasprintf(&inf->result, "directory");
    		return (1);
    	case S_IFLNK:
    		if (inf->msg->link_error != 0) {
    			xasprintf(&inf->result, "unreadable symlink '%s' (%s)",
    			    inf->path, strerror(inf->msg->link_error));
    			return (1);
    		}
    		if (inf->msg->link_target == ELOOP)
    			xasprintf(&inf->result, "symbolic link in a loop");
    		else if (inf->msg->link_target != 0) {
    			xasprintf(&inf->result, "broken symbolic link to '%s'",
    			    inf->msg->link_path);
    		} else {
    			xasprintf(&inf->result, "symbolic link to '%s'",
    			    inf->msg->link_path);
    		}
    		return (1);
    	case S_IFSOCK:
    		xasprintf(&inf->result, "socket");
    		return (1);
    	case S_IFBLK:
    		xasprintf(&inf->result, "block special (%lu/%lu)",
    		    (long)major(inf->msg->sb.st_rdev),
    		    (long)minor(inf->msg->sb.st_rdev));
    		return (1);
    	case S_IFCHR:
    		xasprintf(&inf->result, "character special (%lu/%lu)",
    		    (long)major(inf->msg->sb.st_rdev),
    		    (long)minor(inf->msg->sb.st_rdev));
    		return (1);
    	case S_IFIFO:
    		xasprintf(&inf->result, "fifo (named pipe)");
    		return (1);
    	}
    	return (0);
    }
    
    static int
    try_empty(struct input_file *inf)
    {
    	if (inf->size != 0)
    		return (0);
    
    	if (iflag)
    		xasprintf(&inf->result, "application/x-empty");
    	else
    		xasprintf(&inf->result, "empty");
    	return (1);
    }
    
    static int
    try_access(struct input_file *inf)
    {
    	char tmp[256] = "";
    
    	if (inf->msg->sb.st_size == 0 && S_ISREG(inf->msg->sb.st_mode))
    		return (0); /* empty file */
    	if (inf->fd != -1)
    		return (0);
    
    	if (inf->msg->sb.st_mode & (S_IWUSR|S_IWGRP|S_IWOTH))
    		strlcat(tmp, "writable, ", sizeof tmp);
    	if (inf->msg->sb.st_mode & (S_IXUSR|S_IXGRP|S_IXOTH))
    		strlcat(tmp, "executable, ", sizeof tmp);
    	if (S_ISREG(inf->msg->sb.st_mode))
    		strlcat(tmp, "regular file, ", sizeof tmp);
    	strlcat(tmp, "no read permission", sizeof tmp);
    
    	inf->result = xstrdup(tmp);
    	return (1);
    }
    
    static int
    try_text(struct input_file *inf)
    {
    	const char	*type, *s;
    	int		 flags;
    
    	flags = MAGIC_TEST_TEXT;
    	if (iflag)
    		flags |= MAGIC_TEST_MIME;
    
    	type = text_get_type(inf->base, inf->size);
    	if (type == NULL)
    		return (0);
    
    	s = magic_test(inf->m, inf->base, inf->size, flags);
    	if (s != NULL) {
    		inf->result = xstrdup(s);
    		return (1);
    	}
    
    	s = text_try_words(inf->base, inf->size, flags);
    	if (s != NULL) {
    		if (iflag)
    			inf->result = xstrdup(s);
    		else
    			xasprintf(&inf->result, "%s %s text", type, s);
    		return (1);
    	}
    
    	if (iflag)
    		inf->result = xstrdup("text/plain");
    	else
    		xasprintf(&inf->result, "%s text", type);
    	return (1);
    }
    
    static int
    try_magic(struct input_file *inf)
    {
    	const char	*s;
    	int		 flags;
    
    	flags = 0;
    	if (iflag)
    		flags |= MAGIC_TEST_MIME;
    
    	s = magic_test(inf->m, inf->base, inf->size, flags);
    	if (s != NULL) {
    		inf->result = xstrdup(s);
    		return (1);
    	}
    	return (0);
    }
    
    static int
    try_unknown(struct input_file *inf)
    {
    	if (iflag)
    		xasprintf(&inf->result, "application/octet-stream");
    	else
    		xasprintf(&inf->result, "data");
    	return (1);
    }
    
    static void
    test_file(struct input_file *inf, size_t width)
    {
    	char	*label;
    	int	 stop;
    
    	stop = 0;
    	if (!stop)
    		stop = try_stat(inf);
    	if (!stop)
    		stop = try_access(inf);
    	if (!stop)
    		stop = load_file(inf);
    	if (!stop)
    		stop = try_empty(inf);
    	if (!stop)
    		stop = try_magic(inf);
    	if (!stop)
    		stop = try_text(inf);
    	if (!stop)
    		stop = try_unknown(inf);
    
    	if (bflag)
    		printf("%s\n", inf->result);
    	else {
    		if (strcmp(inf->path, "-") == 0)
    			xasprintf(&label, "/dev/stdin:");
    		else
    			xasprintf(&label, "%s:", inf->path);
    		printf("%-*s %s\n", (int)width, label, inf->result);
    		free(label);
    	}
    	free(inf->result);
    
    	if (inf->mapped && inf->base != NULL)
    		munmap(inf->base, inf->size);
    }