sudo_debug.c   [plain text]


/*
 * Copyright (c) 2011-2016 Todd C. Miller <Todd.Miller@courtesan.com>
 *
 * 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 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 <config.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/uio.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STRING_H
# include <string.h>
#endif /* HAVE_STRING_H */
#ifdef HAVE_STRINGS_H
# include <strings.h>
#endif /* HAVE_STRINGS_H */
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <time.h>

#define DEFAULT_TEXT_DOMAIN	"sudo"
#include "sudo_gettext.h"	/* must be included before sudo_compat.h */

#include "sudo_compat.h"
#include "sudo_fatal.h"
#include "sudo_plugin.h"
#include "sudo_debug.h"
#include "sudo_conf.h"
#include "sudo_util.h"

/*
 * The debug priorities and subsystems are currently hard-coded.
 * In the future we might consider allowing plugins to register their
 * own subsystems and provide direct access to the debugging API.
 */

/* Note: this must match the order in sudo_debug.h */
static const char *const sudo_debug_priorities[] = {
    "crit",
    "err",
    "warn",
    "notice",
    "diag",
    "info",
    "trace",
    "debug",
    NULL
};

/* Note: this must match the order in sudo_debug.h */
static const char *const sudo_debug_default_subsystems[] = {
    "args",
    "conv",
    "edit",
    "event",
    "exec",
    "hooks",
    "main",
    "netif",
    "pcomm",
    "plugin",
    "pty",
    "selinux",
    "util",
    "utmp",
    NULL
};

#define NUM_DEF_SUBSYSTEMS	(sizeof(sudo_debug_default_subsystems) / sizeof(sudo_debug_default_subsystems[0]) - 1)

/*
 * For multiple programs/plugins there is a per-program instance
 * and one or more outputs (files).
 */
struct sudo_debug_output {
    SLIST_ENTRY(sudo_debug_output) entries;
    char *filename;
    int *settings;
    int fd;
};
SLIST_HEAD(sudo_debug_output_list, sudo_debug_output);
struct sudo_debug_instance {
    char *program;
    const char *const *subsystems;
    const unsigned int *subsystem_ids;
    unsigned int max_subsystem;
    struct sudo_debug_output_list outputs;
};

/* Support up to 10 instances. */
#define SUDO_DEBUG_INSTANCE_MAX 10
static struct sudo_debug_instance *sudo_debug_instances[SUDO_DEBUG_INSTANCE_MAX];
static int sudo_debug_last_instance = -1;

static char sudo_debug_pidstr[(((sizeof(int) * 8) + 2) / 3) + 3];
static size_t sudo_debug_pidlen;

#define round_nfds(_n)	(((_n) + (4 * NBBY) - 1) & ~((4 * NBBY) - 1))
static int sudo_debug_fds_size;
static unsigned char *sudo_debug_fds;
static int sudo_debug_max_fd = -1;

/* Default instance index to use for common utility functions. */
static int sudo_debug_active_instance = -1;

/*
 * Free the specified output structure.
 */
static void
sudo_debug_free_output(struct sudo_debug_output *output)
{
    free(output->filename);
    free(output->settings);
    if (output->fd != -1)
	close(output->fd);
    free(output);
}

/*
 * Create a new output file for the specified debug instance.
 * Returns NULL if the file cannot be opened or memory cannot be allocated.
 */
static struct sudo_debug_output *
sudo_debug_new_output(struct sudo_debug_instance *instance,
    struct sudo_debug_file *debug_file)
{
    char *buf, *cp, *last, *subsys, *pri;
    struct sudo_debug_output *output;
    unsigned int j;
    int i;

    /* Create new output for the instance. */
    /* XXX - reuse fd for existing filename? */
    output = calloc(1, sizeof(*output));
    if (output == NULL)
	goto bad;
    output->fd = -1;
    output->settings = reallocarray(NULL, instance->max_subsystem + 1,
	sizeof(int));
    if (output->settings == NULL)
	goto bad;
    output->filename = strdup(debug_file->debug_file);
    if (output->filename == NULL)
	goto bad;
    output->fd = -1;

    /* Init per-subsystems settings to -1 since 0 is a valid priority. */
    for (j = 0; j <= instance->max_subsystem; j++)
	output->settings[j] = -1;

