Skip to content

Commit 6cf93e4

Browse files
committed
Fix use-after-free race in socket driver close
https://ampcode.com/threads/T-019cb8b8-9e4c-7316-9566-c7e3f5f2b6db Fix a use-after-free race condition in the generic_unix socket driver's close handler, detected by Valgrind during CI gen_tcp tests. The close handler in socket_consume_mailbox used a two-phase locking pattern: it acquired the glb->listeners lock to NULL-out the socket_data listener pointers, released it, then called sys_unregister_listener (which re-acquires the lock) to remove the listener from the linked list. Between the unlock and re-lock, the event loop thread could also unlink the same listener node via process_listener_handler after the callback returned NULL. The subsequent list_remove in sys_unregister_listener then operated on stale prev/next pointers, corrupting the list or writing to freed memory. The fix makes the pointer detach and list unlink atomic under a single lock hold by introducing sys_unregister_listener_nolock — a variant that assumes the caller already holds the glb->listeners write lock. The close handler now NULLs the pointers, unlinks the listeners, and releases the lock before freeing the memory. This pattern is specific to generic_unix; ESP32 and RP2 use a single global listener for the socket driver subsystem and are not affected. Signed-off-by: Peter M <petermm@gmail.com>
1 parent e2dda4e commit 6cf93e4

File tree

2 files changed

+19
-16
lines changed

2 files changed

+19
-16
lines changed

src/platforms/generic_unix/lib/socket_driver.c

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
#include <sys/types.h>
4848
#include <unistd.h>
4949

50+
void sys_unregister_listener_nolock(GlobalContext *global, struct EventListener *listener);
51+
5052
// #define ENABLE_TRACE
5153
#include "trace.h"
5254

@@ -1194,31 +1196,26 @@ static NativeHandlerResult socket_consume_mailbox(Context *ctx)
11941196
TRACE("close\n");
11951197
port_send_reply(ctx, pid, ref, OK_ATOM);
11961198
SocketDriverData *socket_data = (SocketDriverData *) ctx->platform_data;
1197-
// Callbacks (active_recv_callback, passive_recv_callback) are called
1198-
// while glb->listeners lock is held. They may want to free the
1199-
// listener, causing a potential double free here.
1200-
// We acquire the lock on listeners here and set the listeners
1201-
// to NULL in the socket_data structure to prevent them from freeing
1202-
// the listeners.
1199+
// Callbacks (active_recv_callback, passive_recv_callback, accept_callback)
1200+
// are called while glb->listeners lock is held. They may free the
1201+
// listener and set the socket_data pointer to NULL.
1202+
// We must atomically detach the pointers AND unlink from the listeners
1203+
// list under the same lock hold, to prevent a race where the callback
1204+
// also unlinks the same listener node.
12031205
synclist_wrlock(&glb->listeners);
12041206
ActiveRecvListener *active_listener = socket_data->active_listener;
12051207
PassiveRecvListener *passive_listener = socket_data->passive_listener;
12061208
socket_data->active_listener = NULL;
12071209
socket_data->passive_listener = NULL;
1208-
synclist_unlock(&glb->listeners);
12091210
if (active_listener) {
1210-
// Then we unregister, which also acquires the lock. The callbacks
1211-
// may have returned NULL which means the listener would no longer
1212-
// be registered, but this will work.
1213-
sys_unregister_listener(glb, &active_listener->base);
1214-
// After the listener is unregistered, the callbacks can no longer
1215-
// be called, so we can eventually free the listener
1216-
free(active_listener);
1211+
sys_unregister_listener_nolock(glb, &active_listener->base);
12171212
}
12181213
if (passive_listener) {
1219-
sys_unregister_listener(glb, &passive_listener->base);
1220-
free(passive_listener);
1214+
sys_unregister_listener_nolock(glb, &passive_listener->base);
12211215
}
1216+
synclist_unlock(&glb->listeners);
1217+
free(active_listener);
1218+
free(passive_listener);
12221219
socket_driver_do_close(ctx);
12231220
// We don't need to remove message.
12241221
return NativeTerminate;

src/platforms/generic_unix/lib/sys.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -698,6 +698,12 @@ void sys_unregister_listener(GlobalContext *global, struct EventListener *listen
698698
synclist_remove(&global->listeners, &listener->listeners_list_head);
699699
}
700700

701+
void sys_unregister_listener_nolock(GlobalContext *global, struct EventListener *listener)
702+
{
703+
listener_event_remove_from_polling_set(listener->fd, global);
704+
list_remove(&listener->listeners_list_head);
705+
}
706+
701707
void sys_register_select_event(GlobalContext *global, ErlNifEvent event, bool is_write)
702708
{
703709
struct GenericUnixPlatformData *platform = global->platform_data;

0 commit comments

Comments
 (0)