| /* |
| * CDDL HEADER START |
| * |
| * The contents of this file are subject to the terms of the |
| * Common Development and Distribution License (the "License"). |
| * You may not use this file except in compliance with the License. |
| * |
| * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE |
| * or http://www.opensolaris.org/os/licensing. |
| * See the License for the specific language governing permissions |
| * and limitations under the License. |
| * |
| * When distributing Covered Code, include this CDDL HEADER in each |
| * file and include the License file at usr/src/OPENSOLARIS.LICENSE. |
| * If applicable, add the following below this CDDL HEADER, with the |
| * fields enclosed by brackets "[]" replaced with your own identifying |
| * information: Portions Copyright [yyyy] [name of copyright owner] |
| * |
| * CDDL HEADER END |
| */ |
| |
| /* |
| * Copyright (c) 2010, Oracle and/or its affiliates. All rights reserved. |
| */ |
| |
| /* |
| * SMBFS I/O Daemon (SMF service) |
| */ |
| |
| #include <sys/types.h> |
| #include <sys/stat.h> |
| #include <sys/note.h> |
| #include <sys/queue.h> |
| |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <signal.h> |
| #include <stdarg.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <strings.h> |
| #include <stdlib.h> |
| #include <synch.h> |
| #include <time.h> |
| #include <unistd.h> |
| #include <ucred.h> |
| #include <wait.h> |
| #include <priv_utils.h> |
| #include <err.h> |
| #include <door.h> |
| #include <libscf.h> |
| #include <locale.h> |
| #include <thread.h> |
| #include <assert.h> |
| |
| #include <netsmb/smb_lib.h> |
| |
| static boolean_t d_flag = B_FALSE; |
| |
| /* Keep a list of child processes. */ |
| typedef struct _child { |
| LIST_ENTRY(_child) list; |
| pid_t pid; |
| uid_t uid; |
| } child_t; |
| static LIST_HEAD(, _child) child_list = { 0 }; |
| mutex_t cl_mutex = DEFAULTMUTEX; |
| |
| static const char smbiod_path[] = "/usr/lib/smbfs/smbiod"; |
| static const char door_path[] = SMBIOD_SVC_DOOR; |
| |
| void svc_dispatch(void *cookie, char *argp, size_t argsz, |
| door_desc_t *dp, uint_t n_desc); |
| static int cmd_start(uid_t uid, gid_t gid); |
| static int new_child(uid_t uid, gid_t gid); |
| static void svc_sigchld(void); |
| static void child_gone(uid_t, pid_t, int); |
| static void svc_cleanup(void); |
| |
| static child_t * |
| child_find_by_pid(pid_t pid) |
| { |
| child_t *cp; |
| |
| assert(MUTEX_HELD(&cl_mutex)); |
| LIST_FOREACH(cp, &child_list, list) { |
| if (cp->pid == pid) |
| return (cp); |
| } |
| return (NULL); |
| } |
| |
| static child_t * |
| child_find_by_uid(uid_t uid) |
| { |
| child_t *cp; |
| |
| assert(MUTEX_HELD(&cl_mutex)); |
| LIST_FOREACH(cp, &child_list, list) { |
| if (cp->uid == uid) |
| return (cp); |
| } |
| return (NULL); |
| } |
| |
| /* |
| * Find out if the service is already running. |
| * Return: true, false. |
| */ |
| static boolean_t |
| already_running(void) |
| { |
| door_info_t info; |
| int fd, rc; |
| |
| if ((fd = open(door_path, O_RDONLY)) < 0) |
| return (B_FALSE); |
| |
| rc = door_info(fd, &info); |
| close(fd); |
| if (rc < 0) |
| return (B_FALSE); |
| |
| return (B_TRUE); |
| } |
| |
| /* |
| * This function will fork off a child process, |
| * from which only the child will return. |
| * |
| * The parent exit status is taken as the SMF start method |
| * success or failure, so the parent waits (via pipe read) |
| * for the child to finish initialization before it exits. |
| * Use SMF error codes only on exit. |
| */ |
| static int |
| daemonize_init(void) |
| { |
| int pid, st; |
| int pfds[2]; |
| |
| chdir("/"); |
| |
| if (pipe(pfds) < 0) { |
| perror("pipe"); |
| exit(SMF_EXIT_ERR_FATAL); |
| } |
| if ((pid = fork1()) == -1) { |
| perror("fork"); |
| exit(SMF_EXIT_ERR_FATAL); |
| } |
| |
| /* |
| * If we're the parent process, wait for either the child to send us |
| * the appropriate exit status over the pipe or for the read to fail |
| * (presumably with 0 for EOF if our child terminated abnormally). |
| * If the read fails, exit with either the child's exit status if it |
| * exited or with SMF_EXIT_ERR_FATAL if it died from a fatal signal. |
| */ |
| if (pid != 0) { |
| /* parent */ |
| close(pfds[1]); |
| if (read(pfds[0], &st, sizeof (st)) == sizeof (st)) |
| _exit(st); |
| if (waitpid(pid, &st, 0) == pid && WIFEXITED(st)) |
| _exit(WEXITSTATUS(st)); |
| _exit(SMF_EXIT_ERR_FATAL); |
| } |
| |
| /* child */ |
| close(pfds[0]); |
| |
| return (pfds[1]); |
| } |
| |
| static void |
| daemonize_fini(int pfd, int rc) |
| { |
| /* Tell parent we're ready. */ |
| (void) write(pfd, &rc, sizeof (rc)); |
| close(pfd); |
| } |
| |
| int |
| main(int argc, char **argv) |
| { |
| sigset_t oldmask, tmpmask; |
| struct sigaction sa; |
| struct rlimit rl; |
| int door_fd = -1, tmp_fd = -1, pfd = -1; |
| int c, sig; |
| int rc = SMF_EXIT_ERR_FATAL; |
| boolean_t created = B_FALSE, attached = B_FALSE; |
| |
| /* set locale and text domain for i18n */ |
| (void) setlocale(LC_ALL, ""); |
| (void) textdomain(TEXT_DOMAIN); |
| |
| while ((c = getopt(argc, argv, "d")) != -1) { |
| switch (c) { |
| case 'd': |
| /* Do debug messages. */ |
| d_flag = B_TRUE; |
| break; |
| default: |
| fprintf(stderr, "Usage: %s [-d]\n", argv[0]); |
| return (SMF_EXIT_ERR_CONFIG); |
| } |
| } |
| |
| if (already_running()) { |
| fprintf(stderr, "%s: already running", argv[0]); |
| return (rc); |
| } |
| |
| /* |
| * Raise the fd limit to max |
| * errors here are non-fatal |
| */ |
| if (getrlimit(RLIMIT_NOFILE, &rl) != 0) { |
| fprintf(stderr, "getrlimit failed, err %d\n", errno); |
| } else if (rl.rlim_cur < rl.rlim_max) { |
| rl.rlim_cur = rl.rlim_max; |
| if (setrlimit(RLIMIT_NOFILE, &rl) != 0) |
| fprintf(stderr, "setrlimit " |
| "RLIMIT_NOFILE %d, err %d", |
| (int)rl.rlim_cur, errno); |
| } |
| |
| /* |
| * Want all signals blocked, as we're doing |
| * synchronous delivery via sigwait below. |
| */ |
| sigfillset(&tmpmask); |
| sigprocmask(SIG_BLOCK, &tmpmask, &oldmask); |
| |
| /* |
| * Do want SIGCHLD, and will waitpid(). |
| */ |
| sa.sa_flags = SA_NOCLDSTOP; |
| sa.sa_handler = SIG_DFL; |
| sigemptyset(&sa.sa_mask); |
| sigaction(SIGCHLD, &sa, NULL); |
| |
| /* |
| * Daemonize, unless debugging. |
| */ |
| if (d_flag) { |
| /* debug: run in foregound (not a service) */ |
| putenv("SMBFS_DEBUG=1"); |
| } else { |
| /* Non-debug: start daemon in the background. */ |
| pfd = daemonize_init(); |
| } |
| |
| /* |
| * Create directory for all smbiod doors. |
| */ |
| if ((mkdir(SMBIOD_RUNDIR, 0755) < 0) && errno != EEXIST) { |
| perror(SMBIOD_RUNDIR); |
| goto out; |
| } |
| |
| /* |
| * Create a file for the main service door. |
| */ |
| unlink(door_path); |
| tmp_fd = open(door_path, O_RDWR|O_CREAT|O_EXCL, 0644); |
| if (tmp_fd < 0) { |
| perror(door_path); |
| goto out; |
| } |
| close(tmp_fd); |
| tmp_fd = -1; |
| created = B_TRUE; |
| |
| /* Setup the door service. */ |
| door_fd = door_create(svc_dispatch, NULL, |
| DOOR_REFUSE_DESC | DOOR_NO_CANCEL); |
| if (door_fd == -1) { |
| perror("svc door_create"); |
| goto out; |
| } |
| fdetach(door_path); |
| if (fattach(door_fd, door_path) < 0) { |
| fprintf(stderr, "%s: fattach failed, %s\n", |
| door_path, strerror(errno)); |
| goto out; |
| } |
| attached = B_TRUE; |
| |
| /* |
| * Initializations done. Tell start method we're up. |
| */ |
| rc = SMF_EXIT_OK; |
| if (pfd != -1) { |
| daemonize_fini(pfd, rc); |
| pfd = -1; |
| } |
| |
| /* |
| * Main thread just waits for signals. |
| */ |
| again: |
| sig = sigwait(&tmpmask); |
| if (d_flag) |
| fprintf(stderr, "main: sig=%d\n", sig); |
| switch (sig) { |
| case SIGINT: |
| case SIGTERM: |
| /* |
| * The whole process contract gets a SIGTERM |
| * at once. Give children a chance to exit |
| * so we can do normal SIGCHLD cleanup. |
| * Prevent new door_open calls. |
| */ |
| fdetach(door_path); |
| attached = B_FALSE; |
| alarm(2); |
| goto again; |
| case SIGALRM: |
| break; /* normal termination */ |
| case SIGCHLD: |
| svc_sigchld(); |
| goto again; |
| case SIGCONT: |
| goto again; |
| default: |
| /* Unexpected signal. */ |
| fprintf(stderr, "svc_main: unexpected sig=%d\n", sig); |
| break; |
| } |
| |
| out: |
| if (attached) |
| fdetach(door_path); |
| if (door_fd != -1) |
| door_revoke(door_fd); |
| if (created) |
| unlink(door_path); |
| |
| /* NB: door threads gone now. */ |
| svc_cleanup(); |
| |
| /* If startup error, report to parent. */ |
| if (pfd != -1) |
| daemonize_fini(pfd, rc); |
| |
| return (rc); |
| } |
| |
| /*ARGSUSED*/ |
| void |
| svc_dispatch(void *cookie, char *argp, size_t argsz, |
| door_desc_t *dp, uint_t n_desc) |
| { |
| ucred_t *ucred = NULL; |
| uid_t uid; |
| gid_t gid; |
| int32_t cmd, rc; |
| |
| /* |
| * Allow a NULL arg call to check if this |
| * daemon is running. Just return zero. |
| */ |
| if (argp == NULL) { |
| rc = 0; |
| goto out; |
| } |
| |
| /* |
| * Get the caller's credentials. |
| * (from client side of door) |
| */ |
| if (door_ucred(&ucred) != 0) { |
| rc = EACCES; |
| goto out; |
| } |
| uid = ucred_getruid(ucred); |
| gid = ucred_getrgid(ucred); |
| |
| /* |
| * Arg is just an int command code. |
| * Reply is also an int. |
| */ |
| if (argsz != sizeof (cmd)) { |
| rc = EINVAL; |
| goto out; |
| } |
| bcopy(argp, &cmd, sizeof (cmd)); |
| switch (cmd) { |
| case SMBIOD_START: |
| rc = cmd_start(uid, gid); |
| break; |
| default: |
| rc = EINVAL; |
| goto out; |
| } |
| |
| out: |
| if (ucred != NULL) |
| ucred_free(ucred); |
| |
| door_return((void *)&rc, sizeof (rc), NULL, 0); |
| } |
| |
| /* |
| * Start a per-user smbiod, if not already running. |
| */ |
| int |
| cmd_start(uid_t uid, gid_t gid) |
| { |
| char door_file[64]; |
| child_t *cp; |
| int pid, fd = -1; |
| |
| mutex_lock(&cl_mutex); |
| cp = child_find_by_uid(uid); |
| if (cp != NULL) { |
| /* This UID already has an IOD. */ |
| mutex_unlock(&cl_mutex); |
| if (d_flag) { |
| fprintf(stderr, "cmd_start: uid %d" |
| " already has an iod\n", uid); |
| } |
| return (0); |
| } |
| |
| /* |
| * OK, create a new child. |
| */ |
| cp = malloc(sizeof (*cp)); |
| if (cp == NULL) { |
| mutex_unlock(&cl_mutex); |
| return (ENOMEM); |
| } |
| cp->pid = 0; /* update below */ |
| cp->uid = uid; |
| LIST_INSERT_HEAD(&child_list, cp, list); |
| mutex_unlock(&cl_mutex); |
| |
| /* |
| * The child will not have permission to create or |
| * destroy files in SMBIOD_RUNDIR so do that here. |
| */ |
| snprintf(door_file, sizeof (door_file), |
| SMBIOD_USR_DOOR, cp->uid); |
| unlink(door_file); |
| fd = open(door_file, O_RDWR|O_CREAT|O_EXCL, 0600); |
| if (fd < 0) { |
| perror(door_file); |
| goto errout; |
| } |
| if (fchown(fd, uid, gid) < 0) { |
| perror(door_file); |
| goto errout; |
| } |
| close(fd); |
| fd = -1; |
| |
| if ((pid = fork1()) == -1) { |
| perror("fork"); |
| goto errout; |
| } |
| if (pid == 0) { |
| (void) new_child(uid, gid); |
| _exit(1); |
| } |
| /* parent */ |
| cp->pid = pid; |
| |
| if (d_flag) { |
| fprintf(stderr, "cmd_start: uid %d new iod, pid %d\n", |
| uid, pid); |
| } |
| |
| return (0); |
| |
| errout: |
| if (fd != -1) |
| close(fd); |
| mutex_lock(&cl_mutex); |
| LIST_REMOVE(cp, list); |
| mutex_unlock(&cl_mutex); |
| free(cp); |
| return (errno); |
| } |
| |
| /* |
| * Assume the passed credentials (from the door client), |
| * drop any extra privileges, and exec the per-user iod. |
| */ |
| static int |
| new_child(uid_t uid, gid_t gid) |
| { |
| char *argv[2]; |
| int flags, rc; |
| |
| flags = PU_RESETGROUPS | PU_LIMITPRIVS | PU_INHERITPRIVS; |
| rc = __init_daemon_priv(flags, uid, gid, PRIV_NET_ACCESS, NULL); |
| if (rc != 0) |
| return (errno); |
| |
| argv[0] = "smbiod"; |
| argv[1] = NULL; |
| (void) execv(smbiod_path, argv); |
| return (errno); |
| } |
| |
| static void |
| svc_sigchld(void) |
| { |
| child_t *cp; |
| pid_t pid; |
| int err, status, found = 0; |
| |
| mutex_lock(&cl_mutex); |
| |
| while ((pid = waitpid(-1, &status, WNOHANG)) > 0) { |
| |
| found++; |
| if (d_flag) |
| fprintf(stderr, "svc_sigchld: pid %d\n", (int)pid); |
| |
| cp = child_find_by_pid(pid); |
| if (cp == NULL) { |
| fprintf(stderr, "Unknown pid %d\n", (int)pid); |
| continue; |
| } |
| child_gone(cp->uid, cp->pid, status); |
| LIST_REMOVE(cp, list); |
| free(cp); |
| } |
| err = errno; |
| |
| mutex_unlock(&cl_mutex); |
| |
| /* ECHILD is the normal end of loop. */ |
| if (pid < 0 && err != ECHILD) |
| fprintf(stderr, "svc_sigchld: waitpid err %d\n", err); |
| if (found == 0) |
| fprintf(stderr, "svc_sigchld: no children?\n"); |
| } |
| |
| static void |
| child_gone(uid_t uid, pid_t pid, int status) |
| { |
| char door_file[64]; |
| int x; |
| |
| if (d_flag) |
| fprintf(stderr, "child_gone: uid %d pid %d\n", |
| uid, (int)pid); |
| |
| snprintf(door_file, sizeof (door_file), |
| SMBIOD_RUNDIR "/%d", uid); |
| unlink(door_file); |
| |
| if (WIFEXITED(status)) { |
| x = WEXITSTATUS(status); |
| if (x != 0) { |
| fprintf(stderr, |
| "uid %d, pid %d exit %d\n", |
| uid, (int)pid, x); |
| } |
| } |
| if (WIFSIGNALED(status)) { |
| x = WTERMSIG(status); |
| fprintf(stderr, |
| "uid %d, pid %d signal %d\n", |
| uid, (int)pid, x); |
| } |
| } |
| |
| /* |
| * Final cleanup before exit. Unlink child doors, etc. |
| * Called while single threaded, so no locks needed here. |
| * The list is normally empty by now due to svc_sigchld |
| * calls during shutdown. But in case there were any |
| * straglers, do cleanup here. Don't bother freeing any |
| * list elements here, as we're exiting. |
| */ |
| static void |
| svc_cleanup(void) |
| { |
| child_t *cp; |
| |
| LIST_FOREACH(cp, &child_list, list) { |
| child_gone(cp->uid, cp->pid, 0); |
| } |
| } |