/*
* 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",
uid, (int)pid, x);
}
}
if (WIFSIGNALED(status)) {
x = WTERMSIG(status);
fprintf(stderr,
"uid %d, pid %d signal %d",
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);
}
}