From 7aa35d78049e10d122e6be8ffa592ab9113db925 Mon Sep 17 00:00:00 2001 From: Navid EMAD Date: Thu, 15 Oct 2026 03:06:12 +0200 Subject: [PATCH] fix(ext): free the non-blocking errors before a raise nogvl_cleanup raised the first SQL Server error before it freed the malloc'd nonblocking_errors array. Each error leaked the array (4,136 bytes). Copy the errors into a Ruby-managed buffer. Free the array before any raise. --- CHANGELOG.md | 2 ++ ext/tiny_tds/result.c | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8166f1f..a5d9d889 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## (unreleased) +* Free the errors that a non-blocking call collects before the extension raises them. Each SQL Server error no longer leaks about 4 KB of native memory. + ## 3.4.1 * Register the C globals that hold Ruby classes and modules with the GC, so raising `TinyTds::Error` after GC compaction no longer crashes with a segmentation fault. Fixes #608. diff --git a/ext/tiny_tds/result.c b/ext/tiny_tds/result.c index 6323403e..de894fa8 100644 --- a/ext/tiny_tds/result.c +++ b/ext/tiny_tds/result.c @@ -121,16 +121,29 @@ static void nogvl_cleanup(DBPROCESS *client) exceptions based on errors from SQL Server. */ short int i; + short int errors_length = userdata->nonblocking_errors_length; - for (i = 0; i < userdata->nonblocking_errors_length; i++) { - tinytds_errordata error = userdata->nonblocking_errors[i]; + /* + A raise longjmps out of this function and skips the code after it. + Free the malloc'd array first. If an error is raised, the GC frees the copy. + */ + VALUE errors_buffer; + tinytds_errordata *errors = ALLOCV_N(tinytds_errordata, errors_buffer, errors_length); + MEMCPY(errors, userdata->nonblocking_errors, tinytds_errordata, errors_length); + free(userdata->nonblocking_errors); + userdata->nonblocking_errors = NULL; + userdata->nonblocking_errors_length = 0; + userdata->nonblocking_errors_size = 0; + + for (i = 0; i < errors_length; i++) { + tinytds_errordata error = errors[i]; // lookahead to drain any info messages ahead of raising error if (!error.is_message) { short int j; - for (j = i; j < userdata->nonblocking_errors_length; j++) { - tinytds_errordata msg_error = userdata->nonblocking_errors[j]; + for (j = i; j < errors_length; j++) { + tinytds_errordata msg_error = errors[j]; if (msg_error.is_message) { rb_tinytds_raise_error(client, msg_error); @@ -141,9 +154,7 @@ static void nogvl_cleanup(DBPROCESS *client) rb_tinytds_raise_error(client, error); } - free(userdata->nonblocking_errors); - userdata->nonblocking_errors_length = 0; - userdata->nonblocking_errors_size = 0; + ALLOCV_END(errors_buffer); } static RETCODE nogvl_dbsqlok(DBPROCESS *client)