fix(protocol): release Data charge to its owning session

Data charged against a ProtocolSession kept only the charge amount, so
data_destroy released it from whatever session was thread-locally bound
at destroy time. Destroying a received Data on another thread, after the
session was unbound, or while a different session was bound leaked the
originating session's budget and underflowed the other's.

Add Data.owner, set it whenever protocol_receive_data_limited charges a
session, and have data_destroy release against that owner directly via
the newly-exported protocol_release_memory_for_session. Uncharged Data
(owner NULL) keeps the previous bound-session fallback.

Add a unit test proving a Data acquired on session A is released to A
even when unrelated session B is bound at destroy time.
This commit is contained in:
2026-09-13 10:05:38 +02:00
parent 9242e86772
commit 5d3c43305e
5 changed files with 71 additions and 3 deletions
+8 -2
View File
@@ -23,6 +23,7 @@ Data* data_create_reserve(size_t size) {
d->data = NULL;
d->size = size;
d->protocol_charge = 0;
d->owner = NULL;
return d;
}
@@ -36,14 +37,19 @@ Data* data_create(void* data, size_t data_size) {
new_data->data = data;
new_data->size = data_size;
new_data->protocol_charge = 0;
new_data->owner = NULL;
return new_data;
}
void data_destroy(Data* data) {
if (data == NULL)
return;
if (data->protocol_charge != 0)
protocol_release_memory(data->protocol_charge);
if (data->protocol_charge != 0) {
if (data->owner != NULL)
protocol_release_memory_for_session(data->owner, data->protocol_charge);
else
protocol_release_memory(data->protocol_charge);
}
free(data->data);
free(data);
}
+11
View File
@@ -3,11 +3,19 @@
#include <stdlib.h>
/* Forward declaration for the connection budget a received Data is charged
* against; defined in protocol.h (which includes this header). */
typedef struct ProtocolSession ProtocolSession;
typedef struct {
void* data;
size_t size;
/* Non-zero only for a buffer charged to the protocol connection budget. */
size_t protocol_charge;
/* Session whose budget `protocol_charge` was reserved from. The charge must
* always be returned to this session, regardless of which session (if any) is
* bound to the destroying thread. NULL for uncharged Data. */
ProtocolSession* owner;
} Data;
Data* data_create_empty(size_t data_size);
@@ -15,5 +23,8 @@ Data* data_create_reserve(size_t size);
Data* data_create(void* data, size_t data_size);
void data_destroy(Data* data);
void protocol_release_memory(size_t charge);
/* Release `charge` against `session` directly instead of the thread-local bound
* session. Used by data_destroy to honor Data.owner. */
void protocol_release_memory_for_session(ProtocolSession* session, size_t charge);
#endif
+2 -1
View File
@@ -40,7 +40,7 @@ static bool protocol_reserve_memory(ProtocolSession* session, size_t charge) {
}
}
static void protocol_release_memory_for_session(ProtocolSession* session, size_t charge) {
void protocol_release_memory_for_session(ProtocolSession* session, size_t charge) {
unsigned long long allocated = atomic_load(&session->total_allocated_bytes);
while (true) {
unsigned long long remaining = (unsigned long long)charge >= allocated ? 0 : allocated - charge;
@@ -573,6 +573,7 @@ Data* protocol_receive_data_limited(ProtocolSession* session, unsigned long long
return NULL;
}
result->protocol_charge = allocation_size;
result->owner = session;
return result;
}