git: 61a29eca550b - main - syslogd: Log messages using libcasper

From: Jake Freeland <jfree_at_FreeBSD.org>
Date: Wed, 27 Nov 2024 22:27:12 UTC
The branch main has been updated by jfree:

URL: https://cgit.FreeBSD.org/src/commit/?id=61a29eca550b80d179934a7198c41ad4d255f81c

commit 61a29eca550b80d179934a7198c41ad4d255f81c
Author:     Jake Freeland <jfree@FreeBSD.org>
AuthorDate: 2024-11-27 22:25:17 +0000
Commit:     Jake Freeland <jfree@FreeBSD.org>
CommitDate: 2024-11-27 22:25:17 +0000

    syslogd: Log messages using libcasper
    
    Some logging operations require access to external resources to
    complete. Logging to F_WALL requires on-demand access to the user
    accounting database. Logging to F_CONSOLE requires access to the
    console. Logging to F_PIPE prompts execution of a command outside
    of capability mode.
    
    These operations cannot be performed in capability mode, so the
    "p_open", "ttymsg", and "wallmsg" commands may be sent to libcasper to
    circumvent these limitations.
    
    Reviewed by:    markj
    Differential Revision:  https://reviews.freebsd.org/D41465
---
 usr.sbin/syslogd/Makefile             |   3 +-
 usr.sbin/syslogd/syslogd.c            |  77 ++++++-------
 usr.sbin/syslogd/syslogd.h            |  15 +++
 usr.sbin/syslogd/syslogd_cap.c        |   8 +-
 usr.sbin/syslogd/syslogd_cap.h        |  25 ++++
 usr.sbin/syslogd/syslogd_cap_config.c |  34 +++++-
 usr.sbin/syslogd/syslogd_cap_log.c    | 211 ++++++++++++++++++++++++++++++++++
 7 files changed, 330 insertions(+), 43 deletions(-)

diff --git a/usr.sbin/syslogd/Makefile b/usr.sbin/syslogd/Makefile
index 4088b2e03651..7b202c69f7f9 100644
--- a/usr.sbin/syslogd/Makefile
+++ b/usr.sbin/syslogd/Makefile
@@ -15,7 +15,8 @@ LIBADD=	util
 
 .if ${MK_CASPER} != "no"
 SRCS+=	syslogd_cap.c \
-	syslogd_cap_config.c
+	syslogd_cap_config.c \
+	syslogd_cap_log.c
 CFLAGS+= -DWITH_CASPER
 LIBADD+= cap_net casper nv
 .endif
diff --git a/usr.sbin/syslogd/syslogd.c b/usr.sbin/syslogd/syslogd.c
index 0c05884cc17d..fcf7d4747706 100644
--- a/usr.sbin/syslogd/syslogd.c
+++ b/usr.sbin/syslogd/syslogd.c
@@ -129,7 +129,6 @@
 #include "pathnames.h"
 #include "syslogd.h"
 #include "syslogd_cap.h"
-#include "ttymsg.h"
 
 const char *ConfFile = _PATH_LOGCONF;
 static const char *PidFile = _PATH_LOGPID;
@@ -337,12 +336,10 @@ static int	evaluate_prop_filter(const struct prop_filter *filter,
 static nvlist_t *prop_filter_compile(const char *);
 static void	parsemsg(const char *, char *);
 static void	printsys(char *);
-static int	p_open(const char *, pid_t *);
 static const char *ttymsg_check(struct iovec *, int, char *, int);
 static void	usage(void);
 static bool	validate(struct sockaddr *, const char *);
 static void	unmapped(struct sockaddr *);
-static void	wallmsg(struct filed *, struct iovec *, const int iovlen);
 static int	waitdaemon(int);
 static void	increase_rcvbuf(int);
 
@@ -1665,16 +1662,6 @@ dofsync(void)
 	needdofsync = false;
 }
 
