From 85782345ffb7d1376cc77a8605c765c48e79f070 Mon Sep 17 00:00:00 2001 From: Randy Stauner Date: Fri, 22 May 2026 17:59:30 -0700 Subject: [PATCH] Fix Semian resource allocator pairing Ruby headers redefine strdup to ruby_strdup in C extensions, so res->name was allocated with Ruby's allocator even though it looked like libc strdup. Free it with xfree instead of free, and make the allocation explicit with ruby_strdup. Also release the malloc'd strkey field and add GC regression coverage for native resource cleanup. --- ext/semian/resource.c | 13 ++++++++++--- test/resource_test.rb | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/ext/semian/resource.c b/ext/semian/resource.c index 6ded69cd0..1301b40be 100644 --- a/ext/semian/resource.c +++ b/ext/semian/resource.c @@ -234,7 +234,7 @@ semian_resource_initialize(VALUE self, VALUE id, VALUE tickets, VALUE quota, VAL // Populate struct fields ms_to_timespec(c_timeout * 1000, &res->timeout); - res->name = strdup(c_id_str); + res->name = ruby_strdup(c_id_str); res->quota = c_quota; res->wait_time = -1; @@ -370,11 +370,18 @@ static void semian_resource_free(void *ptr) { semian_resource_t *res = (semian_resource_t *) ptr; + if (res->name) { - free(res->name); + xfree(res->name); // ruby_strdup res->name = NULL; } - xfree(res); + + if (res->strkey) { + free(res->strkey); // malloc + res->strkey = NULL; + } + + xfree(res); // TypedData_Make_Struct } static size_t diff --git a/test/resource_test.rb b/test/resource_test.rb index b7d2f3902..e3e0c2c26 100644 --- a/test/resource_test.rb +++ b/test/resource_test.rb @@ -424,6 +424,15 @@ def test_get_resource_key assert_equal("0x874714f2", resource.key) end + def test_resource_native_memory_can_be_reclaimed_by_gc + # Regression coverage for Semian::Resource's native free callback. ASAN/LSAN + # builds catch allocator mismatches and leaked native fields when GC runs it. + 10.times do |i| + create_and_destroy_untracked_resource("testing_gc_#{Process.pid}_#{i}") + GC.start(full_mark: true, immediate_sweep: true) + end + end + def test_count resource = create_resource(:testing, tickets: 2) acquired = false @@ -600,6 +609,13 @@ def create_resource(name, **kwargs) resource end + def create_and_destroy_untracked_resource(name) + resource = create_resource(name, tickets: 1) + resource.destroy + @resources.delete(resource) + nil + end + def destroy_resources return unless @resources