Skip to content

Commit 3f83361

Browse files
authored
Merge pull request #120 from sysprog21/closure-lifetime
Add closure lifetime tracking to prevent use-after-free
2 parents b818d67 + 22ad440 commit 3f83361

File tree

7 files changed

+353
-16
lines changed

7 files changed

+353
-16
lines changed

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ libtwin.a_files-y = \
5151
src/timeout.c \
5252
src/image.c \
5353
src/animation.c \
54+
src/closure.c \
5455
src/api.c
5556

5657
libtwin.a_includes-y := \

src/closure.c

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/*
2+
* Twin - A Tiny Window System
3+
* Copyright (c) 2025 National Cheng Kung University, Taiwan
4+
* All rights reserved.
5+
*/
6+
7+
/* Closure lifetime management implementation
8+
*
9+
* The Mado window system uses an asynchronous event model where widgets
10+
* schedule work items and timeouts with themselves as closure pointers. When a
11+
* widget is destroyed, these queued items may still reference the freed memory,
12+
* leading to use-after-free vulnerabilities and crashes.
13+
*
14+
* This implements a reference-counting closure tracking system that:
15+
* 1. Maintains a registry of all active closure pointers
16+
* 2. Tracks reference counts for each closure
17+
* 3. Provides validation before closure use
18+
* 4. Supports marking closures as "being freed" to prevent new references
19+
*
20+
* Flow Control
21+
* ------------
22+
* 1. Registration: When a widget/object is created, it registers itself as a
23+
* closure
24+
* 2. Reference: When scheduling work/timeout, the system adds a reference
25+
* 3. Validation: Before executing callbacks, the system validates the closure
26+
* 4. Cleanup: When work completes or widget is destroyed, references are
27+
* removed
28+
*
29+
* Important: Closures must be registered before they can be used in
30+
* work/timeout systems. The typical pattern is:
31+
* 1. Object creation: _twin_closure_register(obj)
32+
* 2. Schedule work: twin_set_work() calls _twin_closure_ref()
33+
* 3. Object destruction: _twin_closure_mark_for_free() then
34+
* _twin_closure_unregister()
35+
* 4. Work execution: _twin_closure_is_valid() check prevents use-after-free
36+
*/
37+
38+
#include <string.h>
39+
40+
#include "twin_private.h"
41+
42+
/* Global closure tracker instance */
43+
twin_closure_tracker_t _twin_closure_tracker = {0};
44+
45+
/*
46+
* Initialize the closure tracking system.
47+
* Called once during screen initialization to set up the tracking table.
48+
*/
49+
void _twin_closure_tracker_init(void)
50+
{
51+
memset(&_twin_closure_tracker, 0, sizeof(_twin_closure_tracker));
52+
_twin_closure_tracker.initialized = true;
53+
/* TODO: Initialize mutex when threading support is added */
54+
}
55+
56+
/*
57+
* Find an entry for a given closure pointer.
58+
* Uses linear search which is efficient for small closure counts.
59+
*
60+
* Returns entry pointer if found, NULL otherwise
61+
*/
62+
static twin_closure_entry_t *_twin_closure_find_entry(void *closure)
63+
{
64+
if (!closure)
65+
return NULL;
66+
67+
/* Quick rejection of obviously invalid pointers */
68+
if (!twin_pointer_valid(closure))
69+
return NULL;
70+
71+
for (int i = 0; i < _twin_closure_tracker.count; i++) {
72+
if (_twin_closure_tracker.entries[i].closure == closure)
73+
return &_twin_closure_tracker.entries[i];
74+
}
75+
return NULL;
76+
}
77+
78+
/*
79+
* Register a closure pointer with the tracking system.
80+
* If already registered, increments the reference count.
81+
*
82+
* This is typically called when a widget/object is created and may be used
83+
* as a closure in work items or timeouts.
84+
* @closure : The pointer to track (usually a widget or toplevel)
85+
*
86+
* Returns true if successfully registered, false on failure
87+
*/
88+
bool _twin_closure_register(void *closure)
89+
{
90+
if (!closure)
91+
return false;
92+
93+
/* Quick rejection of obviously invalid pointers */
94+
if (!twin_pointer_valid(closure))
95+
return false;
96+
97+
/* If tracker not initialized, skip registration */
98+
if (!_twin_closure_tracker.initialized)
99+
return true; /* Pretend success if tracking not yet enabled */
100+
101+
/* Check if already registered */
102+
twin_closure_entry_t *entry = _twin_closure_find_entry(closure);
103+
if (entry) {
104+
/* Already registered, just increment ref count */
105+
entry->ref_count++;
106+
return true;
107+
}
108+
109+
/* Check capacity */
110+
if (_twin_closure_tracker.count >= TWIN_MAX_CLOSURES)
111+
return false;
112+
113+
/* Add new entry at the end */
114+
entry = &_twin_closure_tracker.entries[_twin_closure_tracker.count++];
115+
entry->closure = closure;
116+
entry->ref_count = 1;
117+
entry->marked_for_free = false;
118+
119+
return true;
120+
}
121+
122+
/*
123+
* Remove a closure from the tracking system.
124+
* Called during object destruction to clean up tracking entries.
125+
*
126+
* Uses swap-with-last removal for O(1) deletion without gaps.
127+
*/
128+
void _twin_closure_unregister(void *closure)
129+
{
130+
if (!closure)
131+
return;
132+
133+
for (int i = 0; i < _twin_closure_tracker.count; i++) {
134+
if (_twin_closure_tracker.entries[i].closure == closure) {
135+
/* Swap with last entry and decrement count */
136+
if (i < _twin_closure_tracker.count - 1) {
137+
_twin_closure_tracker.entries[i] =
138+
_twin_closure_tracker
139+
.entries[_twin_closure_tracker.count - 1];
140+
}
141+
_twin_closure_tracker.count--;
142+
return;
143+
}
144+
}
145+
}
146+
147+
/*
148+
* Increment the reference count for a closure.
149+
* Called when scheduling new work items or timeouts.
150+
*
151+
* Fails if closure is not registered or marked for deletion.
152+
* This prevents new references to objects being destroyed.
153+
*/
154+
bool _twin_closure_ref(void *closure)
155+
{
156+
/* Skip if tracker not initialized */
157+
if (!_twin_closure_tracker.initialized)
158+
return true;
159+
160+
twin_closure_entry_t *entry = _twin_closure_find_entry(closure);
161+
if (!entry || entry->marked_for_free)
162+
return false;
163+
164+
entry->ref_count++;
165+
return true;
166+
}
167+
168+
/*
169+
* Decrement the reference count for a closure.
170+
* Called when work items complete or timeouts are cleared.
171+
*
172+
* Note: We don't auto-unregister at zero refs to maintain explicit
173+
* ownership semantics. The owner must call unregister during destruction.
174+
*/
175+
bool _twin_closure_unref(void *closure)
176+
{
177+
/* Skip if tracker not initialized */
178+
if (!_twin_closure_tracker.initialized)
179+
return true;
180+
181+
twin_closure_entry_t *entry = _twin_closure_find_entry(closure);
182+
if (!entry)
183+
return false;
184+
185+
if (entry->ref_count > 0)
186+
entry->ref_count--;
187+
188+
return true;
189+
}
190+
191+
/*
192+
* Validate a closure pointer before use.
193+
* This is the critical safety check called before executing callbacks.
194+
*
195+
* Validation steps:
196+
* 1. NULL check
197+
* 2. Platform-specific pointer validity (checks for obviously bad addresses)
198+
* 3. Presence in tracking table (untracked = invalid)
199+
* 4. Not marked for deletion
200+
* 5. Has active references
201+
*
202+
* Returns: true if safe to use, false if potentially freed
203+
*/
204+
bool _twin_closure_is_valid(void *closure)
205+
{
206+
if (!closure)
207+
return false;
208+
209+
/* First-line defense: basic pointer sanity */
210+
if (!twin_pointer_valid(closure))
211+
return false;
212+
213+
/* If tracker not initialized, fall back to basic pointer validation */
214+
if (!_twin_closure_tracker.initialized)
215+
return true; /* Assume valid if tracking not yet enabled */
216+
217+
/* Must be tracked to be valid */
218+
twin_closure_entry_t *entry = _twin_closure_find_entry(closure);
219+
if (!entry)
220+
return false;
221+
222+
/* Marked closures are in process of being freed */
223+
if (entry->marked_for_free)
224+
return false;
225+
226+
/* Must have active references */
227+
return entry->ref_count > 0;
228+
}
229+
230+
/*
231+
* Mark a closure as being freed.
232+
* Called at the start of object destruction to prevent races.
233+
*
234+
* Once marked:
235+
* - No new references can be added (ref() will fail)
236+
* - Existing references remain valid until cleared
237+
* - is_valid() returns false to prevent new callback execution
238+
*/
239+
void _twin_closure_mark_for_free(void *closure)
240+
{
241+
twin_closure_entry_t *entry = _twin_closure_find_entry(closure);
242+
if (entry)
243+
entry->marked_for_free = true;
244+
}

