Files
sudo/lib/iolog/iolog_fileio.c
Todd C. Miller 71339c574f Check the return value of fcntl() when setting FD_CLOEXEC.
This should never fail unless the fd is invalid.
Problem reported by Matthias Gerstner of SUSE.
2021-01-06 10:16:00 -07:00

1062 lines
25 KiB
C

/*
* SPDX-License-Identifier: ISC
*
* Copyright (c) 2009-2020 Todd C. Miller <Todd.Miller@sudo.ws>
*
* 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.
*/
/*
* This is an open source non-commercial project. Dear PVS-Studio, please check it.
* PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
*/
#include <config.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef HAVE_STDBOOL_H
# include <stdbool.h>
#else
# include "compat/stdbool.h"
#endif
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include "pathnames.h"
#include "sudo_compat.h"
#include "sudo_conf.h"
#include "sudo_debug.h"
#include "sudo_eventlog.h"
#include "sudo_fatal.h"
#include "sudo_gettext.h"
#include "sudo_iolog.h"
#include "sudo_json.h"
#include "sudo_queue.h"
#include "sudo_util.h"
static unsigned char const gzip_magic[2] = {0x1f, 0x8b};
static unsigned int sessid_max = SESSID_MAX;
static mode_t iolog_filemode = S_IRUSR|S_IWUSR;
static mode_t iolog_dirmode = S_IRWXU;
static uid_t iolog_uid = ROOT_UID;
static gid_t iolog_gid = ROOT_GID;
static bool iolog_gid_set;
static bool iolog_compress;
static bool iolog_flush;
/*
* Set effective user and group-IDs to iolog_uid and iolog_gid.
* If restore flag is set, swap them back.
*/
static bool
io_swapids(bool restore)
{
#ifdef HAVE_SETEUID
static uid_t user_euid = (uid_t)-1;
static gid_t user_egid = (gid_t)-1;
debug_decl(io_swapids, SUDO_DEBUG_UTIL);
if (user_euid == (uid_t)-1)
user_euid = geteuid();
if (user_egid == (gid_t)-1)
user_egid = getegid();
if (restore) {
if (seteuid(user_euid) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to restore effective uid to %d", __func__,
(int)user_euid);
sudo_warn("seteuid() %d -> %d", (int)iolog_uid, (int)user_euid);
debug_return_bool(false);
}
if (setegid(user_egid) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to restore effective gid to %d", __func__,
(int)user_egid);
sudo_warn("setegid() %d -> %d", (int)iolog_gid, (int)user_egid);
debug_return_bool(false);
}
} else {
/* Fail silently if the user has insufficient privileges. */
if (setegid(iolog_gid) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to set effective gid to %d", __func__,
(int)iolog_gid);
debug_return_bool(false);
}
if (seteuid(iolog_uid) == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to set effective uid to %d", __func__,
(int)iolog_uid);
debug_return_bool(false);
}
}
debug_return_bool(true);
#else
return false;
#endif
}
/*
* Create directory and any parent directories as needed.
*/
static bool
iolog_mkdirs(char *path)
{
mode_t omask;
struct stat sb;
int dfd;
bool ok = true, uid_changed = false;
debug_decl(iolog_mkdirs, SUDO_DEBUG_UTIL);
dfd = open(path, O_RDONLY|O_NONBLOCK);
if (dfd == -1 && errno == EACCES) {
/* Try again as the I/O log owner (for NFS). */
if (io_swapids(false)) {
dfd = open(path, O_RDONLY|O_NONBLOCK);
if (!io_swapids(true)) {
ok = false;
goto done;
}
}
}
if (dfd != -1 && fstat(dfd, &sb) != -1) {
if (S_ISDIR(sb.st_mode)) {
if (sb.st_uid != iolog_uid || sb.st_gid != iolog_gid) {
if (fchown(dfd, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to chown %d:%d %s", __func__,
(int)iolog_uid, (int)iolog_gid, path);
}
}
if ((sb.st_mode & ALLPERMS) != iolog_dirmode) {
if (fchmod(dfd, iolog_dirmode) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to chmod 0%o %s", __func__,
(int)iolog_dirmode, path);
}
}
} else {
sudo_warnx(U_("%s exists but is not a directory (0%o)"),
path, (unsigned int) sb.st_mode);
ok = false;
}
goto done;
}
/* umask must not be more restrictive than the file modes. */
omask = umask(ACCESSPERMS & ~(iolog_filemode|iolog_dirmode));
ok = sudo_mkdir_parents(path, iolog_uid, iolog_gid, iolog_dirmode, true);
if (!ok && errno == EACCES) {
/* Try again as the I/O log owner (for NFS). */
uid_changed = io_swapids(false);
if (uid_changed)
ok = sudo_mkdir_parents(path, -1, -1, iolog_dirmode, false);
}
if (ok) {
/* Create final path component. */
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
"mkdir %s, mode 0%o", path, (unsigned int) iolog_dirmode);
ok = mkdir(path, iolog_dirmode) == 0 || errno == EEXIST;
if (!ok) {
if (errno == EACCES && !uid_changed) {
/* Try again as the I/O log owner (for NFS). */
uid_changed = io_swapids(false);
if (uid_changed)
ok = mkdir(path, iolog_dirmode) == 0 || errno == EEXIST;
}
if (!ok)
sudo_warn(U_("unable to mkdir %s"), path);
} else {
if (chown(path, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to chown %d:%d %s", __func__,
(int)iolog_uid, (int)iolog_gid, path);
}
}
}
if (uid_changed) {
if (!io_swapids(true))
ok = false;
}
umask(omask);
done:
if (dfd != -1)
close(dfd);
debug_return_bool(ok);
}
/*
* Create temporary directory and any parent directories as needed.
*/
bool
iolog_mkdtemp(char *path)
{
bool ok, uid_changed = false;
debug_decl(iolog_mkdtemp, SUDO_DEBUG_UTIL);
ok = sudo_mkdir_parents(path, iolog_uid, iolog_gid, iolog_dirmode, true);
if (!ok && errno == EACCES) {
/* Try again as the I/O log owner (for NFS). */
uid_changed = io_swapids(false);
if (uid_changed)
ok = sudo_mkdir_parents(path, -1, -1, iolog_dirmode, false);
}
if (ok) {
/* Create final path component. */
sudo_debug_printf(SUDO_DEBUG_DEBUG|SUDO_DEBUG_LINENO,
"mkdtemp %s", path);
/* We cannot retry mkdtemp() so always open as iolog user */
if (!uid_changed)
uid_changed = io_swapids(false);
if (mkdtemp(path) == NULL) {
sudo_warn(U_("unable to mkdir %s"), path);
ok = false;
} else {
if (chmod(path, iolog_dirmode) != 0) {
sudo_warn(U_("unable to change mode of %s to 0%o"),
path, (unsigned int)iolog_dirmode);
}
}
}
if (uid_changed) {
if (!io_swapids(true))
ok = false;
}
debug_return_bool(ok);
}
/*
* Like rename(2) but changes UID as needed.
*/
bool
iolog_rename(const char *from, const char *to)
{
bool ok, uid_changed = false;
debug_decl(iolog_rename, SUDO_DEBUG_UTIL);
ok = rename(from, to) == 0;
if (!ok && errno == EACCES) {
uid_changed = io_swapids(false);
if (uid_changed)
ok = rename(from, to) == 0;
}
if (uid_changed) {
if (!io_swapids(true))
ok = false;
}
debug_return_bool(ok);
}
/*
* Reset I/O log settings to default values.
*/
void
iolog_set_defaults(void)
{
sessid_max = SESSID_MAX;
iolog_filemode = S_IRUSR|S_IWUSR;
iolog_dirmode = S_IRWXU;
iolog_uid = ROOT_UID;
iolog_gid = ROOT_GID;
iolog_gid_set = false;
iolog_compress = false;
iolog_flush = false;
}
/*
* Set max sequence number (aka session ID)
*/
void
iolog_set_maxseq(unsigned int newval)
{
debug_decl(iolog_set_maxseq, SUDO_DEBUG_UTIL);
/* Clamp to SESSID_MAX as documented. */
if (newval > SESSID_MAX)
newval = SESSID_MAX;
sessid_max = newval;
debug_return;
}
/*
* Set iolog_uid (and iolog_gid if gid not explicitly set).
*/
void
iolog_set_owner(uid_t uid, gid_t gid)
{
debug_decl(iolog_set_owner, SUDO_DEBUG_UTIL);
iolog_uid = uid;
if (!iolog_gid_set)
iolog_gid = gid;
debug_return;
}
/*
* Set iolog_gid.
*/
void
iolog_set_gid(gid_t gid)
{
debug_decl(iolog_set_gid, SUDO_DEBUG_UTIL);
iolog_gid = gid;
iolog_gid_set = true;
debug_return;
}
/*
* Set iolog_filemode and iolog_dirmode.
*/
void
iolog_set_mode(mode_t mode)
{
debug_decl(iolog_set_mode, SUDO_DEBUG_UTIL);
/* I/O log files must be readable and writable by owner. */
iolog_filemode = S_IRUSR|S_IWUSR;
/* Add in group and other read/write if specified. */
iolog_filemode |= mode & (S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH);
/* For directory mode, add execute bits as needed. */
iolog_dirmode = iolog_filemode | S_IXUSR;
if (iolog_dirmode & (S_IRGRP|S_IWGRP))
iolog_dirmode |= S_IXGRP;
if (iolog_dirmode & (S_IROTH|S_IWOTH))
iolog_dirmode |= S_IXOTH;
debug_return;
}
/*
* Set iolog_compress
*/
void
iolog_set_compress(bool newval)
{
debug_decl(iolog_set_compress, SUDO_DEBUG_UTIL);
iolog_compress = newval;
debug_return;
}
/*
* Set iolog_flush
*/
void
iolog_set_flush(bool newval)
{
debug_decl(iolog_set_flush, SUDO_DEBUG_UTIL);
iolog_flush = newval;
debug_return;
}
/*
* Wrapper for openat(2) that sets umask and retries as iolog_uid/iolog_gid
* if openat(2) returns EACCES.
*/
int
iolog_openat(int dfd, const char *path, int flags)
{
int fd;
mode_t omask = S_IRWXG|S_IRWXO;
debug_decl(iolog_openat, SUDO_DEBUG_UTIL);
if (ISSET(flags, O_CREAT)) {
/* umask must not be more restrictive than the file modes. */
omask = umask(ACCESSPERMS & ~(iolog_filemode|iolog_dirmode));
}
fd = openat(dfd, path, flags, iolog_filemode);
if (fd == -1 && errno == EACCES) {
/* Enable write bit if it is missing. */
struct stat sb;
if (fstatat(dfd, path, &sb, 0) == 0) {
mode_t write_bits = iolog_filemode & (S_IWUSR|S_IWGRP|S_IWOTH);
if ((sb.st_mode & write_bits) != write_bits) {
if (fchmodat(dfd, path, iolog_filemode, 0) == 0)
fd = openat(dfd, path, flags, iolog_filemode);
}
}
}
if (fd == -1 && errno == EACCES) {
/* Try again as the I/O log owner (for NFS). */
if (io_swapids(false)) {
fd = openat(dfd, path, flags, iolog_filemode);
if (!io_swapids(true)) {
/* io_swapids() warns on error. */
if (fd != -1) {
close(fd);
fd = -1;
}
}
}
}
if (ISSET(flags, O_CREAT))
umask(omask);
debug_return_int(fd);
}
/*
* Read the on-disk sequence number, set sessid to the next
* number, and update the on-disk copy.
* Uses file locking to avoid sequence number collisions.
*/
bool
iolog_nextid(char *iolog_dir, char sessid[7])
{
char buf[32], *ep;
int i, len, fd = -1;
unsigned long id = 0;
ssize_t nread;
bool ret = false;
char pathbuf[PATH_MAX];
static const char b36char[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
debug_decl(iolog_nextid, SUDO_DEBUG_UTIL);
/*
* Create I/O log directory if it doesn't already exist.
*/
if (!iolog_mkdirs(iolog_dir))
goto done;
/*
* Open sequence file
*/
len = snprintf(pathbuf, sizeof(pathbuf), "%s/seq", iolog_dir);
if (len < 0 || len >= ssizeof(pathbuf)) {
errno = ENAMETOOLONG;
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: %s/seq", __func__, iolog_dir);
goto done;
}
fd = iolog_openat(AT_FDCWD, pathbuf, O_RDWR|O_CREAT);
if (fd == -1) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to open %s", __func__, pathbuf);
goto done;
}
if (!sudo_lock_file(fd, SUDO_LOCK)) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"unable to lock %s", pathbuf);
goto done;
}
if (fchown(fd, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to fchown %d:%d %s", __func__,
(int)iolog_uid, (int)iolog_gid, pathbuf);
}
/* Read current seq number (base 36). */
nread = read(fd, buf, sizeof(buf) - 1);
if (nread != 0) {
if (nread == -1) {
goto done;
}
if (buf[nread - 1] == '\n')
nread--;
buf[nread] = '\0';
id = strtoul(buf, &ep, 36);
if (ep == buf || *ep != '\0' || id >= sessid_max) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO,
"%s: bad sequence number: %s", pathbuf, buf);
id = 0;
}
}
id++;
/*
* Convert id to a string and stash in sessid.
* Note that that least significant digits go at the end of the string.
*/
for (i = 5; i >= 0; i--) {
buf[i] = b36char[id % 36];
id /= 36;
}
buf[6] = '\n';
/* Stash id for logging purposes. */
memcpy(sessid, buf, 6);
sessid[6] = '\0';
/* Rewind and overwrite old seq file, including the NUL byte. */
#ifdef HAVE_PWRITE
if (pwrite(fd, buf, 7, 0) != 7) {
#else
if (lseek(fd, 0, SEEK_SET) == -1 || write(fd, buf, 7) != 7) {
#endif
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to write %s", __func__, pathbuf);
goto done;
}
ret = true;
done:
if (fd != -1)
close(fd);
debug_return_bool(ret);
}
/*
* Create path and any intermediate directories.
* If path ends in 'XXXXXX', use mkdtemp().
*/
bool
iolog_mkpath(char *path)
{
size_t len;
bool ret;
debug_decl(iolog_mkpath, SUDO_DEBUG_UTIL);
/*
* Create path and intermediate subdirs as needed.
* If path ends in at least 6 Xs (ala POSIX mktemp), use mkdtemp().
* Sets iolog_gid (if it is not already set) as a side effect.
*/
len = strlen(path);
if (len >= 6 && strcmp(&path[len - 6], "XXXXXX") == 0)
ret = iolog_mkdtemp(path);
else
ret = iolog_mkdirs(path);
sudo_debug_printf(SUDO_DEBUG_INFO|SUDO_DEBUG_LINENO, "iolog path %s", path);
debug_return_bool(ret);
}
/*
* Append suffix to pathbuf after len chars and open the resulting file.
* Note that the size of pathbuf is assumed to be PATH_MAX.
* Stores the open file handle which has the close-on-exec flag set.
* XXX - move enabled logic into caller?
*/
bool
iolog_open(struct iolog_file *iol, int dfd, int iofd, const char *mode)
{
int flags;
const char *file;
unsigned char magic[2];
debug_decl(iolog_open, SUDO_DEBUG_UTIL);
if (mode[0] == 'r') {
flags = mode[1] == '+' ? O_RDWR : O_RDONLY;
} else if (mode[0] == 'w') {
flags = O_CREAT|O_TRUNC;
flags |= mode[1] == '+' ? O_RDWR : O_WRONLY;
} else {
sudo_debug_printf(SUDO_DEBUG_ERROR,
"%s: invalid I/O mode %s", __func__, mode);
debug_return_bool(false);
}
if ((file = iolog_fd_to_name(iofd)) == NULL) {
sudo_debug_printf(SUDO_DEBUG_ERROR,
"%s: invalid iofd %d", __func__, iofd);
debug_return_bool(false);
}
iol->writable = false;
iol->compressed = false;
if (iol->enabled) {
int fd = iolog_openat(dfd, file, flags);
if (fd != -1) {
if (*mode == 'w') {
if (fchown(fd, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to fchown %d:%d %s", __func__,
(int)iolog_uid, (int)iolog_gid, file);
}
iol->compressed = iolog_compress;
} else {
/* check for gzip magic number */
if (pread(fd, magic, sizeof(magic), 0) == ssizeof(magic)) {
if (magic[0] == gzip_magic[0] && magic[1] == gzip_magic[1])
iol->compressed = true;
}
}
if (fcntl(fd, F_SETFD, FD_CLOEXEC) != -1) {
#ifdef HAVE_ZLIB_H
if (iol->compressed)
iol->fd.g = gzdopen(fd, mode);
else
#endif
iol->fd.f = fdopen(fd, mode);
}
if (iol->fd.v != NULL) {
switch ((flags & O_ACCMODE)) {
case O_WRONLY:
case O_RDWR:
iol->writable = true;
break;
}
} else {
int save_errno = errno;
close(fd);
errno = save_errno;
fd = -1;
}
}
if (fd == -1) {
iol->enabled = false;
debug_return_bool(false);
}
} else {
if (*mode == 'w') {
/* Remove old log file in case we recycled sequence numbers. */
(void)unlinkat(dfd, file, 0);
}
}
debug_return_bool(true);
}
#ifdef HAVE_ZLIB_H
static const char *
gzstrerror(gzFile file)
{
const char *errstr;
int errnum;
errstr = gzerror(file, &errnum);
if (errnum == Z_ERRNO)
errstr = strerror(errno);
return errstr;
}
#endif /* HAVE_ZLIB_H */
/*
* Close an I/O log.
*/
bool
iolog_close(struct iolog_file *iol, const char **errstr)
{
bool ret = true;
debug_decl(iolog_close, SUDO_DEBUG_UTIL);
#ifdef HAVE_ZLIB_H
if (iol->compressed) {
int errnum;
/* Must check error indicator before closing. */
if (iol->writable) {
if (gzflush(iol->fd.g, Z_SYNC_FLUSH) != Z_OK) {
ret = false;
if (errstr != NULL)
*errstr = gzstrerror(iol->fd.g);
}
}
errnum = gzclose(iol->fd.g);
if (ret && errnum != Z_OK) {
ret = false;
if (errstr != NULL)
*errstr = errnum == Z_ERRNO ? strerror(errno) : "unknown error";
}
} else
#endif
if (fclose(iol->fd.f) != 0) {
ret = false;
if (errstr != NULL)
*errstr = strerror(errno);
}
debug_return_bool(ret);
}
/*
* I/O log wrapper for fseek/gzseek.
*/
off_t
iolog_seek(struct iolog_file *iol, off_t offset, int whence)
{
off_t ret;
//debug_decl(iolog_seek, SUDO_DEBUG_UTIL);
#ifdef HAVE_ZLIB_H
if (iol->compressed)
ret = gzseek(iol->fd.g, offset, whence);
else
#endif
ret = fseeko(iol->fd.f, offset, whence);
//debug_return_off_t(ret);
return ret;
}
/*
* I/O log wrapper for rewind/gzrewind.
*/
void
iolog_rewind(struct iolog_file *iol)
{
debug_decl(iolog_rewind, SUDO_DEBUG_UTIL);
#ifdef HAVE_ZLIB_H
if (iol->compressed)
(void)gzrewind(iol->fd.g);
else
#endif
rewind(iol->fd.f);
debug_return;
}
/*
* Read from a (possibly compressed) I/O log file.
*/
ssize_t
iolog_read(struct iolog_file *iol, void *buf, size_t nbytes,
const char **errstr)
{
ssize_t nread;
debug_decl(iolog_read, SUDO_DEBUG_UTIL);
if (nbytes > UINT_MAX) {
errno = EINVAL;
if (errstr != NULL)
*errstr = strerror(errno);
debug_return_ssize_t(-1);
}
#ifdef HAVE_ZLIB_H
if (iol->compressed) {
if ((nread = gzread(iol->fd.g, buf, nbytes)) == -1) {
if (errstr != NULL)
*errstr = gzstrerror(iol->fd.g);
}
} else
#endif
{
nread = (ssize_t)fread(buf, 1, nbytes, iol->fd.f);
if (nread == 0 && ferror(iol->fd.f)) {
nread = -1;
if (errstr != NULL)
*errstr = strerror(errno);
}
}
debug_return_ssize_t(nread);
}
/*
* Write to an I/O log, optionally compressing.
*/
ssize_t
iolog_write(struct iolog_file *iol, const void *buf, size_t len,
const char **errstr)
{
ssize_t ret;
debug_decl(iolog_write, SUDO_DEBUG_UTIL);
if (len > UINT_MAX) {
errno = EINVAL;
if (errstr != NULL)
*errstr = strerror(errno);
debug_return_ssize_t(-1);
}
#ifdef HAVE_ZLIB_H
if (iol->compressed) {
ret = gzwrite(iol->fd.g, (const voidp)buf, len);
if (ret == 0) {
ret = -1;
if (errstr != NULL)
*errstr = gzstrerror(iol->fd.g);
goto done;
}
if (iolog_flush) {
if (gzflush(iol->fd.g, Z_SYNC_FLUSH) != Z_OK) {
ret = -1;
if (errstr != NULL)
*errstr = gzstrerror(iol->fd.g);
goto done;
}
}
} else
#endif
{
ret = fwrite(buf, 1, len, iol->fd.f);
if (ret == 0) {
ret = -1;
if (errstr != NULL)
*errstr = strerror(errno);
goto done;
}
if (iolog_flush) {
if (fflush(iol->fd.f) != 0) {
ret = -1;
if (errstr != NULL)
*errstr = strerror(errno);
goto done;
}
}
}
done:
debug_return_ssize_t(ret);
}
/*
* Returns true if at end of I/O log file, else false.
*/
bool
iolog_eof(struct iolog_file *iol)
{
bool ret;
debug_decl(iolog_eof, SUDO_DEBUG_UTIL);
#ifdef HAVE_ZLIB_H
if (iol->compressed)
ret = gzeof(iol->fd.g) == 1;
else
#endif
ret = feof(iol->fd.f) == 1;
debug_return_int(ret);
}
void
iolog_clearerr(struct iolog_file *iol)
{
debug_decl(iolog_eof, SUDO_DEBUG_UTIL);
#ifdef HAVE_ZLIB_H
if (iol->compressed)
gzclearerr(iol->fd.g);
else
#endif
clearerr(iol->fd.f);
debug_return;
}
/*
* Like gets() but for struct iolog_file.
*/
char *
iolog_gets(struct iolog_file *iol, char *buf, size_t nbytes,
const char **errstr)
{
char *str;
debug_decl(iolog_gets, SUDO_DEBUG_UTIL);
if (nbytes > UINT_MAX) {
errno = EINVAL;
if (errstr != NULL)
*errstr = strerror(errno);
debug_return_str(NULL);
}
#ifdef HAVE_ZLIB_H
if (iol->compressed) {
if ((str = gzgets(iol->fd.g, buf, nbytes)) == NULL) {
if (errstr != NULL)
*errstr = gzstrerror(iol->fd.g);
}
} else
#endif
{
if ((str = fgets(buf, nbytes, iol->fd.f)) == NULL) {
if (errstr != NULL)
*errstr = strerror(errno);
}
}
debug_return_str(str);
}
/*
* Write the legacy I/O log file that contains the user and command info.
* This file is not compressed.
*/
static bool
iolog_write_info_file_legacy(int dfd, struct eventlog *evlog)
{
char * const *av;
FILE *fp;
int error, fd;
debug_decl(iolog_info_write_log, SUDO_DEBUG_UTIL);
fd = iolog_openat(dfd, "log", O_CREAT|O_TRUNC|O_WRONLY);
if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to open %s/log", evlog->iolog_path);
if (fd != -1)
close(fd);
debug_return_bool(false);
}
if (fchown(fd, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to fchown %d:%d %s/log", __func__,
(int)iolog_uid, (int)iolog_gid, evlog->iolog_path);
}
fprintf(fp, "%lld:%s:%s:%s:%s:%d:%d\n%s\n",
(long long)evlog->submit_time.tv_sec,
evlog->submituser ? evlog->submituser : "unknown",
evlog->runuser ? evlog->runuser : RUNAS_DEFAULT,
evlog->rungroup ? evlog->rungroup : "",
evlog->ttyname ? evlog->ttyname : "unknown",
evlog->lines, evlog->columns,
evlog->cwd ? evlog->cwd : "unknown");
fputs(evlog->command ? evlog->command : "unknown", fp);
for (av = evlog->argv + 1; *av != NULL; av++) {
fputc(' ', fp);
fputs(*av, fp);
}
fputc('\n', fp);
fflush(fp);
if ((error = ferror(fp))) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to write to I/O log file %s/log", evlog->iolog_path);
}
fclose(fp);
debug_return_bool(!error);
}
/*
* Write the "log.json" file that contains the user and command info.
* This file is not compressed.
*/
static bool
iolog_write_info_file_json(int dfd, struct eventlog *evlog)
{
struct json_container json;
struct json_value json_value;
bool ret = false;
FILE *fp = NULL;
int fd = -1;
debug_decl(iolog_write_info_file_json, SUDO_DEBUG_UTIL);
if (!sudo_json_init(&json, 4, false, false))
debug_return_bool(false);
/* Timestamp */
if (!sudo_json_open_object(&json, "timestamp"))
goto oom;
json_value.type = JSON_NUMBER;
json_value.u.number = evlog->submit_time.tv_sec;
if (!sudo_json_add_value(&json, "seconds", &json_value))
goto oom;
json_value.type = JSON_NUMBER;
json_value.u.number = evlog->submit_time.tv_nsec;
if (!sudo_json_add_value(&json, "nanoseconds", &json_value))
goto oom;
if (!sudo_json_close_object(&json))
goto oom;
if (!eventlog_store_json(&json, evlog))
goto done;
fd = iolog_openat(dfd, "log.json", O_CREAT|O_TRUNC|O_WRONLY);
if (fd == -1 || (fp = fdopen(fd, "w")) == NULL) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to open %s/log.json", evlog->iolog_path);
goto done;
}
if (fchown(fd, iolog_uid, iolog_gid) != 0) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_ERRNO,
"%s: unable to fchown %d:%d %s/log", __func__,
(int)iolog_uid, (int)iolog_gid, evlog->iolog_path);
}
fd = -1;
fprintf(fp, "{%s\n}\n", sudo_json_get_buf(&json));
fflush(fp);
if (ferror(fp)) {
sudo_debug_printf(SUDO_DEBUG_ERROR|SUDO_DEBUG_LINENO|SUDO_DEBUG_ERRNO,
"unable to write to I/O log file %s/log.json", evlog->iolog_path);
goto done;
}
ret = true;
goto done;
oom:
sudo_warnx(U_("%s: %s"), __func__, U_("unable to allocate memory"));
done:
sudo_json_free(&json);
if (fp != NULL)
fclose(fp);
if (fd != -1)
close(fd);
debug_return_bool(ret);
}
/*
* Write the I/O log and log.json files that contain user and command info.
* These files are not compressed.
*/
bool
iolog_write_info_file(int dfd, struct eventlog *evlog)
{
debug_decl(iolog_write_info_file, SUDO_DEBUG_UTIL);
if (!iolog_write_info_file_legacy(dfd, evlog))
debug_return_bool(false);
if (!iolog_write_info_file_json(dfd, evlog))
debug_return_bool(false);
debug_return_bool(true);
}
/*
* Map IOFD_* -> name.
*/
const char *
iolog_fd_to_name(int iofd)
{
const char *ret;
debug_decl(iolog_fd_to_name, SUDO_DEBUG_UTIL);
switch (iofd) {
case IOFD_STDIN:
ret = "stdin";
break;
case IOFD_STDOUT:
ret = "stdout";
break;
case IOFD_STDERR:
ret = "stderr";
break;
case IOFD_TTYIN:
ret = "ttyin";
break;
case IOFD_TTYOUT:
ret = "ttyout";
break;
case IOFD_TIMING:
ret = "timing";
break;
default:
ret = "unknown";
sudo_debug_printf(SUDO_DEBUG_ERROR, "%s: unexpected iofd %d",
__func__, iofd);
break;
}
debug_return_const_str(ret);
}