    /* Open debug file. */
    output->fd = open(output->filename, O_WRONLY|O_APPEND, S_IRUSR|S_IWUSR);
    if (output->fd == -1) {
	/* Create debug file as needed and set group ownership. */
	if (errno == ENOENT) {
	    output->fd = open(output->filename, O_WRONLY|O_APPEND|O_CREAT,
		S_IRUSR|S_IWUSR);
	}
	if (output->fd == -1)
	    goto bad;
	ignore_result(fchown(output->fd, (uid_t)-1, 0));
    }
    (void)fcntl(output->fd, F_SETFD, FD_CLOEXEC);
    if (sudo_debug_fds_size < output->fd) {
	/* Bump fds size to the next multiple of 4 * NBBY. */
	const int old_size = sudo_debug_fds_size / NBBY;
	const int new_size = round_nfds(output->fd + 1) / NBBY;
	unsigned char *new_fds;

	new_fds = realloc(sudo_debug_fds, new_size);
	if (new_fds == NULL)
	    goto bad;
	memset(new_fds + old_size, 0, new_size - old_size);
	sudo_debug_fds = new_fds;
	sudo_debug_fds_size = new_size * NBBY;
    }
    sudo_setbit(sudo_debug_fds, output->fd);
    if (output->fd > sudo_debug_max_fd)
	sudo_debug_max_fd = output->fd;

    /* Parse Debug conf string. */
    buf = strdup(debug_file->debug_flags);
    if (buf == NULL)
	goto bad;
    for ((cp = strtok_r(buf, ",", &last)); cp != NULL; (cp = strtok_r(NULL, ",", &last))) {
	/* Should be in the form subsys@pri. */
	subsys = cp;
	if ((pri = strchr(cp, '@')) == NULL)
	    continue;
	*pri++ = '\0';

	/* Look up priority and subsystem, fill in sudo_debug_settings[]. */
	for (i = 0; sudo_debug_priorities[i] != NULL; i++) {
	    if (strcasecmp(pri, sudo_debug_priorities[i]) == 0) {
		for (j = 0; instance->subsystems[j] != NULL; j++) {
		    if (strcasecmp(subsys, "all") == 0) {
			const unsigned int idx = instance->subsystem_ids ?
			    SUDO_DEBUG_SUBSYS(instance->subsystem_ids[j]) : j;
			if (i > output->settings[idx])
			    output->settings[idx] = i;
			continue;
		    }
		    if (strcasecmp(subsys, instance->subsystems[j]) == 0) {
			const unsigned int idx = instance->subsystem_ids ?
			    SUDO_DEBUG_SUBSYS(instance->subsystem_ids[j]) : j;
			if (i > output->settings[idx])
			    output->settings[idx] = i;
			break;
		    }
		}
		break;
	    }
	}
    }
    free(buf);

    return output;
bad:
    sudo_warn_nodebug(NULL);
    if (output != NULL)
	sudo_debug_free_output(output);
    return NULL;
}

/*
 * Register a program/plugin with the debug framework,
 * parses settings string from sudo.conf and opens debug_files.
 * If subsystem names are specified they override the default values.
 * NOTE: subsystems must not be freed by caller unless deregistered.
 * Sets the active instance to the newly registered instance.
 * Returns instance index on success or SUDO_DEBUG_INSTANCE_INITIALIZER
 * on failure.
 */
