History

js: fix tiny leaks if js_try throws(!)

As it turns out, js_try can throw if it runs out of try-stack (without/before entering either the try part or the catch part). If it happens, then C code which does allocation -> try will leak. In mpv there were two places which do alloc and then try, one of them as part of the autofree system. On both cases the leak is the smallest possible (zero allocation) - talloc_new(NULL); It's very unlikely to trigger - an autofree mpv API should be called when the try-stack is exactly full, and our next try will throw (and guaranteed to get caught at an outer level, but with a leak). Fix that by doing the allocation inside the try block, so that if try throws before it's entered then nothing got allocated/leaked. Mujs internal code also has/had similar leaks, which are getting fixed around this time (July 2021, post mujs 1.1.3). [1] exhaust the try-stack or call-stack, whichever comes first: function kaboom() { try { kaboom() } catch(e) {} }

Avi Halachmi (:avih) committed 5 years ago

Commit 5ed03381 parent 36b7cff1 file · +41 −16Browse files

Showing the changes to player/javascript.c (1 of 1 file). Show all

Unified · Split

player/javascript.c+41−16
@@ -101,16 +101,25 @@ static uint64_t jsL_checkuint64(js_State *J, int idx); · 100 unmodified lines
101101 // padded with undefined if called with less, or bigger if called with more.
102102 //
103103 // - Almost all vm APIs (js_*) may throw an error - a longjmp to the last
104−// recovery/catch point, which could skip releasing resources. Use protected
105−// code (e.g. js_pcall) between aquisition and release. Alternatively, use
106−// the autofree mechanism to manage it more easily. See more details below.
104+// recovery/catch point, which could skip releasing resources. This includes
105+// js_try itself(!), except at the outer-most [1] js_try which is always
106+// entering the try part (and the catch part if the try part throws).
107+// The assumption should be that anything can throw and needs careful setup.
108+// One such automated setup is the autofree mechanism. Details later.
107109 //
108110 // - Unless named s_foo, all the functions at this file (inc. init) which
109111 // touch the vm may throw, but either cleanup resources regardless (mostly
110112 // autofree) or leave allocated resources on caller-provided talloc context
111113 // which the caller should release, typically with autofree (e.g. makenode).
112114 //
113−// - Functions named s_foo (safe foo) never throw, return 0 on success, else 1.
115+// - Functions named s_foo (safe foo) never throw if called at the outer-most
116+// try-levels, or, inside JS C functions - never throw after allocating.
117+// If they didn't throw then they return 0 on success, 1 on js-errors.
118+//
119+// [1] In practice the N outer-most (nested) tries are guaranteed to enter the
120+// try/carch code, where N is the mujs try-stack size (64 with mujs 1.1.3).
121+// But because we can't track try-level at (called-back) JS C functions,
122+// it's only guaranteed when we know we're near the outer-most try level.
114123
115124 /**********************************************************************
116125 * mpv scripting API error handling
@@ -201,13 +210,27 @@ static bool pushed_error(js_State *J, int err, int def) · 84 unmodified lines
201210 // inserted into the vm using af_newcfunction, but otherwise used normally.
202211 //
203212 // To wrap an autofree function af_TARGET in C:
204−// 1. Create a wrapper s_TARGET which runs af_TARGET safely inside js_try.
205−// 2. Use s_TARGET like so (always autofree, and throws if af_TARGET threw):
206−// void *af = talloc_new(NULL);
207−// int r = s_TARGET(J, ..., af); // use J, af where the callee expects.
213+// 1. Create a wrapper s_TARGET which does this:
214+// if (js_try(J))
215+// return 1;
216+// *af = talloc_new(NULL);
217+// af_TARGET(J, ..., *af);
218+// js_endtry(J);
219+// return 0;
220+// 2. Use s_TARGET like so (frees if allocated, throws if {s,af}_TARGET threw):
221+// void *af = NULL;
222+// int r = s_TARGET(J, ..., &af); // use J, af where the callee expects.
208223 // talloc_free(af);
209224 // if (r)
210225 // js_throw(J);
226+//
227+// The reason that the allocation happens inside try/catch is that js_try
228+// itself can throw (if it runs out of try-stack) and therefore the code
229+// inside the try part is not reached - but neither is the catch part(!),
230+// and instead it throws to the next outer catch - but before we've allocated
231+// anything, hence no leaks on such case. If js_try did get entered, then the
232+// allocation happened, and then if af_TARGET threw then s_TARGET will catch
233+// it (and return 1) and we'll free if afterwards.
211234
212235 // add_af_file, add_af_dir, add_af_mpv_alloc take a valid FILE*/DIR*/char* value
213236 // respectively, and fclose/closedir/mpv_free it when the parent is freed.
@@ -266,11 +289,12 @@ static mpv_node *new_af_mpv_node(void *parent) · 52 unmodified lines
266289 typedef void (*af_CFunction)(js_State*, void*);
267290
268291 // safely run autofree js c function directly
269−static int s_run_af_jsc(js_State *J, af_CFunction fn, void *af)
292+static int s_run_af_jsc(js_State *J, af_CFunction fn, void **af)
270293 {
271294 if (js_try(J))
272295 return 1;
273− fn(J, af);
296+ *af = talloc_new(NULL);
297+ fn(J, *af);
274298 js_endtry(J);
275299 return 0;
276300 }
@@ -285,8 +309,8 @@ static void script__autofree(js_State *J) · 8 unmodified lines
285309 af_CFunction fn = (af_CFunction)js_touserdata(J, -1, "af_fn");
286310 js_pop(J, 2);
287311
288− void *af = talloc_new(NULL);
289− int r = s_run_af_jsc(J, fn, af);
312+ void *af = NULL;
313+ int r = s_run_af_jsc(J, fn, &af);
290314 talloc_free(af);
291315 if (r)
292316 js_throw(J);
@@ -357,11 +381,12 @@ static void af_push_file(js_State *J, const char *fname, int limit, void *af) · 64 unmodified lines
357381 }
358382
359383 // Safely run af_push_file.
360−static int s_push_file(js_State *J, const char *fname, int limit, void *af)
384+static int s_push_file(js_State *J, const char *fname, int limit, void **af)
361385 {
362386 if (js_try(J))
363387 return 1;
364− af_push_file(J, fname, limit, af);
388+ *af = talloc_new(NULL);
389+ af_push_file(J, fname, limit, *af);
365390 js_endtry(J);
366391 return 0;
367392 }
@@ -369,8 +394,8 @@ static int s_push_file(js_State *J, const char *fname, int limit, void *af) · 1 unmodified line
369394 // Called directly, push up to limit bytes of file fname (from builtin/os).
370395 static void push_file_content(js_State *J, const char *fname, int limit)
371396 {
372− void *af = talloc_new(NULL);
373− int r = s_push_file(J, fname, limit, af);
397+ void *af = NULL;
398+ int r = s_push_file(J, fname, limit, &af);
374399 talloc_free(af);
375400 if (r)
376401 js_throw(J);

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t