-/*
- * List of iovecs to which entries can be appended.
- * Used for constructing the message to be logged.
- */
-struct iovlist {
-	struct iovec	iov[TTYMSG_IOV_MAX];
-	size_t		iovcnt;
-	size_t		totalsize;
-};
-
 static void
 iovlist_init(struct iovlist *il)
 {
@@ -1831,8 +1818,16 @@ fprintlog_write(struct filed *f, struct iovlist *il, int flags)
 		dprintf(" %s\n", f->f_pname);
 		iovlist_append(il, "\n");
 		if (f->f_procdesc == -1) {
-			if ((f->f_file = p_open(f->f_pname,
-			    &f->f_procdesc)) < 0) {
+			struct filed *f_in_list;
+			size_t i = 0;
+
+			STAILQ_FOREACH(f_in_list, &fhead, next) {
+				if (f_in_list == f)
+					break;
+				++i;
+			}
+			f->f_file = p_open(i, f->f_pname, &f->f_procdesc);
+			if (f->f_file < 0) {
 				logerror(f->f_pname);
 				break;
 			}
@@ -2073,9 +2068,12 @@ fprintlog_successive(struct filed *f, int flags)
  *
  *	Write the specified message to either the entire
  *	world, or a list of approved users.
+ *
+ * Note: This function is wrapped by cap_wallmsg() when Capsicum support is
+ * enabled so ttymsg() can be called.
  */
-static void
-wallmsg(struct filed *f, struct iovec *iov, const int iovlen)
+void
+wallmsg(const struct filed *f, struct iovec *iov, const int iovlen)
 {
 	static int reenter;			/* avoid calling ourselves */
 	struct utmpx *ut;
@@ -2091,10 +2089,8 @@ wallmsg(struct filed *f, struct iovec *iov, const int iovlen)
 			continue;
 		if (f->f_type == F_WALL) {
 			if ((p = ttymsg(iov, iovlen, ut->ut_line,
-			    TTYMSGTIME)) != NULL) {
-				errno = 0;	/* already in msg */
-				logerror(p);
-			}
+			    TTYMSGTIME)) != NULL)
+				dprintf("%s\n", p);
 			continue;
 		}
 		/* should we send the message to this user? */
@@ -2103,10 +2099,8 @@ wallmsg(struct filed *f, struct iovec *iov, const int iovlen)
 				break;
 			if (!strcmp(f->f_uname[i], ut->ut_user)) {
 				if ((p = ttymsg_check(iov, iovlen, ut->ut_line,
-				    TTYMSGTIME)) != NULL) {
-					errno = 0;	/* already in msg */
-					logerror(p);
-				}
+				    TTYMSGTIME)) != NULL)
+					dprintf("%s\n", p);
 				break;
 			}
 		}
@@ -2392,6 +2386,13 @@ parseconfigfile(FILE *cf, bool allow_includes, nvlist_t *nvl_conf)
 	return (nvl_conf);
 }
 