int
sudo_debug_register_v1(const char *program, const char *const subsystems[],
    unsigned int ids[], struct sudo_conf_debug_file_list *debug_files)
{
    struct sudo_debug_instance *instance = NULL;
    struct sudo_debug_output *output;
    struct sudo_debug_file *debug_file;
    int idx, free_idx = -1;
    debug_decl_func(sudo_debug_register);

    if (debug_files == NULL)
	return SUDO_DEBUG_INSTANCE_INITIALIZER;

    /* Use default subsystem names if none are provided. */
    if (subsystems == NULL) {
	subsystems = sudo_debug_default_subsystems;
    } else if (ids == NULL) {
	/* If subsystems are specified we must have ids[] too. */
	return SUDO_DEBUG_INSTANCE_INITIALIZER;
    }

    /* Search for existing instance. */
    for (idx = 0; idx <= sudo_debug_last_instance; idx++) {
	if (sudo_debug_instances[idx] == NULL) {
	    free_idx = idx;
	    continue;
	}
	if (sudo_debug_instances[idx]->subsystems == subsystems &&
	    strcmp(sudo_debug_instances[idx]->program, program) == 0) {
	    instance = sudo_debug_instances[idx];
	    break;
	}
    }

    if (instance == NULL) {
	unsigned int i, j, max_id = NUM_DEF_SUBSYSTEMS - 1;

	/* Fill in subsystem name -> id mapping as needed. */
	if (ids != NULL) {
	    for (i = 0; subsystems[i] != NULL; i++) {
		/* Check default subsystems. */
		for (j = 0; j < NUM_DEF_SUBSYSTEMS; j++) {
		    if (strcmp(subsystems[i], sudo_debug_default_subsystems[j]) == 0)
			break;
		}
		if (j == NUM_DEF_SUBSYSTEMS)
		    j = ++max_id;
		ids[i] = ((j + 1) << 6);
	    }
	}

	if (free_idx != -1)
	    idx = free_idx;
	if (idx == SUDO_DEBUG_INSTANCE_MAX) {
	    /* XXX - realloc? */
	    sudo_warnx_nodebug("too many debug instances (max %d)", SUDO_DEBUG_INSTANCE_MAX);
	    return SUDO_DEBUG_INSTANCE_INITIALIZER;
	}
	if (idx != sudo_debug_last_instance + 1 && idx != free_idx) {
	    sudo_warnx_nodebug("%s: instance number mismatch: expected %d or %d, got %d", __func__, sudo_debug_last_instance + 1, free_idx, idx);
	    return SUDO_DEBUG_INSTANCE_INITIALIZER;
	}
	if ((instance = malloc(sizeof(*instance))) == NULL)
	    return SUDO_DEBUG_INSTANCE_INITIALIZER;
	if ((instance->program = strdup(program)) == NULL) {
	    free(instance);
	    return SUDO_DEBUG_INSTANCE_INITIALIZER;
	}
	instance->subsystems = subsystems;
	instance->subsystem_ids = ids;
	instance->max_subsystem = max_id;
	SLIST_INIT(&instance->outputs);
	sudo_debug_instances[idx] = instance;
	if (idx != free_idx)
	    sudo_debug_last_instance++;
    } else {
	/* Check for matching instance but different ids[]. */
	if (ids != NULL && instance->subsystem_ids != ids) {
	    unsigned int i;

	    for (i = 0; subsystems[i] != NULL; i++)
		ids[i] = instance->subsystem_ids[i];
	}
    }

    TAILQ_FOREACH(debug_file, debug_files, entries) {
	output = sudo_debug_new_output(instance, debug_file);
	if (output != NULL)
	    SLIST_INSERT_HEAD(&instance->outputs, output, entries);
    }

    /* Set active instance. */
    sudo_debug_active_instance = idx;

    /* Stash the pid string so we only have to format it once. */
    if (sudo_debug_pidlen == 0) {
	(void)snprintf(sudo_debug_pidstr, sizeof(sudo_debug_pidstr), "[%d] ",
	    (int)getpid());
	sudo_debug_pidlen = strlen(sudo_debug_pidstr);
    }

    return idx;
}

/*
 * De-register the specified instance from the debug subsystem
 * and free up any associated data structures.
 */
int
sudo_debug_deregister_v1(int idx)
{
    struct sudo_debug_instance *instance;
    struct sudo_debug_output *output, *next;
    debug_decl_func(sudo_debug_deregister);

    if (idx < 0 || idx > sudo_debug_last_instance) {
	sudo_warnx_nodebug("%s: invalid instance ID %d, max %d",
	    __func__, idx, sudo_debug_last_instance);
	return -1;
    }
    /* Reset active instance as needed. */
    if (sudo_debug_active_instance == idx)
	sudo_debug_active_instance = -1;

    instance = sudo_debug_instances[idx];
    if (instance == NULL)
	return -1;		/* already deregistered */

    /* Free up instance data, note that subsystems[] is owned by caller. */
    sudo_debug_instances[idx] = NULL;
    SLIST_FOREACH_SAFE(output, &instance->outputs, entries, next) {
	close(output->fd);
	free(output->filename);
	free(output->settings);
	free(output);
    }
    free(instance->program);
    free(instance);

    if (idx == sudo_debug_last_instance)
	sudo_debug_last_instance--;

    return 0;
}

int
sudo_debug_get_instance_v1(const char *program)
{
    int idx;

    for (idx = 0; idx <= sudo_debug_last_instance; idx++) {
	if (sudo_debug_instances[idx] == NULL)
	    continue;
	if (strcmp(sudo_debug_instances[idx]->program, program) == 0)
	    return idx;
    }
    return SUDO_DEBUG_INSTANCE_INITIALIZER;
}

