Commit Diff


commit - 70b68b95fb01cb2b260ca9a92849088b4ee34fb2
commit + 056b9939639819c1a2e85f0e73fc02dda5b27b78
blob - 004de43f19fb128200743738afea48b0a2631923
blob + 2d9b5159b468ad7de6156909a7d880704f13c4ca
--- regress/Makefile
+++ regress/Makefile
@@ -1,4 +1,4 @@
-SUBDIR=	agentcfg anthropic buf conf deploy diff editor http journal json log markdown \
+SUBDIR=	agentcfg anthropic buf conf deploy diff editor http imsgev journal json log markdown \
 	model_window openai output print sandbox skills sse term tools turn web xmalloc
 
 .include <bsd.subdir.mk>
blob - d397b20feb0f182630b200d95785c71f75775960
blob + 5d18e65afa8d09013140fc89fe12daa92eeb6006
--- regress/README
+++ regress/README
@@ -43,8 +43,9 @@ I9  privileges only narrow		sandbox (each role reaches
 					steady pledge before untrusted
 					input; forbidden syscalls trip
 					PLDG violations)
-I10 mutual distrust across roles	imsg length/type validation in
-					worker.c/coord.c; hub-and-spoke
+I10 mutual distrust across roles	imsgev (partially consumed payloads
+					fail closed); imsg length/type
+					validation in worker.c/coord.c; hub-and-spoke
 					authority map in proto.h; turn
 					(a hostile second terminal cannot
 					flip a committed turn; a tool
@@ -60,7 +61,8 @@ I12 no ambient privilege		no setuid, no daemon, no soc
 					(coord.c); tools proves anchored
 					process-group cleanup even after
 					forced executor death
-I13 length-aware strings		buf, json (NUL round-trip),
+I13 length-aware strings		buf, json (NUL round-trip), imsgev
+					(empty and embedded-NUL payload views),
 					sse (NUL transparency),
 					anthropic, openai, http, web, output
 					(NUL-bearing framing, headers, bodies,
@@ -171,7 +173,9 @@ src/fugu/agentcfg.c			agentcfg; turn (live schema/rout
 src/fugu/priv.c, coord.c		sandbox (spawn/handshake/teardown),
 					turn (lead and subagent turn loops)
 src/fugu/journal.c			journal, turn
-src/common/imsgev.c, worker.c		sandbox (worker runtime)
+src/common/imsgev.c			imsgev (whole-payload views); sandbox
+					(event runtime)
+src/common/worker.c			sandbox (worker runtime)
 src/fugu-api (request/stream)		turn (correlated concurrent lead/agent
 					requests), print (deadline/retry/long-key
 					reflection), openai (real requests + SSE)
blob - /dev/null
blob + 93314c1cf5366d4e2cbfa834d399e80834883a5f (mode 644)
--- /dev/null
+++ regress/imsgev/Makefile
@@ -0,0 +1,7 @@
+PROG=	imsgev_test
+SRCS=	imsgev_test.c imsgev.c log.c
+
+LDADD=	-levent -lutil
+DPADD=	${LIBEVENT} ${LIBUTIL}
+
+.include <bsd.regress.mk>
blob - /dev/null
blob + f2fdf7f0d1295eab089010d5678c8daf04235d2d (mode 644)
--- /dev/null
+++ regress/imsgev/imsgev_test.c
@@ -0,0 +1,97 @@
+/*
+ * Copyright (c) 2026 Isaac <isaac@itm.works>
+ *
+ * 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 <sys/types.h>
+#include <sys/socket.h>
+
+#include <err.h>
+#include <errno.h>
+#include <imsg.h>
+#include <stdint.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "imsgev.h"
+
+#include "regress.h"
+
+static void
+make_imsg(const void *data, size_t len, struct imsgbuf *receiver,
+    struct imsg *imsg)
+{
+	struct imsgbuf	 sender;
+	int		 fds[2];
+
+	if (socketpair(AF_UNIX, SOCK_STREAM, PF_UNSPEC, fds) == -1)
+		err(1, "socketpair");
+	if (imsgbuf_init(&sender, fds[0]) == -1)
+		err(1, "imsgbuf_init sender");
+	if (imsgbuf_init(receiver, fds[1]) == -1)
+		err(1, "imsgbuf_init receiver");
+	if (imsg_compose(&sender, 1, 0, -1, -1, data, len) == -1)
+		err(1, "imsg_compose");
+	if (imsgbuf_flush(&sender) == -1)
+		err(1, "imsgbuf_flush");
+	if (imsgbuf_read(receiver) != 1)
+		errx(1, "imsgbuf_read");
+	if (imsgbuf_get(receiver, imsg) != 1)
+		errx(1, "imsgbuf_get");
+	imsgbuf_clear(&sender);
+	close(fds[0]);
+	close(fds[1]);
+}
+
+int
+main(void)
+{
+	static const u_char	 binary[] = { 'a', '\0', 'b' };
+	struct imsgev_payload	 payload;
+	struct imsgbuf		 receiver;
+	struct imsg		 imsg;
+	u_char			 byte;
+
+	make_imsg(NULL, 0, &receiver, &imsg);
+	payload.data = (const u_char *)1;
+	payload.len = 1;
+	CHECK(imsgev_get_payload(&imsg, &payload) == 0);
+	CHECK(payload.data == NULL);
+	CHECK(payload.len == 0);
+	imsg_free(&imsg);
+	imsgbuf_clear(&receiver);
+
+	make_imsg(binary, sizeof(binary), &receiver, &imsg);
+	CHECK(imsgev_get_payload(&imsg, &payload) == 0);
+	CHECK(payload.len == sizeof(binary));
+	CHECK(payload.data != NULL &&
+	    memcmp(payload.data, binary, sizeof(binary)) == 0);
+	imsg_free(&imsg);
+	imsgbuf_clear(&receiver);
+
+	make_imsg(binary, sizeof(binary), &receiver, &imsg);
+	CHECK(imsg_get_buf(&imsg, &byte, sizeof(byte)) == 0);
+	CHECK(byte == 'a');
+	payload.data = (const u_char *)1;
+	payload.len = SIZE_MAX;
+	errno = 0;
+	CHECK(imsgev_get_payload(&imsg, &payload) == -1);
+	CHECK(errno == EBADMSG);
+	CHECK(payload.data == NULL);
+	CHECK(payload.len == 0);
+	imsg_free(&imsg);
+	imsgbuf_clear(&receiver);
+
+	REGRESS_END();
+}
blob - 2cee5e52e25388738c7402a78e90bdad830f7263
blob + eae76c222a9645b9bd3757daa9fcf0204a9b45c7
--- src/common/imsgev.c
+++ src/common/imsgev.c
@@ -17,6 +17,7 @@
 #include <sys/types.h>
 
 #include <event.h>
+#include <errno.h>
 #include <imsg.h>
 #include <stdint.h>
 
@@ -67,6 +68,34 @@ imsgev_compose(struct imsgev *iev, uint32_t type, uint
 	return (ret);
 }
 
+int
+imsgev_get_payload(struct imsg *imsg, struct imsgev_payload *payload)
+{
+	struct ibuf	 ib;
+	size_t		 len;
+
+	payload->data = NULL;
+	payload->len = 0;
+	if (imsg->hdr.len < IMSG_HEADER_SIZE) {
+		errno = EBADMSG;
+		return (-1);
+	}
+	len = imsg_get_len(imsg);
+	if (len != imsg->hdr.len - IMSG_HEADER_SIZE) {
+		errno = EBADMSG;
+		return (-1);
+	}
+	if (len == 0)
+		return (0);
+	if (imsg_get_ibuf(imsg, &ib) == -1 || ibuf_size(&ib) != len) {
+		errno = EBADMSG;
+		return (-1);
+	}
+	payload->data = ibuf_data(&ib);
+	payload->len = ibuf_size(&ib);
+	return (0);
+}
+
 void
 imsgev_clear(struct imsgev *iev)
 {
blob - 912e02e97f21c2bd74e1163842b466f0df041b0e
blob + c19e3d504553e550f77a3268b8d2a63617840730
--- src/common/imsgev.h
+++ src/common/imsgev.h
@@ -33,11 +33,22 @@ struct imsgev {
 	short		 events;
 };
 
+/*
+ * A whole, unconsumed imsg payload.  The bytes are borrowed until
+ * imsg_free().  An empty payload is a successful { NULL, 0 } view;
+ * malformed or previously consumed payloads fail with EBADMSG.
+ */
+struct imsgev_payload {
+	const u_char	*data;
+	size_t		 len;
+};
+
 void	imsgev_init(struct imsgev *, int, void (*)(int, short, void *),
 	    void *);
 void	imsgev_add(struct imsgev *);
 int	imsgev_compose(struct imsgev *, uint32_t, uint32_t, pid_t, int,
 	    const void *, size_t);
+int	imsgev_get_payload(struct imsg *, struct imsgev_payload *);
 void	imsgev_clear(struct imsgev *);
 
 #endif /* IMSGEV_H */
blob - 5b7aab176b5f31d13771bcc71ec0e14336c9c592
blob + 6a041592b8100a7964a10178d9a337589172090f
--- src/fugu/coord.c
+++ src/fugu/coord.c
@@ -469,19 +469,6 @@ msg_count(const struct msglist *l)
 	return (n);
 }
 
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf	 ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 static int
 protect_has(const struct buf *b, const char *path)
 {
@@ -1134,6 +1121,7 @@ send_ui_config(struct coord *c)
 static void
 stream_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 	uint32_t	 type = imsg_get_type(imsg);
@@ -1150,7 +1138,10 @@ stream_event(struct coord *c, struct imsg *imsg)
 	if (imsg_get_id(imsg) != c->lead_api_id)
 		fatalx("lead provider correlation mismatch");
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get provider stream payload");
+	p = payload.data;
+	len = payload.len;
 
 	switch (type) {
 	case FUGU_IMSG_A_TEXT:
@@ -1301,12 +1292,16 @@ stream_event(struct coord *c, struct imsg *imsg)
 static void
 tool_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
 	if (c->wait != WAIT_TOOL)
 		return;
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get tool result payload");
+	p = payload.data;
+	len = payload.len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_TOOL_OUT:
@@ -1349,10 +1344,14 @@ static void
 tool_ctl_event(struct coord *c, struct imsg *imsg)
 {
 	struct tool_turn_ack	 ack;
+	struct imsgev_payload	 payload;
 	const u_char		*p;
 	size_t			 len;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get tool Turn ACK payload");
+	p = payload.data;
+	len = payload.len;
 	if (c->tool_ctl_expected == 0 || p == NULL || len != sizeof(ack))
 		fatalx("unexpected tool Turn ACK");
 	memcpy(&ack, p, sizeof(ack));
@@ -1367,6 +1366,7 @@ tool_ctl_event(struct coord *c, struct imsg *imsg)
 static void
 web_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
@@ -1374,7 +1374,10 @@ web_event(struct coord *c, struct imsg *imsg)
 		return;
 	if (imsg_get_id(imsg) != c->web_id)
 		fatalx("web result correlation mismatch");
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get web result payload");
+	p = payload.data;
+	len = payload.len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_WEB_OUT:
@@ -1418,12 +1421,16 @@ web_event(struct coord *c, struct imsg *imsg)
 static void
 ctx_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
 	if (c->wait != WAIT_CTX)
 		return;
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get project context payload");
+	p = payload.data;
+	len = payload.len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_CTX_DATA:
@@ -1492,10 +1499,14 @@ configured_provider_name(const struct coord *c, int sl
 static void
 ui_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get UI input payload");
+	p = payload.data;
+	len = payload.len;
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_UI_SUBMIT:
 		/* A submit only lands while idle: mid-turn, the tty queues
@@ -1629,12 +1640,16 @@ ui_event(struct coord *c, struct imsg *imsg)
 static void
 model_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
 	if (!c->ui)
 		return;			/* line mode never asks (2.2) */
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get model listing payload");
+	p = payload.data;
+	len = payload.len;
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_MODEL:
 	{
@@ -1710,10 +1725,14 @@ model_event(struct coord *c, struct imsg *imsg)
 static void
 compose_event(struct coord *c, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get compose payload");
+	p = payload.data;
+	len = payload.len;
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_UI_COMPOSE:
 		if (len == 0)
@@ -1873,13 +1892,17 @@ agent_web_start_next(struct coord *c)
 static void
 agent_provider_event(struct subagent *a, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char *p;
 	size_t len;
 	uint32_t type = imsg_get_type(imsg);
 
 	if (!a->api_inflight)
 		fatalx("provider event for idle subagent");
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get relayed provider payload");
+	p = payload.data;
+	len = payload.len;
 	if (!a->done && imsgev_compose(&a->iev, type, 0, -1, -1, p, len) == -1)
 		fatal("relay provider event to subagent");
 	if (type == FUGU_IMSG_A_DONE || type == FUGU_IMSG_A_ERROR)
@@ -1892,6 +1915,7 @@ static void
 agent_web_event(struct subagent *a, struct imsg *imsg)
 {
 	struct coord *c = a->coord;
+	struct imsgev_payload payload;
 	const u_char *p;
 	size_t len;
 	uint32_t outtype;
@@ -1899,7 +1923,10 @@ agent_web_event(struct subagent *a, struct imsg *imsg)
 	if (!a->web_inflight || c->agent_web_active != a ||
 	    c->web_busy_id != a->web_id || imsg_get_id(imsg) != a->web_id)
 		fatalx("web event for idle subagent");
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get relayed web payload");
+	p = payload.data;
+	len = payload.len;
 	if (imsg_get_type(imsg) == FUGU_IMSG_WEB_OUT)
 		outtype = FUGU_IMSG_AGENT_WEB_OUT;
 	else if (imsg_get_type(imsg) == FUGU_IMSG_WEB_RESULT)
@@ -2244,6 +2271,7 @@ agent_dispatch(int fd, short event, void *arg)
 	struct coord *c = a->coord;
 	struct imsgbuf *ibuf = &a->iev.ibuf;
 	struct imsg imsg;
+	struct imsgev_payload payload;
 	const u_char *p;
 	struct tool_req req;
 	ssize_t n;
@@ -2293,7 +2321,10 @@ agent_dispatch(int fd, short event, void *arg)
 		if (n == 0)
 			break;
 		type = imsg_get_type(&imsg);
-		p = imsg_bytes(&imsg, &len);
+		if (imsgev_get_payload(&imsg, &payload) == -1)
+			fatal("get subagent broker payload");
+		p = payload.data;
+		len = payload.len;
 		switch (type) {
 		case FUGU_IMSG_READY:
 			if (len != 0 || a->ready)
blob - 0d4591c227b88282ec35a94f224dd3819ac44845
blob + 2935e1ff82c36ffc61949854b34bf9e6fe29eda1
--- src/fugu-api/main.c
+++ src/fugu-api/main.c
@@ -221,20 +221,6 @@ ca_path(void)
 	return (tls_default_ca_cert_file());
 }
 
-/* A read-only view of an imsg's payload; valid until imsg_free. */
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf	 ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 /* A key crosses the wire as trailing bytes; it feeds an HTTP header,
  * so it must be NUL-free (I13). */
 static char *
@@ -276,11 +262,15 @@ api_lockdown(struct worker *w, struct imsg *imsg)
 {
 	struct api		*a = w->ctx;
 	struct hello_api	 h;
+	struct imsgev_payload payload;
 	struct prov		*v;
 	const u_char		*p;
 	size_t			 len;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get API HELLO payload");
+	p = payload.data;
+	len = payload.len;
 	if (p == NULL || len < sizeof(h))
 		fatalx("fugu-api: short HELLO");
 	memcpy(&h, p, sizeof(h));
@@ -321,11 +311,15 @@ static void
 prov_add(struct api *a, struct imsg *imsg)
 {
 	struct hello_prov	 hp;
+	struct imsgev_payload	 payload;
 	struct prov		*v;
 	const u_char		*p;
 	size_t			 len;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get PROV payload");
+	p = payload.data;
+	len = payload.len;
 	if (p == NULL || len < sizeof(hp))
 		fatalx("fugu-api: short PROV");
 	memcpy(&hp, p, sizeof(hp));
@@ -1817,14 +1811,19 @@ api_dispatch(struct worker *w, struct imsg *imsg)
 {
 	struct api	*a = w->ctx;
 	struct api_request *r;
+	struct imsgev_payload payload;
 	size_t		 len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_REQUEST: {
-		const u_char	*p = imsg_bytes(imsg, &len);
+		const u_char	*p;
 		uint32_t	 id = imsg_get_id(imsg);
 		size_t		 cap, growth;
 
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get REQUEST payload");
+		p = payload.data;
+		len = payload.len;
 		if ((r = request_find(a, id)) == NULL)
 			r = request_new(w, id);
 		if (r->phase != REQ_ASSEMBLY && r->phase != REQ_REJECTED)
blob - fb3c99fc91d2262de753a404e38cb12639ff8994
blob + 7840fd7766fb54edcb5b70b8462764f98d184973
--- src/fugu-editor/main.c
+++ src/fugu-editor/main.c
@@ -53,20 +53,6 @@ static struct buf	 seed;		/* accumulated EDIT_SEED chu
 static volatile sig_atomic_t edit_child; /* running editor pgid */
 static char		 edit_path[64];	/* the live temp file, for SIGTERM cleanup */
 
-/* Borrow an imsg's payload bytes (length-checked by the caller). */
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf	ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 /* A coordinator death mid-compose must not orphan the user's editor or
  * leave the seeded temp file behind ("the temp file never survives"). */
 static void
@@ -311,12 +297,16 @@ compose(struct worker *w)
 static int
 edit_dispatch(struct worker *w, struct imsg *imsg)
 {
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_EDIT_SEED:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get EDIT_SEED payload");
+		p = payload.data;
+		len = payload.len;
 		if (p == NULL || seed.len + len > FUGU_EDIT_MAX)
 			fatalx("compose seed overrun");	/* I10, fail closed */
 		buf_add(&seed, p, len);
blob - d37bcaf8fb50b8ec2b9e2ed1e60476b21a7b9895
blob + 83c5b7c960279f612572e5e849cb4f2e3152ad0b
--- src/fugu-tool/agent.c
+++ src/fugu-tool/agent.c
@@ -229,19 +229,6 @@ agent_boot_prompt(struct agent_boot *b, const void *da
 	return (0);
 }
 
-static const u_char *
-frame_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 static void
 agent_send(struct agent_state *a, uint32_t type, const void *data, size_t len)
 {
@@ -416,6 +403,7 @@ agent_generation(struct agent_state *a, const struct t
 {
 	struct buf body;
 	struct imsg imsg;
+	struct imsgev_payload payload;
 	const u_char *p;
 	size_t len;
 	uint32_t type;
@@ -439,7 +427,10 @@ agent_generation(struct agent_state *a, const struct t
 
 	while (agent_next(a, &imsg)) {
 		type = imsg_get_type(&imsg);
-		p = frame_bytes(&imsg, &len);
+		if (imsgev_get_payload(&imsg, &payload) == -1)
+			fatal("get subagent provider payload");
+		p = payload.data;
+		len = payload.len;
 		switch (type) {
 		case FUGU_IMSG_A_TEXT:
 			if (len > FUGU_RESPONSE_MAX - a->text.len) {
@@ -564,6 +555,7 @@ agent_web(struct agent_state *a, struct agent_tcall *t
 	struct tool_req req;
 	struct tool_result result;
 	struct imsg imsg;
+	struct imsgev_payload payload;
 	const u_char *p;
 	size_t len;
 
@@ -576,7 +568,10 @@ agent_web(struct agent_state *a, struct agent_tcall *t
 	buf_reset(out);
 	*is_error = 1;
 	while (agent_next(a, &imsg)) {
-		p = frame_bytes(&imsg, &len);
+		if (imsgev_get_payload(&imsg, &payload) == -1)
+			fatal("get subagent web payload");
+		p = payload.data;
+		len = payload.len;
 		switch (imsg_get_type(&imsg)) {
 		case FUGU_IMSG_AGENT_WEB_OUT:
 			if (p == NULL || len == 0 || len > FUGU_WEB_OUT_MAX - out->len)
blob - b06198cd808cda70d20ee3537d1329ccc42fd8ce
blob + 2f07ba49a039d9992f3210584cd7ced3f6ef9042
--- src/fugu-tool/main.c
+++ src/fugu-tool/main.c
@@ -101,20 +101,6 @@ pump_jobs(int fd, short ev, void *arg)
 		job_pump(ts->ctx->jobs);
 }
 
-/* A read-only view of an imsg's payload; valid until imsg_free. */
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf	 ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 /*
  * Unveil the protect entries away with empty permissions (I5/I9).
  *
@@ -236,11 +222,15 @@ tool_lockdown(struct worker *w, struct imsg *imsg)
 {
 	struct tool_state	*ts = w->ctx;
 	struct hello_tool	 h;
+	struct imsgev_payload	 payload;
 	const u_char		*p;
 	size_t			 len;
 	int			 passed_fd, snapshot_fd, write_ok, net_ok;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get tool HELLO payload");
+	p = payload.data;
+	len = payload.len;
 	if (p == NULL || len < sizeof(h))
 		fatalx("fugu-tool: short HELLO");
 	memcpy(&h, p, sizeof(h));
@@ -399,22 +389,32 @@ static int
 tool_dispatch(struct worker *w, struct imsg *imsg)
 {
 	struct tool_state	*ts = w->ctx;
+	struct imsgev_payload	 payload;
 	const u_char		*p;
 	size_t			 len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_AGENT_START:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get AGENT_START payload");
+		p = payload.data;
+		len = payload.len;
 		if (!ts->agent || agent_boot_start(&ts->boot, p, len) == -1)
 			fatalx("fugu-tool: bad AGENT_START");
 		return (0);
 	case FUGU_IMSG_AGENT_SYSTEM:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get AGENT_SYSTEM payload");
+		p = payload.data;
+		len = payload.len;
 		if (!ts->agent || agent_boot_system(&ts->boot, p, len) == -1)
 			fatalx("fugu-tool: bad AGENT_SYSTEM");
 		return (0);
 	case FUGU_IMSG_AGENT_PROMPT:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get AGENT_PROMPT payload");
+		p = payload.data;
+		len = payload.len;
 		if (!ts->agent || agent_boot_prompt(&ts->boot, p, len) == -1)
 			fatalx("fugu-tool: bad AGENT_PROMPT");
 		return (0);
@@ -451,7 +451,10 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 	case FUGU_IMSG_TOOL_REQ: {
 		struct tool_req	 req;
 
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get TOOL_REQ payload");
+		p = payload.data;
+		len = payload.len;
 		if (p == NULL || len < sizeof(req))
 			fatalx("fugu-tool: short TOOL_REQ");
 		memcpy(&req, p, sizeof(req));
@@ -462,7 +465,10 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		return (0);
 	}
 	case FUGU_IMSG_TOOL_ARG:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get TOOL_ARG payload");
+		p = payload.data;
+		len = payload.len;
 		if (ts->arg.len + len > FUGU_TOOL_ARG_MAX)
 			fatalx("fugu-tool: tool input too large");
 		if (p != NULL && len > 0)
@@ -474,7 +480,10 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		size_t		 off;
 
 		/* the payload is a NUL-terminated path (coordinator-built) */
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get CTX_READ payload");
+		p = payload.data;
+		len = payload.len;
 		if (p == NULL || len == 0 || len >= sizeof(path) ||
 		    memchr(p, '\0', len) != NULL)
 			fatalx("fugu-tool: bad CTX_READ");
@@ -501,7 +510,10 @@ tool_dispatch(struct worker *w, struct imsg *imsg)
 		size_t			 plen;
 
 		/* path '\0' bullet-bytes */
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get CTX_APPEND payload");
+		p = payload.data;
+		len = payload.len;
 		if (p == NULL || (plen = strnlen((const char *)p, len)) == len)
 			fatalx("fugu-tool: bad CTX_APPEND");
 		path = (const char *)p;
blob - 28f63779f1fa42debfdf0de1872100134d910011
blob + a3bfff69f738ee02517ed6fd50a3bd136efdecc8
--- src/fugu-tty/ui.c
+++ src/fugu-tty/ui.c
@@ -162,20 +162,6 @@ struct ui {
 
 static struct ui	*UI;		/* for the atexit terminal restore */
 
-/* Borrow an imsg's payload bytes (length-checked by the caller). */
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf	ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 /* ---- small helpers -------------------------------------------------- */
 
 static int
@@ -1531,12 +1517,16 @@ int
 ui_render(struct worker *w, struct imsg *imsg)
 {
 	struct ui	*u = w->ctx;
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len;
 
 	if (u == NULL)
 		return (-1);
-	p = imsg_bytes(imsg, &len);	/* fetched once: the ibuf is consumed */
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get UI payload");
+	p = payload.data;
+	len = payload.len;
 
 	switch (imsg_get_type(imsg)) {
 	case FUGU_IMSG_UI_CONFIG:
blob - 7e6959a1391bfc8c861ed9768748cdee3b54e804
blob + efcc30a384f2600ea0ffd58567d501f97d1025dc
--- src/fugu-web/main.c
+++ src/fugu-web/main.c
@@ -58,19 +58,6 @@ struct web_state {
 
 static struct web_state *web_cleanup_state;
 
-static const u_char *
-imsg_bytes(struct imsg *imsg, size_t *lenp)
-{
-	struct ibuf ib;
-
-	if (imsg_get_len(imsg) == 0 || imsg_get_ibuf(imsg, &ib) == -1) {
-		*lenp = 0;
-		return (NULL);
-	}
-	*lenp = ibuf_size(&ib);
-	return (ibuf_data(&ib));
-}
-
 static const char *
 env_or(const char *name, const char *fallback)
 {
@@ -124,10 +111,14 @@ web_lockdown(struct worker *w, struct imsg *imsg)
 {
 	struct web_state	*ws = w->ctx;
 	struct hello_web	 h;
+	struct imsgev_payload payload;
 	const u_char	*p, *token, *allow, *block;
 	size_t		 len, left, providerlen;
 
-	p = imsg_bytes(imsg, &len);
+	if (imsgev_get_payload(imsg, &payload) == -1)
+		fatal("get web HELLO payload");
+	p = payload.data;
+	len = payload.len;
 	if (p == NULL || len < sizeof(h) || len > FUGU_WEB_HELLO_MAX)
 		fatalx("fugu-web: malformed HELLO");
 	memcpy(&h, p, sizeof(h));
@@ -227,6 +218,7 @@ static int
 web_dispatch(struct worker *w, struct imsg *imsg)
 {
 	struct web_state	*ws = w->ctx;
+	struct imsgev_payload payload;
 	const u_char	*p;
 	size_t		 len, i, namelen;
 
@@ -234,7 +226,10 @@ web_dispatch(struct worker *w, struct imsg *imsg)
 	case FUGU_IMSG_WEB_REQ: {
 		struct tool_req req;
 
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get WEB_REQ payload");
+		p = payload.data;
+		len = payload.len;
 		if (p == NULL || len != sizeof(req) || ws->have_name ||
 		    imsg_get_id(imsg) == 0)
 			fatalx("fugu-web: malformed WEB_REQ");
@@ -252,7 +247,10 @@ web_dispatch(struct worker *w, struct imsg *imsg)
 		return (0);
 	}
 	case FUGU_IMSG_WEB_ARG:
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get WEB_ARG payload");
+		p = payload.data;
+		len = payload.len;
 		if (!ws->have_name || imsg_get_id(imsg) != ws->reqid ||
 		    p == NULL || len == 0 ||
 		    len > FUGU_WEB_ARG_MAX - ws->arg.len)
@@ -264,7 +262,10 @@ web_dispatch(struct worker *w, struct imsg *imsg)
 		struct buf out;
 		int is_error;
 
-		p = imsg_bytes(imsg, &len);
+		if (imsgev_get_payload(imsg, &payload) == -1)
+			fatal("get WEB_RUN payload");
+		p = payload.data;
+		len = payload.len;
 		if (p != NULL || len != 0 || !ws->have_name ||
 		    imsg_get_id(imsg) != ws->reqid)
 			fatalx("fugu-web: malformed WEB_RUN");