+/*
+ * Read configuration file and create filed entries for each line.
+ *
+ * Note: This function is wrapped by cap_readconfigfile() when Capsicum
+ * support is enabled so resources can be acquired outside of the security
+ * sandbox.
+ */
 nvlist_t *
 readconfigfile(const char *path)
 {
@@ -3433,15 +3434,18 @@ validate(struct sockaddr *sa, const char *hname)
 /*
  * Fairly similar to popen(3), but returns an open descriptor, as
  * opposed to a FILE *.
+ *
+ * Note: This function is wrapped by cap_p_open() when Capsicum support is
+ * enabled, which allows piped processes to run outside of the capability
+ * sandbox.
  */
-static int
+int
 p_open(const char *prog, int *rpd)
 {
 	struct sigaction act = { };
 	int pfd[2], pd;
 	pid_t pid;
 	char *argv[4]; /* sh -c cmd NULL */
-	char errmsg[200];
 
 	if (pipe(pfd) == -1)
 		return (-1);
@@ -3456,18 +3460,14 @@ p_open(const char *prog, int *rpd)
 		argv[1] = strdup("-c");
 		argv[2] = strdup(prog);
 		argv[3] = NULL;
-		if (argv[0] == NULL || argv[1] == NULL || argv[2] == NULL) {
-			logerror("strdup");
-			exit(1);
-		}
+		if (argv[0] == NULL || argv[1] == NULL || argv[2] == NULL)
+			err(1, "strdup");
 
 		alarm(0);
 		act.sa_handler = SIG_DFL;
 		for (size_t i = 0; i < nitems(sigcatch); ++i) {
-			if (sigaction(sigcatch[i], &act, NULL) == -1) {
-				logerror("sigaction");
-				exit(1);
-			}
+			if (sigaction(sigcatch[i], &act, NULL) == -1)
+				err(1, "sigaction");
 		}
 
 		dup2(pfd[0], STDIN_FILENO);
@@ -3490,11 +3490,8 @@ p_open(const char *prog, int *rpd)
 	 */
 	if (fcntl(pfd[1], F_SETFL, O_NONBLOCK) == -1) {
 		/* This is bad. */
-		(void)snprintf(errmsg, sizeof(errmsg),
-			       "Warning: cannot change pipe to PID %d to "
-			       "non-blocking behaviour.",
-			       (int)pid);
-		logerror(errmsg);
+		dprintf("Warning: cannot change pipe to PID %d to non-blocking"
+		    "behaviour.", pid);
 	}
 	*rpd = pd;
 	return (pfd[1]);
diff --git a/usr.sbin/syslogd/syslogd.h b/usr.sbin/syslogd/syslogd.h
index 16de03590ada..2d74c4d0ebbb 100644
--- a/usr.sbin/syslogd/syslogd.h
+++ b/usr.sbin/syslogd/syslogd.h
@@ -67,6 +67,7 @@
 #include <sys/nv.h>
 #include <sys/queue.h>
 #include <sys/time.h>
+#include <sys/uio.h>
 
 #define SYSLOG_NAMES
 #include <sys/syslog.h>
@@ -75,6 +76,8 @@
 #include <stdbool.h>
 #include <stdio.h>
 
+#include "ttymsg.h"
+
 #define	MAXLINE		8192		/* maximum line length */
 #define	MAXSVLINE	MAXLINE		/* maximum saved line length */
 #define	MAXUNAMES	20		/* maximum number of user names */
@@ -172,11 +175,23 @@ struct filed {
 	STAILQ_ENTRY(filed) next;		/* next in linked list */
 };
 
+/*
+ * List of iovecs to which entries can be appended.
+ * Used for constructing the message to be logged.
+ */
+struct iovlist {
+	struct iovec	iov[TTYMSG_IOV_MAX];
+	size_t		iovcnt;
+	size_t		totalsize;
+};
+
 extern const char *ConfFile;
 extern char LocalHostName[MAXHOSTNAMELEN];
 
 void closelogfiles(void);
 void logerror(const char *);
+int p_open(const char *, pid_t *);
 nvlist_t *readconfigfile(const char *);
+void wallmsg(const struct filed *, struct iovec *, const int);
 
 #endif /* !_SYSLOGD_H_ */
diff --git a/usr.sbin/syslogd/syslogd_cap.c b/usr.sbin/syslogd/syslogd_cap.c
index 6f64ee8878bf..7539e6b8661b 100644
--- a/usr.sbin/syslogd/syslogd_cap.c
+++ b/usr.sbin/syslogd/syslogd_cap.c
@@ -45,8 +45,14 @@ casper_command(const char *cmd, const nvlist_t *limits __unused,
 {
 	int error = EINVAL;
 
-	if (strcmp(cmd, "readconfigfile") == 0)
+	if (strcmp(cmd, "p_open") == 0)
+		error = casper_p_open(nvlin, nvlout);
+	else if (strcmp(cmd, "readconfigfile") == 0)
 		error = casper_readconfigfile(nvlin, nvlout);
+	else if (strcmp(cmd, "ttymsg") == 0)
+		error = casper_ttymsg(nvlin, nvlout);
+	else if (strcmp(cmd, "wallmsg") == 0)
+		error = casper_wallmsg(nvlin);
 
 	return (error);
 }
diff --git a/usr.sbin/syslogd/syslogd_cap.h b/usr.sbin/syslogd/syslogd_cap.h
index 24b448ff2352..420676aa72f2 100644
--- a/usr.sbin/syslogd/syslogd_cap.h
+++ b/usr.sbin/syslogd/syslogd_cap.h
@@ -47,8 +47,27 @@
 
 #include "syslogd.h"
 
+/*
+ * Information used to verify filed integrity when executing outside of the
+ * security sandbox.
+ */
+struct cap_filed {
+	size_t idx;
+	char pipe_cmd[MAXPATHLEN];
+	SLIST_ENTRY(cap_filed) next;
+};
+extern SLIST_HEAD(cfiled_list, cap_filed) cfiled_head;
+
+int cap_p_open(cap_channel_t *, size_t, const char *, int *);
 nvlist_t *cap_readconfigfile(cap_channel_t *, const char *);
+const char *cap_ttymsg(cap_channel_t *, struct iovec *, int, const char *, int);
+void cap_wallmsg(cap_channel_t *, const struct filed *, struct iovec *,
+    const int);
+
+int casper_p_open(nvlist_t *, nvlist_t *);
 int casper_readconfigfile(nvlist_t *, nvlist_t *);
+int casper_ttymsg(nvlist_t *, nvlist_t *);
+int casper_wallmsg(nvlist_t *);
 
 nvlist_t *filed_to_nvlist(const struct filed *);
 nvlist_t *prop_filter_to_nvlist(const struct prop_filter *pfilter);
@@ -58,8 +77,14 @@ struct prop_filter *nvlist_to_prop_filter(const nvlist_t *nvl_prop_filter);
 
 #else /* !WITH_CASPER */
 
+#define	cap_p_open(chan, f_idx, prog, rpd) \
+	p_open(prog, rpd)
 #define	cap_readconfigfile(chan, cf) \
 	readconfigfile(cf)
+#define	cap_ttymsg(chan, iov, iovcnt, line, tmout) \
+	ttymsg(iov, iovcnt, line, tmout)
+#define	cap_wallmsg(chan, f, iov, iovcnt) \
+	wallmsg(f, iov, iovcnt)
 
 #endif /* WITH_CASPER */
 
diff --git a/usr.sbin/syslogd/syslogd_cap_config.c b/usr.sbin/syslogd/syslogd_cap_config.c
index 366c035925d6..09d49b0a41b2 100644
--- a/usr.sbin/syslogd/syslogd_cap_config.c
+++ b/usr.sbin/syslogd/syslogd_cap_config.c
@@ -277,6 +277,9 @@ cap_readconfigfile(cap_channel_t *chan, const char *path)
 int
 casper_readconfigfile(nvlist_t *nvlin, nvlist_t *nvlout)
 {
+	const nvlist_t * const *filed_list;
+	nvlist_t *nvl_conf;
+	size_t n_fileds;
 	const char *path;
 
 	/*
@@ -291,6 +294,35 @@ casper_readconfigfile(nvlist_t *nvlin, nvlist_t *nvlout)
 	strlcpy(LocalHostName, nvlist_get_string(nvlin, "LocalHostName"),
 	    sizeof(LocalHostName));
 
-	nvlist_move_nvlist(nvlout, "nvl_conf", readconfigfile(path));
+	nvl_conf = readconfigfile(path);
+
+	/* Remove old filed data in case we are reloading. */
+	while (!SLIST_EMPTY(&cfiled_head)) {
+		struct cap_filed *cfiled;
+
+		cfiled = SLIST_FIRST(&cfiled_head);
+		SLIST_REMOVE_HEAD(&cfiled_head, next);
+		free(cfiled);
+	}
+	/* Record F_PIPE filed data for use in p_open(). */
+	if (!nvlist_exists_nvlist_array(nvl_conf, "filed_list"))
+		return (0);
+	filed_list = nvlist_get_nvlist_array(nvl_conf, "filed_list", &n_fileds);
+	for (size_t i = 0; i < n_fileds; ++i) {
+		if (nvlist_get_number(filed_list[i], "f_type") == F_PIPE) {
+			struct cap_filed *cfiled;
+			const char *pipe_cmd;
+
+			cfiled = malloc(sizeof(*cfiled));
+			if (cfiled == NULL)
+				err(1, "malloc");
+			cfiled->idx = i;
+			pipe_cmd = nvlist_get_string(filed_list[i], "f_pname");
+			strlcpy(cfiled->pipe_cmd, pipe_cmd, sizeof(cfiled->pipe_cmd));
+			SLIST_INSERT_HEAD(&cfiled_head, cfiled, next);
+		}
+	}
+
+	nvlist_move_nvlist(nvlout, "nvl_conf", nvl_conf);
 	return (0);
 }
diff --git a/usr.sbin/syslogd/syslogd_cap_log.c b/usr.sbin/syslogd/syslogd_cap_log.c
new file mode 100644
index 000000000000..0156cc6f6b6c
--- /dev/null
+++ b/usr.sbin/syslogd/syslogd_cap_log.c
@@ -0,0 +1,211 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 The FreeBSD Foundation
+ *
+ * This software was developed by Jake Freeland <jfree@FreeBSD.org>
+ * under sponsorship from the FreeBSD Foundation.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <err.h>
+#include <string.h>
+
+#include "syslogd_cap.h"
+
+struct cfiled_list cfiled_head;
+
+int
+cap_p_open(cap_channel_t *chan, size_t filed_idx, const char *prog,
+    int *procdesc)
+{
+	nvlist_t *nvl = nvlist_create(0);
+	int error, pipedesc_w;
+
+	nvlist_add_string(nvl, "cmd", "p_open");
+	nvlist_add_number(nvl, "filed_idx", filed_idx);
+	nvlist_add_string(nvl, "prog", prog);
+	nvl = cap_xfer_nvlist(chan, nvl);
+	if (nvl == NULL) {
+		logerror("Failed to xfer p_open nvlist");
+		exit(1);
+	}
+	error = nvlist_get_number(nvl, "error");
+	if (error != 0) {
+		errno = error;
+		logerror("Failed to open piped command");
+	}
+	pipedesc_w = dnvlist_take_descriptor(nvl, "pipedesc_w", -1);
+	*procdesc = dnvlist_take_descriptor(nvl, "procdesc", -1);
+
+	nvlist_destroy(nvl);
+	return (pipedesc_w);
+}
+
+int
+casper_p_open(nvlist_t *nvlin, nvlist_t *nvlout)
+{
+	struct cap_filed *cfiled;
+	size_t filed_idx;
+	int pipedesc_w, procdesc = -1;
+	const char *prog;
+
+	filed_idx = nvlist_get_number(nvlin, "filed_idx");
+	prog = nvlist_get_string(nvlin, "prog");
+	SLIST_FOREACH(cfiled, &cfiled_head, next) {
+		if (cfiled->idx != filed_idx)
+			continue;
+		if (strcmp(cfiled->pipe_cmd, prog) != 0)
+			return (-1);
+
+		pipedesc_w = p_open(prog, &procdesc);
+		if (pipedesc_w == -1)
+			return (-1);
+		nvlist_move_descriptor(nvlout, "pipedesc_w", pipedesc_w);
+		nvlist_move_descriptor(nvlout, "procdesc", procdesc);
+		return (0);
+	}
+
+	return (-1);
+}
+
+const char *
+cap_ttymsg(cap_channel_t *chan, struct iovec *iov, int iovcnt,
+    const char *line, int tmout)
+{
+	nvlist_t *nvl = nvlist_create(0);
+	int error;
+	static char errbuf[1024];
+	char *ret = NULL;
+
+	nvlist_add_string(nvl, "cmd", "ttymsg");
+	for (int i = 0; i < iovcnt; ++i)
+		nvlist_append_string_array(nvl, "iov_strs", iov[i].iov_base);
+	nvlist_add_string(nvl, "line", line);
+	nvlist_add_number(nvl, "tmout", tmout);
+
+	nvl = cap_xfer_nvlist(chan, nvl);
+	if (nvl == NULL) {
+		logerror("Failed to xfer ttymsg nvlist");
+		exit(1);
+	}
+	error = nvlist_get_number(nvl, "error");
+	if (error != 0) {
+		errno = error;
+		logerror("Failed to ttymsg");
+	}
+	if (nvlist_exists_string(nvl, "errstr")) {
+		const char *errstr = nvlist_get_string(nvl, "errstr");
+		(void)strlcpy(errbuf, errstr, sizeof(errbuf));
+		ret = errbuf;
+	}
+
+	nvlist_destroy(nvl);
+	return (ret);
+}
+
+int
+casper_ttymsg(nvlist_t *nvlin, nvlist_t *nvlout)
+{
+	char **nvlstrs;
+	struct iovec *iov;
+	size_t iovcnt;
+	int tmout;
+	const char *line;
+
+	nvlstrs = nvlist_take_string_array(nvlin, "iov_strs", &iovcnt);
+	assert(iovcnt <= TTYMSG_IOV_MAX);
+	iov = calloc(iovcnt, sizeof(*iov));
+	if (iov == NULL)
+		err(EXIT_FAILURE, "calloc");
+	for (size_t i = 0; i < iovcnt; ++i) {
+		iov[i].iov_base = nvlstrs[i];
+		iov[i].iov_len = strlen(nvlstrs[i]);
+	}
+	line = nvlist_get_string(nvlin, "line");
+	tmout = nvlist_get_number(nvlin, "tmout");
+	line = ttymsg(iov, iovcnt, line, tmout);
+	if (line != NULL)
+		nvlist_add_string(nvlout, "errstr", line);
+
+	free(iov);
+	return (0);
+}
+
+void
+cap_wallmsg(cap_channel_t *chan, const struct filed *f, struct iovec *iov,
+    int iovcnt)
+{
+	nvlist_t *nvl = nvlist_create(0);
+	int error;
+
+	nvlist_add_string(nvl, "cmd", "wallmsg");
+	/*
+	 * The filed_to_nvlist() function is not needed
+	 * here because wallmsg() only uses f_type and
+	 * fu_uname members, which are both inline.
+	 */
+	nvlist_add_binary(nvl, "filed", f, sizeof(*f));
+	for (int i = 0; i < iovcnt; ++i)
+		nvlist_append_string_array(nvl, "iov_strs", iov[i].iov_base);
+
+	nvl = cap_xfer_nvlist(chan, nvl);
+	if (nvl == NULL) {
+		logerror("Failed to xfer wallmsg nvlist");
+		exit(1);
+	}
+	error = nvlist_get_number(nvl, "error");
+	if (error != 0) {
+		errno = error;
+		logerror("Failed to wallmsg");
+	}
+	nvlist_destroy(nvl);
+}
+
+int
+casper_wallmsg(nvlist_t *nvlin)
+{
+	const struct filed *f;
+	char **nvlstrs;
+	struct iovec *iov;
+	size_t sz;
+
+	f = nvlist_get_binary(nvlin, "filed", &sz);
+	assert(sz == sizeof(*f));
+	nvlstrs = nvlist_take_string_array(nvlin, "iov_strs", &sz);
+	assert(sz <= TTYMSG_IOV_MAX);
+	iov = calloc(sz, sizeof(*iov));
+	if (iov == NULL)
+		err(EXIT_FAILURE, "calloc");
+	for (size_t i = 0; i < sz; ++i) {
+		iov[i].iov_base = nvlstrs[i];
+		iov[i].iov_len = strlen(nvlstrs[i]);
+	}
+	wallmsg(f, iov, sz);
+
+	for (size_t i = 0; i < sz; ++i)
+		free(iov[i].iov_base);
+	free(iov);
+	return (0);
+}