pid_t
sudo_debug_fork_v1(void)
{
    pid_t pid;

    if ((pid = fork()) == 0) {
	(void)snprintf(sudo_debug_pidstr, sizeof(sudo_debug_pidstr), "[%d] ",
	    (int)getpid());
	sudo_debug_pidlen = strlen(sudo_debug_pidstr);
    }

    return pid;
}

void
sudo_debug_enter_v1(const char *func, const char *file, int line,
    int subsys)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"-> %s @ %s:%d", func, file, line);
}

void
sudo_debug_exit_v1(const char *func, const char *file, int line,
    int subsys)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d", func, file, line);
}

void
sudo_debug_exit_int_v1(const char *func, const char *file, int line,
    int subsys, int rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %d", func, file, line, rval);
}

void
sudo_debug_exit_long_v1(const char *func, const char *file, int line,
    int subsys, long rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %ld", func, file, line, rval);
}

void
sudo_debug_exit_size_t_v1(const char *func, const char *file, int line,
    int subsys, size_t rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %zu", func, file, line, rval);
}

void
sudo_debug_exit_ssize_t_v1(const char *func, const char *file, int line,
    int subsys, ssize_t rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %zd", func, file, line, rval);
}

void
sudo_debug_exit_bool_v1(const char *func, const char *file, int line,
    int subsys, bool rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %s", func, file, line, rval ? "true" : "false");
}

void
sudo_debug_exit_str_v1(const char *func, const char *file, int line,
    int subsys, const char *rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %s", func, file, line, rval ? rval : "(null)");
}

void
sudo_debug_exit_str_masked_v1(const char *func, const char *file, int line,
    int subsys, const char *rval)
{
    static const char stars[] = "********************************************************************************";
    int len = rval ? strlen(rval) : sizeof("(null)") - 1;

    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %.*s", func, file, line, len, rval ? stars : "(null)");
}

void
sudo_debug_exit_ptr_v1(const char *func, const char *file, int line,
    int subsys, const void *rval)
{
    sudo_debug_printf2(NULL, NULL, 0, subsys | SUDO_DEBUG_TRACE,
	"<- %s @ %s:%d := %p", func, file, line, rval);
}

void
sudo_debug_write2_v1(int fd, const char *func, const char *file, int lineno,
    const char *str, int len, int errnum)
{
    char *timestr, numbuf[(((sizeof(int) * 8) + 2) / 3) + 2];
    time_t now;
    struct iovec iov[12];
    int iovcnt = 3;

    /* Prepend program name and pid with a trailing space. */
    iov[1].iov_base = (char *)getprogname();
    iov[1].iov_len = strlen(iov[1].iov_base);
    iov[2].iov_base = sudo_debug_pidstr;
    iov[2].iov_len = sudo_debug_pidlen;

    /* Add string, trimming any trailing newlines. */
    while (len > 0 && str[len - 1] == '\n')
	len--;
    if (len > 0) {
	iov[iovcnt].iov_base = (char *)str;
	iov[iovcnt].iov_len = len;
	iovcnt++;
    }

    /* Append error string if errno is specified. */
    if (errnum) {
	if (len > 0) {
	    iov[iovcnt].iov_base = ": ";
	    iov[iovcnt].iov_len = 2;
	    iovcnt++;
	}
	iov[iovcnt].iov_base = strerror(errnum);
	iov[iovcnt].iov_len = strlen(iov[iovcnt].iov_base);
	iovcnt++;
    }

    /* If function, file and lineno are specified, append them. */
    if (func != NULL && file != NULL && lineno != 0) {
	iov[iovcnt].iov_base = " @ ";
	iov[iovcnt].iov_len = 3;
	iovcnt++;

	iov[iovcnt].iov_base = (char *)func;
	iov[iovcnt].iov_len = strlen(func);
	iovcnt++;

	iov[iovcnt].iov_base = "() ";
	iov[iovcnt].iov_len = 3;
	iovcnt++;

	iov[iovcnt].iov_base = (char *)file;
	iov[iovcnt].iov_len = strlen(file);
	iovcnt++;

	(void)snprintf(numbuf, sizeof(numbuf), ":%d", lineno);
	iov[iovcnt].iov_base = numbuf;
	iov[iovcnt].iov_len = strlen(numbuf);
	iovcnt++;
    }

    /* Append newline. */
    iov[iovcnt].iov_base = "\n";
    iov[iovcnt].iov_len = 1;
    iovcnt++;

    /* Do timestamp last due to ctime's static buffer. */
    time(&now);
    timestr = ctime(&now) + 4;
    timestr[15] = ' ';	/* replace year with a space */
    timestr[16] = '\0';
    iov[0].iov_base = timestr;
    iov[0].iov_len = 16;

    /* Write message in a single syscall */
    ignore_result(writev(fd, iov, iovcnt));
}

