C++20 port 9/14: own AEAD-2022 UDP sessions by reference (eviction fix) (#3073)

* Own AEAD-2022 UDP sessions by reference so eviction cannot free a live session

The session table and the relay contexts each hold a reference; evicting a
session from the full table no longer frees memory a relay still uses.
Ported from the parked fix/aead2022-udp-session-eviction work.

* Include <netinet/in.h> in the UDP eviction test client (FreeBSD)

FreeBSD's <arpa/inet.h> does not define struct sockaddr_in.
This commit is contained in:
Max Lv
2026-10-15 10:02:02 +08:00
committed by GitHub
parent 0fb282245c
commit 7041693ee4
10 changed files with 440 additions and 31 deletions
+7
View File
@@ -52,6 +52,13 @@ cipher_t *aead_init(const char *pass, const char *key, const char *method);
int aead_is_2022(const cipher_t *cipher);
void aead_set_role(int role); /* override compile-time default; used by tests */
/*
* Server-side UDP session table size. Inserting beyond it evicts the least
* recently used session; a relay context that still references the evicted
* session keeps it alive until it releases it.
*/
#define AEAD_2022_UDP_SESSION_TABLE_CAP 4096
int aead_2022_encrypt_udp(buffer_t *, cipher_t *, size_t, void **);
int aead_2022_decrypt_udp(buffer_t *, cipher_t *, size_t, void **);
void aead_2022_udp_session_release(void *);
+24 -21
View File
@@ -41,6 +41,7 @@
#include "aead_internal.h"
#include "cache.h"
#include "utils.h"
#include "ss_ref.h"
#define SS2022_UDP_TYPE_CLIENT 0
#define SS2022_UDP_TYPE_SERVER 1
@@ -59,6 +60,7 @@
* peer session we are receiving from.
*/
typedef struct ss2022_udp_session {
ss_ref_t ref; /* Table and relay slots independently own this session. */
cipher_t *cipher;
/* outgoing */
@@ -78,12 +80,6 @@ typedef struct ss2022_udp_session {
uint8_t peer_session_id[SS2022_SESSION_ID_LEN];
int peer_session_known;
/*
* Server sessions are owned by the session table (they are looked up by
* client session ID, which is only known after decryption); client
* sessions are owned by the caller's slot.
*/
int table_owned;
} ss2022_udp_session_t;
/*
@@ -91,19 +87,24 @@ typedef struct ss2022_udp_session {
* requires servers to route by session ID rather than source address, so
* that sessions survive client network changes.
*/
#define SS2022_SESSION_TABLE_CAP 4096
#define SS2022_SESSION_TABLE_CAP AEAD_2022_UDP_SESSION_TABLE_CAP
#define SS2022_SESSION_TTL 600
static struct cache *ss2022_sessions = NULL;
static void
session_destroy(void *object)
{
sodium_memzero(object, sizeof(ss2022_udp_session_t));
ss_free(object);
}
/* Cache callback: drop the table's reference when an entry is evicted. */
static void
session_free_cb(void *key, void *element)
{
(void)key;
if (element != NULL) {
sodium_memzero(element, sizeof(ss2022_udp_session_t));
ss_free(element);
}
ss_ref_release(element);
}
static void
@@ -181,7 +182,8 @@ session_new(cipher_t *cipher)
{
ss2022_udp_session_t *s = (ss2022_udp_session_t *)ss_malloc(sizeof(ss2022_udp_session_t));
memset(s, 0, sizeof(ss2022_udp_session_t));
s->cipher = cipher;
s->cipher = cipher;
ss_ref_init(s, session_destroy);
return s;
}
@@ -214,20 +216,20 @@ session_by_id(const uint8_t *session_id, cipher_t *cipher)
cache_clear(ss2022_sessions, SS2022_SESSION_TTL);
s = session_new(cipher);
s->table_owned = 1;
cache_insert(ss2022_sessions, (char *)session_id, SS2022_SESSION_ID_LEN, s);
s = session_new(cipher);
/* Transfer the initial reference to the table on successful insertion. */
if (cache_insert(ss2022_sessions, (char *)session_id,
SS2022_SESSION_ID_LEN, s) != 0) {
ss_ref_release(s);
return NULL;
}
return s;
}
void
aead_2022_udp_session_release(void *slot)
{
ss2022_udp_session_t *s = (ss2022_udp_session_t *)slot;
if (s == NULL || s->table_owned)
return; /* table-owned sessions are freed when the table evicts them */
sodium_memzero(s, sizeof(ss2022_udp_session_t));
ss_free(s);
ss_ref_release(slot);
}
static int
@@ -531,7 +533,6 @@ aead_2022_decrypt_udp(buffer_t *ciphertext, cipher_t *cipher, size_t capacity,
s = session_by_id(session_id, cipher);
if (s == NULL)
return CRYPTO_ERROR;
*slot = s; /* the reply path encrypts with this same session */
} else {
s = session_get(slot, cipher);
}
@@ -585,6 +586,8 @@ aead_2022_decrypt_udp(buffer_t *ciphertext, cipher_t *cipher, size_t capacity,
if (is_server) {
memcpy(s->peer_session_id, session_id, SS2022_SESSION_ID_LEN);
s->peer_session_known = 1;
/* Publish only validated packets and retain the reply session. */
ss_ref_assign(slot, s);
}
size_t payload_len = plen - o;
+82
View File
@@ -0,0 +1,82 @@
#ifndef SS_REF_H
#define SS_REF_H
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
/* Intrusive shared ownership for a single event-loop thread. Embed this as
* the FIRST member of an object, initialize once, and never copy the object.
* Each owning slot/table entry holds one reference; raw aliases are borrowed.
* The destructor frees the enclosing object. Cycles and cross-thread sharing
* require a different ownership scheme. */
typedef struct ss_ref {
size_t count;
void (*destroy)(void *);
} ss_ref_t;
static inline void
ss_ref_init(void *object, void (*destroy)(void *))
{
ss_ref_t *ref = (ss_ref_t *)object;
ref->count = 1;
ref->destroy = destroy;
}
static inline void *
ss_ref_retain(void *object)
{
if (object != NULL) {
ss_ref_t *ref = (ss_ref_t *)object;
if (ref->count == 0 || ref->count == SIZE_MAX)
abort();
ref->count++;
}
return object;
}
static inline void
ss_ref_release(void *object)
{
if (object != NULL) {
ss_ref_t *ref = (ss_ref_t *)object;
if (ref->count == 0)
abort();
if (--ref->count == 0)
ref->destroy(object);
}
}
/* Reset/move also support opaque objects with their own release callback.
* Slots must be actual void * objects, not casts from typed pointer slots.
* Move consumes the source reference and releases the previous destination. */
static inline void
ss_ptr_reset(void **slot, void (*release)(void *))
{
void *old = *slot;
*slot = NULL;
if (old != NULL)
release(old);
}
static inline void
ss_ptr_move(void **dst, void **src, void (*release)(void *))
{
if (dst == src)
return;
void *old = *dst;
*dst = *src;
*src = NULL;
if (old != NULL)
release(old);
}
/* Copy a borrowed reference into an owning slot, retaining before release. */
static inline void
ss_ref_assign(void **slot, void *object)
{
void *owned = ss_ref_retain(object);
ss_ptr_move(slot, &owned, ss_ref_release);
}
#endif /* SS_REF_H */
+17 -10
View File
@@ -52,6 +52,7 @@
#include "netutils.h"
#include "cache.h"
#include "udprelay.h"
#include "ss_ref.h"
#include "ss_windows.h"
#ifdef MODULE_REMOTE
@@ -628,6 +629,7 @@ close_and_free_query(SS_P_ struct query_ctx *ctx)
bfree(ctx->buf);
ss_free(ctx->buf);
}
ss_ptr_reset(&ctx->udp_session, ctx->server_ctx->crypto->udp_session_release);
ss_free(ctx);
}
}
@@ -641,11 +643,8 @@ close_and_free_remote(SS_P_ remote_ctx_t *ctx)
ss_timer_stop(SS_A_ & ctx->watcher);
ss_io_stop(SS_A_ & ctx->io);
ss_socket_close(ctx->fd);
if (ctx->udp_session != NULL
&& ctx->server_ctx != NULL
&& ctx->server_ctx->crypto->udp_session_release != NULL) {
ctx->server_ctx->crypto->udp_session_release(ctx->udp_session);
}
if (ctx->server_ctx != NULL)
ss_ptr_reset(&ctx->udp_session, ctx->server_ctx->crypto->udp_session_release);
ss_free(ctx);
}
}
@@ -735,6 +734,8 @@ resolv_cb(struct sockaddr *addr, void *data)
}
if (remote_ctx != NULL) {
ss_ptr_move(&remote_ctx->udp_session, &query_ctx->udp_session,
query_ctx->server_ctx->crypto->udp_session_release);
if (addr->sa_family == AF_INET)
memcpy(&remote_ctx->dst_addr, addr, sizeof(struct sockaddr_in));
else
@@ -1018,6 +1019,9 @@ server_recv_cb(SS_P_ ss_io *w, int revents)
socklen_t src_addr_len = sizeof(struct sockaddr_storage);
unsigned int offset = 0;
#ifdef MODULE_REMOTE
void *udp_session = NULL;
#endif
#ifdef MODULE_REDIR
char control_buffer[64] = { 0 };
@@ -1083,8 +1087,6 @@ server_recv_cb(SS_P_ ss_io *w, int revents)
* decryption, so the crypto layer resolves the session itself and hands
* it back here to be attached to the relay context.
*/
void *udp_session;
udp_session = NULL;
int err;
err = server_ctx->crypto->decrypt_udp != NULL
? server_ctx->crypto->decrypt_udp(buf, server_ctx->crypto->cipher,
@@ -1475,9 +1477,9 @@ server_recv_cb(SS_P_ ss_io *w, int revents)
}
}
if (remote_ctx != NULL) {
// Attach the AEAD-2022 session so replies are encrypted with it
remote_ctx->udp_session = udp_session;
if (remote_ctx != NULL && !need_query) {
ss_ptr_move(&remote_ctx->udp_session, &udp_session,
server_ctx->crypto->udp_session_release);
}
if (remote_ctx != NULL && !need_query) {
@@ -1506,6 +1508,8 @@ server_recv_cb(SS_P_ ss_io *w, int revents)
struct query_ctx *query_ctx = new_query_ctx(buf->data + addr_header_len,
buf->len - addr_header_len);
query_ctx->server_ctx = server_ctx;
ss_ptr_move(&query_ctx->udp_session, &udp_session,
server_ctx->crypto->udp_session_release);
query_ctx->addr_header_len = addr_header_len;
query_ctx->src_addr = src_addr;
memcpy(query_ctx->addr_header, addr_header, addr_header_len);
@@ -1525,6 +1529,9 @@ server_recv_cb(SS_P_ ss_io *w, int revents)
#endif
CLEAN_UP:
#ifdef MODULE_REMOTE
ss_ptr_reset(&udp_session, server_ctx->crypto->udp_session_release);
#endif
bfree(buf);
ss_free(buf);
}
+1
View File
@@ -75,6 +75,7 @@ typedef struct query_ctx {
char addr_header[MAX_ADDR_HEADER_SIZE];
struct server_ctx *server_ctx;
struct remote_ctx *remote_ctx;
void *udp_session; /* Owned until transferred to the resolved relay. */
} query_ctx_t;
#endif
+15
View File
@@ -95,6 +95,9 @@ if(Python3_Interpreter_FOUND)
endif()
endif()
# Shared-reference ownership and consuming pointer moves.
ss_add_test(test_ref "test_ref.c" "")
# test_base64 - no external deps
ss_add_test(test_base64
"test_base64.c;${PROJECT_SOURCE_DIR}/src/base64.cpp"
@@ -290,6 +293,18 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
--bin-dir ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
set_tests_properties(test_cli PROPERTIES LABELS "integration" TIMEOUT 60)
if(UNIX)
add_executable(udp_session_eviction_client
udp_session_eviction_client.c ${SS_CRYPTO_TEST_SOURCES})
target_compile_definitions(udp_session_eviction_client PRIVATE HAVE_CONFIG_H)
target_compile_options(udp_session_eviction_client PRIVATE -UNDEBUG)
target_link_libraries(udp_session_eviction_client
${TEST_CRYPTO_LIBS} ${SS_MATH_LIBRARY})
ss_project_options(udp_session_eviction_client)
add_test(NAME test_udp_session_eviction
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_udp_session_eviction.py
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
set_tests_properties(test_udp_session_eviction PROPERTIES
LABELS "integration" TIMEOUT 300)
add_test(NAME test_experiment_supervisor
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/experiments/test_supervisor.py
--bin ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+82
View File
@@ -327,6 +327,10 @@ test_udp_replay_rejected(const char *method)
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
/* A rejected packet must not publish a session into a fresh relay slot. */
void *rejected_session = NULL;
assert(c->decrypt_udp(&copy, c->cipher, 2048, &rejected_session) == CRYPTO_ERROR);
assert(rejected_session == NULL);
/* the very same packet again must be refused */
assert(c->decrypt_udp(&copy, c->cipher, 2048, &server_session) == CRYPTO_ERROR);
@@ -338,6 +342,83 @@ test_udp_replay_rejected(const char *method)
free_crypto(c);
}
/* Encrypt one client packet into buf and hand it to the server's slot. */
static void
client_to_server(crypto_t *c, buffer_t *buf, const uint8_t *pkt, size_t len,
void **client_session, void **server_session)
{
memcpy(buf->data, pkt, len);
buf->len = len;
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(buf, c->cipher, 2048, client_session) == CRYPTO_OK);
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(buf, c->cipher, 2048, server_session) == CRYPTO_OK);
assert(buf->len == len && memcmp(buf->data, pkt, len) == 0);
}
/*
* A relay context keeps its server session across table eviction. Flooding
* the table with fresh client session IDs used to free the session behind
* the relay's back, so the reply path encrypted with freed memory.
*/
static void
test_udp_session_survives_table_eviction(const char *method)
{
static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 };
crypto_t *c = make_crypto(method);
void *client_session = NULL;
void *server_session = NULL;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session != NULL);
void *first = server_session;
void *shared_session = NULL;
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &shared_session);
assert(shared_session == first);
/* Reattaching the same slot must not acquire an extra reference. */
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session == first);
/* Each new client session ID inserts a table entry; overflow evicts ours. */
for (int i = 0; i < AEAD_2022_UDP_SESSION_TABLE_CAP + 1; i++) {
void *other_client = NULL, *other_server = NULL;
client_to_server(c, &buf, pkt, sizeof(pkt), &other_client, &other_server);
assert(other_server != NULL && other_server != first);
c->udp_session_release(other_client);
c->udp_session_release(other_server);
}
/* Dropping one relay owner must leave the other one usable. */
c->udp_session_release(shared_session);
/* The reply must still encrypt with the evicted session and verify. */
memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
assert(server_session == first);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
assert(buf.len == sizeof(pkt) && memcmp(buf.data, pkt, sizeof(pkt)) == 0);
/* The client's next packet re-creates a table entry and swaps the slot. */
client_to_server(c, &buf, pkt, sizeof(pkt), &client_session, &server_session);
assert(server_session != NULL && server_session != first);
/* Releasing the slot after eviction must neither leak nor double free. */
c->udp_session_release(server_session);
c->udp_session_release(client_session);
bfree(&buf);
aead_set_role(CRYPTO_ROLE_CLIENT);
free_crypto(c);
}
/* The 2022 ciphers must reject a PSK of the wrong size rather than pad it. */
static void
test_psk_parsing(void)
@@ -386,6 +467,7 @@ main(void)
test_tcp_tamper_detection(m);
test_udp_roundtrip(m);
test_udp_replay_rejected(m);
test_udp_session_survives_table_eviction(m);
}
return 0;
+63
View File
@@ -0,0 +1,63 @@
#include <assert.h>
#include <stdlib.h>
#include "ss_ref.h"
typedef struct {
ss_ref_t ref;
int *destroyed;
} test_object_t;
static void
destroy_object(void *object)
{
test_object_t *value = object;
++*value->destroyed;
free(value);
}
static void *
new_object(int *destroyed)
{
test_object_t *value = malloc(sizeof(*value));
assert(value != NULL);
ss_ref_init(value, destroy_object);
value->destroyed = destroyed;
return value;
}
int
main(void)
{
int destroyed = 0;
void *table = new_object(&destroyed);
void *relay = NULL;
ss_ref_assign(&relay, table);
ss_ref_assign(&relay, relay); /* Self-assignment must preserve ownership. */
ss_ptr_reset(&table, ss_ref_release); /* Simulate eviction. */
assert(table == NULL && destroyed == 0);
ss_ptr_reset(&relay, ss_ref_release);
assert(relay == NULL && destroyed == 1);
ss_ptr_reset(&relay, ss_ref_release); /* Repeated cleanup is harmless. */
assert(destroyed == 1);
void *query = new_object(&destroyed);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release);
assert(query == NULL && relay != NULL && destroyed == 2);
ss_ptr_move(&relay, &relay, ss_ref_release); /* Self-move. */
assert(relay != NULL && destroyed == 2);
ss_ref_assign(&query, relay);
ss_ptr_move(&relay, &query, ss_ref_release); /* Two owners, same object. */
assert(query == NULL && relay != NULL && destroyed == 2);
ss_ref_assign(&relay, NULL);
assert(relay == NULL && destroyed == 3);
relay = new_object(&destroyed);
ss_ptr_move(&relay, &query, ss_ref_release); /* Move an empty source. */
assert(relay == NULL && query == NULL && destroyed == 4);
assert(ss_ref_retain(NULL) == NULL);
ss_ref_release(NULL);
ss_ptr_reset(&relay, NULL); /* Legacy crypto has no session/callback. */
return 0;
}
+35
View File
@@ -0,0 +1,35 @@
"""Exercise the production relay at the exact AEAD-2022 cache boundary."""
import argparse
import os
from pathlib import Path
import subprocess
from interop import free_port, peers
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--bin', type=Path, required=True)
parser.add_argument('--server', type=Path)
parser.add_argument('--count', type=int, choices=(4095, 4096))
args = parser.parse_args()
binaries = args.bin.resolve()
server = (args.server or binaries / 'ss-server').resolve()
environment = dict(os.environ, UBSAN_OPTIONS='halt_on_error=1:print_stacktrace=1')
for method in ('2022-blake3-aes-128-gcm', '2022-blake3-aes-256-gcm',
'2022-blake3-chacha20-poly1305'):
key = ('AAAAAAAAAAAAAAAAAAAAAA==' if 'aes-128' in method else
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=')
for count in ((args.count,) if args.count else (4095, 4096)):
port = free_port()
command = [str(server), '-s', '127.0.0.1', '-p', str(port),
'-k', key, '-m', method, '-u', '-t', '60']
with peers([command], [port], environment):
subprocess.run([str(binaries / 'udp_session_eviction_client'),
str(port), str(count), method, key],
env=environment, check=True, timeout=45)
print(f'{method}: {count} sessions, both replies verified', flush=True)
if __name__ == '__main__':
main()
+114
View File
@@ -0,0 +1,114 @@
/* Live UDP relay regression based on Akhil Koul (@akoul)'s private report.
* Two source sockets preserve one relay while replacing the other's session.
* Receive each forwarded packet before sending the next: no racing counters,
* sleeps, or unbounded waits are needed to hit the exact cache boundary. */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sodium.h>
#include "aead.h"
#include "crypto.h"
#include "ppbloom.h"
#include "utils.h"
int verbose = 0;
struct cache *nonce_cache = NULL;
static int
udp_socket(void)
{
int fd = socket(AF_INET, SOCK_DGRAM, 0);
assert(fd >= 0);
struct timeval timeout = { 5, 0 };
assert(setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)) == 0);
return fd;
}
static void
check_reply(int upstream, int client, const struct sockaddr_in *peer,
crypto_t *crypto, void **session, buffer_t *packet)
{
const unsigned char reply = 0x42;
assert(sendto(upstream, &reply, 1, 0, (const struct sockaddr *)peer,
sizeof(*peer)) == 1);
ssize_t n = recv(client, packet->data, packet->capacity, 0);
assert(n > 0);
packet->len = (size_t)n;
assert(crypto->decrypt_udp(packet, crypto->cipher, 2048, session) == CRYPTO_OK);
assert(packet->len == 8 && (unsigned char)packet->data[0] == 1);
assert((unsigned char)packet->data[7] == reply);
}
int
main(int argc, char **argv)
{
assert(argc == 5);
int port = atoi(argv[1]);
int count = atoi(argv[2]);
assert(port > 0 && port <= 65535 && count > 1);
assert(sodium_init() >= 0);
crypto_t *crypto = crypto_init(argv[4], NULL, argv[3]);
assert(crypto != NULL);
aead_set_role(CRYPTO_ROLE_CLIENT);
int upstream = udp_socket(), first = udp_socket(), flood = udp_socket();
struct sockaddr_in origin = { 0 }, server = { 0 };
origin.sin_family = server.sin_family = AF_INET;
origin.sin_addr.s_addr = server.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
server.sin_port = htons((uint16_t)port);
assert(bind(upstream, (struct sockaddr *)&origin, sizeof(origin)) == 0);
socklen_t length = sizeof(origin);
assert(getsockname(upstream, (struct sockaddr *)&origin, &length) == 0);
uint16_t upstream_port = ntohs(origin.sin_port);
const unsigned char plaintext[] = {
1, 127, 0, 0, 1, (unsigned char)(upstream_port >> 8),
(unsigned char)upstream_port, 0x41
};
buffer_t packet = { 0 };
balloc(&packet, 2048);
void *first_session = NULL, *last_session = NULL;
struct sockaddr_in first_peer = { 0 }, last_peer = { 0 };
for (int i = 0; i < count; ++i) {
void *session = NULL;
memcpy(packet.data, plaintext, sizeof(plaintext));
packet.len = sizeof(plaintext);
assert(crypto->encrypt_udp(&packet, crypto->cipher, 2048, &session) == CRYPTO_OK);
assert(sendto(i == 0 ? first : flood, packet.data, packet.len, 0,
(struct sockaddr *)&server, sizeof(server)) == (ssize_t)packet.len);
unsigned char payload[2];
length = sizeof(last_peer);
assert(recvfrom(upstream, payload, sizeof(payload), 0,
(struct sockaddr *)&last_peer, &length) == 1);
assert(payload[0] == 0x41);
if (i == 0) {
first_peer = last_peer;
first_session = session;
} else if (i == count - 1) {
last_session = session;
} else {
crypto->udp_session_release(session);
}
}
/* The first relay must survive eviction; the reused relay must use its
* latest session, not a leaked or stale previous owner. */
check_reply(upstream, first, &first_peer, crypto, &first_session, &packet);
check_reply(upstream, flood, &last_peer, crypto, &last_session, &packet);
crypto->udp_session_release(first_session);
crypto->udp_session_release(last_session);
bfree(&packet);
close(upstream);
close(first);
close(flood);
ppbloom_free();
ss_free(crypto->cipher);
ss_free(crypto);
return 0;
}