src/screen.c

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@ twin_screen_t *twin_screen_create(twin_coord_t width,
1515
twin_put_span_t put_span,
1616
void *closure)
1717
{
18+
static bool closure_tracker_initialized = false;
1819
twin_screen_t *screen = calloc(1, sizeof(twin_screen_t));
1920
if (!screen)
2021
return NULL;
2122

23+
/* Initialize closure tracking system on first screen creation */
24+
if (!closure_tracker_initialized) {
25+
_twin_closure_tracker_init();
26+
closure_tracker_initialized = true;
27+
}
28+
2229
screen->top = 0;
2330
screen->bottom = 0;
2431
screen->width = width;

src/timeout.c

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,9 @@ void _twin_run_timeout(void)
5050
twin_timeout_t *first = (twin_timeout_t *) _twin_queue_set_order(&head);
5151
for (timeout = first; timeout && twin_time_compare(now, >=, timeout->time);
5252
timeout = (twin_timeout_t *) timeout->queue.order) {
53-
/* Validate closure pointer before executing timeout callback */
54-
if (!twin_pointer_valid(timeout->closure)) {
55-
/* Invalid closure pointer, remove this timeout */
56-
_twin_queue_delete(&head, &timeout->queue);
53+
/* Validate closure pointer using closure lifetime tracking */
54+
if (!_twin_closure_is_valid(timeout->closure)) {
55+
/* Invalid or freed closure, skip this timeout */
5756
continue;
5857
}
5958

@@ -75,10 +74,15 @@ twin_timeout_t *twin_set_timeout(twin_timeout_proc_t timeout_proc,
7574
if (!timeout)
7675
return NULL;
7776

78-
/* Basic validation of closure pointer at scheduling time */
79-
if (closure && !twin_pointer_valid(closure)) {
80-
free(timeout);
81-
return NULL;
77+
/* Register closure with lifetime tracking if provided */
78+
if (closure) {
79+
if (!_twin_closure_register(closure)) {
80+
/* Failed to register closure */
81+
free(timeout);
82+
return NULL;
83+
}
84+
/* Add reference for this timeout */
85+
_twin_closure_ref(closure);
8286
}
8387

8488
if (!start)
@@ -92,7 +96,16 @@ twin_timeout_t *twin_set_timeout(twin_timeout_proc_t timeout_proc,
9296

9397
void twin_clear_timeout(twin_timeout_t *timeout)
9498
{
99+
if (!timeout)
100+
return;
101+
95102
_twin_queue_delete(&head, &timeout->queue);
103+
104+
/* Unref the closure */
105+
if (timeout->closure)
106+
_twin_closure_unref(timeout->closure);
107+
108+
free(timeout);
96109
}
97110

98111
twin_time_t _twin_timeout_delay(void)

src/toplevel.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ static void _twin_toplevel_destroy(twin_window_t *window)
5353
twin_toplevel_t *toplevel = window->client_data;
5454
twin_event_t event;
5555

56+
/* Mark this toplevel as being freed to prevent new work items */
57+
_twin_closure_mark_for_free(toplevel);
58+
59+
/* Unregister from closure tracking */
60+
_twin_closure_unregister(toplevel);
61+
5662
event.kind = TwinEventDestroy;
5763
(*toplevel->box.widget.dispatch)(&toplevel->box.widget, &event);
5864
}
@@ -68,6 +74,9 @@ void _twin_toplevel_init(twin_toplevel_t *toplevel,
6874
window->event = _twin_toplevel_event;
6975
window->client_data = toplevel;
7076
_twin_box_init(&toplevel->box, 0, window, TwinBoxVert, dispatch);
77+
78+
/* Register this toplevel with closure tracking */
79+
_twin_closure_register(toplevel);
7180
}
7281

7382
twin_toplevel_t *twin_toplevel_create(twin_screen_t *screen,

src/twin_private.h

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,4 +844,54 @@ static inline bool twin_pointer_valid(const void *ptr)
844844
return ptr && (uintptr_t) ptr >= TWIN_POINTER_MIN_VALID;
845845
}
846846

847+
/*
848+
* Closure lifetime management
849+
*
850+
* This system tracks closure pointers used in work queues and timeouts
851+
* to prevent use-after-free bugs when widgets are destroyed while
852+
* callbacks are still queued.
853+
*/
854+
855+
/* Maximum number of tracked closures */
856+
#define TWIN_MAX_CLOSURES 1024
857+
858+
/* Closure tracking entry */
859+
typedef struct _twin_closure_entry {
860+
void *closure; /* Closure pointer */
861+
uint32_t ref_count; /* Reference count */
862+
bool marked_for_free; /* Closure is being freed */
863+
} twin_closure_entry_t;
864+
865+
/* Closure tracking table */
866+
typedef struct _twin_closure_tracker {
867+
twin_closure_entry_t entries[TWIN_MAX_CLOSURES];
868+
int count;
869+
bool initialized; /* Track initialization status */
870+
/* Mutex would go here for thread safety if needed */
871+
} twin_closure_tracker_t;
872+
873+
/* Global closure tracker instance */
874+
extern twin_closure_tracker_t _twin_closure_tracker;
875+
876+
/* Initialize closure tracking system */
877+
void _twin_closure_tracker_init(void);
878+
879+
/* Register a closure with the tracking system */
880+
bool _twin_closure_register(void *closure);
881+
882+
/* Unregister a closure from the tracking system */
883+
void _twin_closure_unregister(void *closure);
884+
885+
/* Increment reference count for a closure */
886+
bool _twin_closure_ref(void *closure);
887+
888+
/* Decrement reference count for a closure */
889+
bool _twin_closure_unref(void *closure);
890+
891+
/* Check if a closure is valid and not marked for deletion */
892+
bool _twin_closure_is_valid(void *closure);
893+
894+
/* Mark a closure as being freed (prevents new references) */
895+
void _twin_closure_mark_for_free(void *closure);
896+
847897
#endif /* _TWIN_PRIVATE_H_ */

0 commit comments

Comments
 (0)