void
sudo_debug_vprintf2_v1(const char *func, const char *file, int lineno, int level,
    const char *fmt, va_list ap)
{
    int buflen, pri, saved_errno = errno;
    unsigned int subsys;
    char static_buf[1024], *buf = static_buf;
    struct sudo_debug_instance *instance;
    struct sudo_debug_output *output;
    debug_decl_func(sudo_debug_vprintf2);

    if (sudo_debug_active_instance == -1)
	goto out;

    /* Extract priority and subsystem from level. */
    pri = SUDO_DEBUG_PRI(level);
    subsys = SUDO_DEBUG_SUBSYS(level);

    /* Find matching instance. */
    if (sudo_debug_active_instance > sudo_debug_last_instance) {
	sudo_warnx_nodebug("%s: invalid instance ID %d, max %d",
	    __func__, sudo_debug_active_instance, sudo_debug_last_instance);
	goto out;
    }
    instance = sudo_debug_instances[sudo_debug_active_instance];
    if (instance == NULL) {
	sudo_warnx_nodebug("%s: unregistered instance index %d", __func__,
	    sudo_debug_active_instance);
	goto out;
    }

    SLIST_FOREACH(output, &instance->outputs, entries) {
	/* Make sure we want debug info at this level. */
	if (subsys <= instance->max_subsystem && output->settings[subsys] >= pri) {
	    va_list ap2;

	    /* Operate on a copy of ap to support multiple outputs. */
	    va_copy(ap2, ap);
	    buflen = fmt ? vsnprintf(static_buf, sizeof(static_buf), fmt, ap2) : 0;
	    va_end(ap2);
	    if (buflen >= (int)sizeof(static_buf)) {
		va_list ap3;

		/* Not enough room in static buf, allocate dynamically. */
		va_copy(ap3, ap);
		buflen = vasprintf(&buf, fmt, ap3);
		va_end(ap3);
	    }
	    if (buflen != -1) {
		int errcode = ISSET(level, SUDO_DEBUG_ERRNO) ? saved_errno : 0;
		if (ISSET(level, SUDO_DEBUG_LINENO))
		    sudo_debug_write2(output->fd, func, file, lineno, buf, buflen, errcode);
		else
		    sudo_debug_write2(output->fd, NULL, NULL, 0, buf, buflen, errcode);
		if (buf != static_buf) {
		    free(buf);
		    buf = static_buf;
		}
	    }
	}
    }
out:
    errno = saved_errno;
}

#ifdef NO_VARIADIC_MACROS
void
sudo_debug_printf_nvm_v1(int pri, const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    sudo_debug_vprintf2(NULL, NULL, 0, pri, fmt, ap);
    va_end(ap);
}
#endif /* NO_VARIADIC_MACROS */

void
sudo_debug_printf2_v1(const char *func, const char *file, int lineno, int level,
    const char *fmt, ...)
{
    va_list ap;

    va_start(ap, fmt);
    sudo_debug_vprintf2(func, file, lineno, level, fmt, ap);
    va_end(ap);
}

#define EXEC_PREFIX "exec "

void
sudo_debug_execve2_v1(int level, const char *path, char *const argv[], char *const envp[])
{
    int buflen, pri, saved_errno = errno;
    unsigned int subsys;
    struct sudo_debug_instance *instance;
    struct sudo_debug_output *output;
    char * const *av;
    char *cp, static_buf[4096], *buf = static_buf;
    size_t plen;
    debug_decl_func(sudo_debug_execve2);

    if (sudo_debug_active_instance == -1)
	goto out;

    /* Extract priority and subsystem from level. */
    pri = SUDO_DEBUG_PRI(level);
    subsys = SUDO_DEBUG_SUBSYS(level);

    /* Find matching instance. */
    if (sudo_debug_active_instance > sudo_debug_last_instance) {
	sudo_warnx_nodebug("%s: invalid instance ID %d, max %d",
	    __func__, sudo_debug_active_instance, sudo_debug_last_instance);
	goto out;
    }
    instance = sudo_debug_instances[sudo_debug_active_instance];
    if (instance == NULL) {
	sudo_warnx_nodebug("%s: unregistered instance index %d", __func__,
	    sudo_debug_active_instance);
	goto out;
    }
    if (subsys > instance->max_subsystem)
	goto out;

    SLIST_FOREACH(output, &instance->outputs, entries) {
	bool log_envp = false;

	/* Make sure we want debug info at this level. */
	if (output->settings[subsys] < pri)
	    continue;

	/* Log envp for debug level "debug". */
	if (output->settings[subsys] >= SUDO_DEBUG_DEBUG - 1 && envp[0] != NULL)
	    log_envp = true;

	/* Alloc and build up buffer. */
	plen = strlen(path);
	buflen = sizeof(EXEC_PREFIX) -1 + plen;
	if (argv[0] != NULL) {
	    buflen += sizeof(" []") - 1;
	    for (av = argv; *av; av++)
		buflen += strlen(*av) + 1;
	    buflen--;
	}
	if (log_envp) {
	    buflen += sizeof(" []") - 1;
	    for (av = envp; *av; av++)
		buflen += strlen(*av) + 1;
	    buflen--;
	}
	if (buflen >= (int)sizeof(static_buf)) {
	    buf = malloc(buflen + 1);
	    if (buf == NULL)
		goto out;
	}

	/* Copy prefix and command. */
	memcpy(buf, EXEC_PREFIX, sizeof(EXEC_PREFIX) - 1);
	cp = buf + sizeof(EXEC_PREFIX) - 1;
	memcpy(cp, path, plen);
	cp += plen;

	/* Copy argv. */
	if (argv[0] != NULL) {
	    *cp++ = ' ';
	    *cp++ = '[';
	    for (av = argv; *av; av++) {
		size_t avlen = strlen(*av);
		memcpy(cp, *av, avlen);
		cp += avlen;
		*cp++ = ' ';
	    }
	    cp[-1] = ']';
	}

	if (log_envp) {
	    *cp++ = ' ';
	    *cp++ = '[';
	    for (av = envp; *av; av++) {
		size_t avlen = strlen(*av);
		memcpy(cp, *av, avlen);
		cp += avlen;
		*cp++ = ' ';
	    }
	    cp[-1] = ']';
	}

	*cp = '\0';

	sudo_debug_write(output->fd, buf, buflen, 0);
	if (buf != static_buf) {
	    free(buf);
	    buf = static_buf;
	}
    }
out:
    errno = saved_errno;
}

/*
 * Returns the active instance or SUDO_DEBUG_INSTANCE_INITIALIZER
 * if no instance is active.
 */
int
sudo_debug_get_active_instance_v1(void)
{
    return sudo_debug_active_instance;
}

/*
 * Sets a new active instance, returning the old one.
 * Note that the old instance may be SUDO_DEBUG_INSTANCE_INITIALIZER
 * if this is the only instance.
 */
int
sudo_debug_set_active_instance_v1(int idx)
{
    const int old_idx = sudo_debug_active_instance;

    if (idx >= -1 && idx <= sudo_debug_last_instance)
	sudo_debug_active_instance = idx;
    return old_idx;
}

/*
 * Replace the ofd with nfd in all outputs if present.
 * Also updates sudo_debug_fds.
 */
void
sudo_debug_update_fd_v1(int ofd, int nfd)
{
    int idx;

    if (ofd <= sudo_debug_max_fd && sudo_isset(sudo_debug_fds, ofd)) {
	/* Update sudo_debug_fds. */
	sudo_clrbit(sudo_debug_fds, ofd);
	sudo_setbit(sudo_debug_fds, nfd);

	/* Update the outputs. */
	for (idx = 0; idx <= sudo_debug_last_instance; idx++) {
	    struct sudo_debug_instance *instance;
	    struct sudo_debug_output *output;

	    instance = sudo_debug_instances[idx];
	    if (instance == NULL)
		continue;
	    SLIST_FOREACH(output, &instance->outputs, entries) {
		if (output->fd == ofd)
		    output->fd = nfd;
	    }
	}
    }
}

/*
 * Returns the highest debug output fd or -1 if no debug files open.
 * Fills in fds with the value of sudo_debug_fds.
 */
int
sudo_debug_get_fds_v1(unsigned char **fds)
{
    *fds = sudo_debug_fds;
    return sudo_debug_max_fd;
}