You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A full read-through of every C source in this repository (c/src/*.c, java/android/nnstreamer/src/main/jni/*.c, tests/capi/unittest_util.c, ~18k lines) for memory-safety defects. Every item below was verified by tracing the actual control flow; nothing here is speculative.
All file:line references are against commit 7ead26abf0b72fa637f03f66fcb47a7802e6e462 ([C-Api] handle message callback, current main). Line numbers will drift as fixes land; use the function names to relocate.
Severity:
HIGH – crash / heap corruption / deadlock on a normal usage path
MEDIUM – needs a specific condition or a race
LOW – leak, theoretical OOB, or hard-to-hit race
Each task is independent; fix them one at a time (one PR per item or per small group), tick the box, and reference this issue in the PR. A suggested fix is given for each, but verify against the current code before applying.
Conventions used in this repo that matter for several fixes:
_ml_tensors_data_destroy_internal(data, free_data) (c/src/ml-api-common.c:692): if data->destroy is set it calls that callback; otherwise (when free_data) it g_frees every tensors[i].data for i < num_tensors.
ml_pipeline_src_input_data() (c/src/ml-api-inference-pipeline.c:1683): on validation failures before the push it jumps to dont_destroy_data: and does not free ML_PIPELINE_BUF_POLICY_AUTO_FREE data; after the push it always frees it.
ML_SINGLE_GET_VALID_HANDLE_LOCKED (c/src/ml-api-inference-single.c:50): takes global G_LOCK(magic) then single_h->mutex (non-recursive GMutex).
HIGH
H1. single-shot: self-deadlock in invoke_thread after an invoke timeout
Cause: on timeout, set_destroy_notify(single_h, _out, TRUE) sets _out->destroy = ml_single_destroy_notify_cb (when the sub-plugin has allocate_in_invoke). Later invoke_thread holds single_h->mutex (line 541) and calls __process_output() → ml_tensors_data_destroy(output) → ml_single_destroy_notify_cb → ML_SINGLE_GET_VALID_HANDLE_LOCKED → g_mutex_lock(&single_h->mutex) again. Non-recursive mutex → permanent wait, while holding global magic lock, so every single-shot API in the process blocks. Same at lines 550 and 579.
Repro: ml_single_set_timeout(h, N), model slower than N once, framework with allocate_in_invoke == TRUE (e.g. tensorflow, custom filters that allocate).
Fix: never call ml_tensors_data_destroy() on a list-tracked output while holding single_h->mutex. In the thread: __destroy_notify(output, single_h); ((ml_tensors_data_s*)output)->destroy = NULL; ml_tensors_data_destroy(output); or release the mutex around the destroy.
H2. single-shot: double free of framework-allocated output when app follows documented order (close, then destroy)
Cause: nnstreamer-single.h:116-118 tells apps to call ml_single_close() before ml_tensors_data_destroy() on outputs. __destroy_notify hands the buffers back via klass->destroy_notify() and sets data->destroy = NULL but leaves tensors[i].data pointing at the released memory. The app's later ml_tensors_data_destroy() then hits the destroy == NULL branch and g_frees each tensors[i].data → double free / free of non-GLib memory.
Fix: in __destroy_notify, after destroy_notify, set data->tensors[i].data = NULL; data->tensors[i].size = 0; for i < data->num_tensors.
H3. training-offloading: sink-callback user_data freed before the pipeline is stopped/destroyed (UAF)
Cause: g_hash_table_destroy(training_s->node_table) (frees every ml_service_node_info_s) runs before ml_pipeline_destroy(training_s->pipeline_h), and ml_pipeline_stop() is never called. A buffer reaching the sink in that window uses freed node_info.
Repro: receiver in PLAYING, app calls ml_service_destroy() without ml_service_stop().
Cause: the out handle passed to the callback wraps memory owned by tensor_filter. nns_parse_tensors_data with clone == TRUE and a pre-existing handle does if (data->tensors[i].data && size != data_size) g_clear_pointer(&data->tensors[i].data, g_free) then g_mallocs a replacement. The freed pointer was GstMemory-mapped memory (invalid free); the new buffer is never seen by the framework (leak) and the original is later unmapped by GStreamer (UAF). If the Java array has more tensors than out_info.num_tensors, the extra indices are g_malloced and leaked on every invoke.
Repro: Java CustomFilter.invoke() returns a TensorsData whose buffer capacity differs from the declared output size (e.g. allocated from a different TensorsInfo, or FLEXIBLE format where Java does not validate size).
Fix: in nns_customfilter_invoke, validate the returned object (tensor count == out->num_tensors, each GetDirectBufferCapacity == out->tensors[i].size) and return -1 on mismatch. In nns_parse_tensors_data, never free/reallocate a pre-existing buffer when created == FALSE; copy MIN(size, data_size) or fail.
H5. Android JNI: private data freed before callbacks are stopped (UAF / NULL deref on streaming thread)
File: java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:71-111 (nns_free_element_data: priv_destroy_func runs before ml_pipeline_sink_unregister), :238-291 (nns_destroy_pipe_info: priv_data = NULL and g_hash_table_destroy(element_handles) run before ml_pipeline_destroy / ml_service_destroy / ml_single_close), java/.../nnstreamer-native-pipeline.c:113, 153, 160, 196, 203, java/.../nnstreamer-native-service.c:179, 205
Cause: Pipeline.close() calls nativeDestroy without stopping. nns_sink_data_cb runs on the tensor_sink streaming thread and reads item->priv_data (freed: ml_tensors_info_destroy(priv->out_info) clears its mutex, DeleteGlobalRef(out_info_obj)) and pipe_info->priv_data (NULL) → priv->mid_sink_cb. Same for the state callback and nns_service_event_cb (invoked from the ml-service message thread without mls->lock).
Fix: reverse the order. In nns_free_element_data, unregister/release the C-API handle first, then priv_destroy_func. In nns_destroy_pipe_info, destroy the underlying handle (ml_pipeline_destroy etc.) before freeing priv_data and the element table. Add priv NULL checks in the callbacks.
H6. Android JNI: CustomFilter.close() frees pipe_info while the filter is still registered (unregister failure ignored)
Cause: ml_pipeline_custom_easy_filter_unregister() returns ML_ERROR_INVALID_PARAMETER and keeps the filter registered (with c->pdata == pipe_info) when ref_count > 0. The JNI ignores the return value, deletes global refs and g_free(pipe_info). The next buffer calls nns_customfilter_invoke(in, out, user_data = freed pipe_info).
Repro: close the CustomFilter while a Pipeline using it is alive.
Fix: check the return value; if unregister fails, do not free pipe_info and propagate the error to Java.
MEDIUM
M1. ml_service_query_request / ml_service_pipeline_get_state: service type not checked → wrong priv struct dereferenced
Cause: _ml_service_handle_is_valid() accepts every ml_service_type_e. Passing an EXTENSION handle (from ml_service_new) to ml_service_query_request interprets ml_extension_s as _ml_service_query_s; query->src_h is actually the timeout field → ml_pipeline_src_input_data segfaults in handle_init. ml_service_pipeline_get_state similarly sends a bogus server->id to the agent.
Fix: reject mls->type != ML_SERVICE_TYPE_CLIENT_QUERY (resp. != ML_SERVICE_TYPE_SERVER_PIPELINE) in the public API and/or the internal function.
M2. ml_service_get_information: g_strdup(val) outside the lock → UAF race with ml_service_set_information
File: c/src/ml-api-service.c:687-696
Cause: ml_option_get returns the internal string; the lock is released; then g_strdup(val). A concurrent ml_service_set_information on the same key does g_hash_table_insert → _ml_info_value_free → g_free(old).
Fix: move *value = g_strdup(val) inside the locked region.
M3. Cloned input leaked when ml_pipeline_src_input_data fails before the push (extension + Android JNI)
Cause: pre-push validation failures (num_tensors out of range, ML_ERROR_TRY_AGAIN when caps are not negotiated yet, tensor count/size mismatch) return without freeing AUTO_FREE data, but both callers assume ownership was transferred. Each failing ml_service_request() / Pipeline.inputData() leaks the whole ml_tensors_data_clone (handle + all buffers + cloned info), and the extension gives the app no error return, so it repeats.
Fix (choose one, document it): (a) make ml_pipeline_src_input_data honor AUTO_FREE on every error path so ownership is unambiguous; or (b) in the extension use if (status == ML_ERROR_NONE) msg->input = NULL; and in JNI pre-validate and free on the pre-push failures. Note that post-push errors (GST_FLOW_FLUSHING/EOS, lines 1788-1801) already freed the data, so callers cannot simply free on any error today.
M4. offloading: curl download buffer used as a NUL-terminated string (heap over-read)
Cause: data/data_len come from nns_edge_data_get(data_h, 0, &data, &data_len) (:364). For MODEL_RAW/REPLY the payload is binary; _ml_error_report("... %s", (gchar *) data) walks past the buffer. For PIPELINE_RAW/MODEL_URI the peer is not obliged to send a trailing NUL, yet ml_service_pipeline_set, g_strdup(data) and curl URL parsing treat it as a string.
Fix: never format data with %s (log service_key/name instead). For string payloads, require data_len > 0 && ((gchar *) data)[data_len - 1] == '\0' or use g_strndup(data, data_len).
M6. training-offloading: received_lock/received_cond cleared before joining the checker thread
File: c/src/ml-api-service-training-offloading.c:901-907, :535-545 (_check_received_data_thread sleeps 100 ms then locks received_lock), :583-587
Cause: g_cond_clear/g_mutex_clear run before g_thread_join. The thread can outlive _training_offloading_check_received_data (second ml_service_start on a receiver skips the wait because is_received is still TRUE; spurious wakeup at :585-587 returns early). Locking a cleared GMutex is UB.
Fix: join first, then clear. Also reset is_received = FALSE before spawning and replace the spurious-wakeup break with a proper predicate loop.
M7. offloading: training_s freed while the nnstreamer-edge receive thread can still dispatch into it
File: c/src/ml-api-service-offloading.c:704-718 (_ml_service_offloading_release_internal: _ml_service_training_offloading_destroy before nns_edge_release_handle), :385 (offloading_mode read without lock), c/src/ml-api-service-training-offloading.c:812-836 (_ml_service_training_offloading_process_received_data writes training_s->receiver_pipe_json_str)
Cause: the edge event callback runs on the edge thread; destroy frees training_s (:934) while edge_h is intentionally kept alive. A PIPELINE_RAW/REPLY message arriving during ml_service_destroy writes into freed memory.
Fix: stop delivery first (nns_edge_set_event_callback(edge_h, NULL, NULL) or a running flag under a mutex) before _ml_service_training_offloading_destroy; guard offloading_mode/priv reads with mls->lock.
M8. single-shot: ml_single_set_info_in_handle overwrites in_info with the OUTPUT info
File: c/src/ml-api-inference-single.c:884-899, :670-698 (ml_single_update_info returns output info in *out_info), :764-785 (ml_single_set_gst_info already sets both in_info/out_info)
Cause: for is_input == TRUE with a non-matching tensors_info, ml_single_update_info(single, tensors_info, &info) fills info with the output info, then the code does gst_tensors_info_free(dest); _ml_tensors_info_copy_from_ml(dest, info) with dest == &single_h->in_info. Afterwards single_h->in_info == out_info; __setup_in_out_tensors and _ml_single_invoke_validate_data size/validate input against output dims → an app that sizes input from ml_single_get_input_info() passes validation with a too-small buffer → over-read inside the sub-plugin. Hidden when in/out shapes coincide (e.g. add.tflite).
Repro: ml_single_open* with an input_info that differs from the model's configured input on a framework whose set_input_info succeeds (tflite resize), with different input/output sizes.
Fix: after a successful ml_single_update_info, just ml_tensors_info_destroy(info) and skip the copy into dest (ml_single_set_gst_info already updated both).
Cause: gst_tensor_meta_info_parse_header(&meta, map[i].data) return value ignored; _data->tensors[i].size = map[i].size - hsize wraps when the memory is shorter than the flex header. The app callback receives a ~2^64 size; the parser reads past the mapped memory.
Repro: any pipeline with other/tensors,format=flexible caps in front of tensor_sink/appsink and a short buffer (e.g. filesrc ! capsfilter ... ! tensor_sink).
Fix: check the parse result and map[i].size >= hsize; goto error otherwise.
M10. single-shot: uninitialized GError * on thread-creation failure
Cause: g_thread_try_new(..., &error) refuses to set a non-NULL *error; on failure error->message / g_clear_error(&error) dereference stack garbage.
Fix: GError *error = NULL;
M11. single-shot: ml_single_close (or spurious wakeup) wakes a pending timed invoke → dangling output handed to the app
File: c/src/ml-api-inference-single.c:1580-1588, :1376-1377 (ml_single_close sets JOIN_REQUESTED and broadcasts), :546-551 (thread destroys output when JOIN_REQUESTED)
Cause: if (g_cond_wait_until(...)) status = single_h->status; does not check state. Woken by close, the caller reads the stale status (e.g. ML_ERROR_NONE) and returns _out while the thread later destroys it.
Fix: wait in while (single_h->state == RUNNING); after waking, if state == JOIN_REQUESTED return ML_ERROR_STREAMS_PIPE and do not publish _out.
M12. single-shot: ml_single_invoke_fast timeout → invoke thread keeps writing into the app-owned output buffer
Cause: with need_alloc == FALSE, _out = *output (user buffer). On timeout the API returns but __invoke(single_h, input, output, FALSE) continues writing into it. nnstreamer-single.h:155-173 does not require the app to keep output alive after a timeout → write-after-free if the app frees/reuses it.
Fix: for !need_alloc, either clone the output descriptor and copy back on completion, or document the lifetime requirement and drop the result on the thread when the caller timed out.
M13. ml_pipeline_destroy: element nodes freed while the pipeline is still PLAYING (in-flight sink callback uses freed elem)
File: c/src/ml-api-inference-pipeline.c:1193-1224, :544-606 (cleanup_node), :307 (cb_sink_event locks elem->lock); existing @todo CRITICAL comments at :286 and :549
Cause: g_hash_table_destroy(p->namednodes) runs before gst_element_set_state(PAUSED/NULL). cleanup_node unlocks e->lock, clears it and g_free(e); a streaming thread waiting on that lock in cb_sink_event resumes on freed memory. g_signal_handler_disconnect does not wait for a running handler.
Fix: bring the pipeline to NULL before destroying namednodes, or refcount ml_pipeline_element and have callbacks hold a ref.
M14. Tizen: res_handles hash table and every pipeline_resource_s value leaked per pipeline
File: c/src/ml-api-inference-tizen-privilege-check.c:692-706 (ml_tizen_mm_res_release), :749 (table created with value destroy NULL), :809-819 (g_new0(pipeline_resource_s) + g_strdup inserted)
Cause: g_hash_table_remove_all frees keys only; the table itself is never unref'd before g_free(mm_handle).
Fix: create the table with a value destroy that frees mm_res->type and mm_res; g_hash_table_unref(mm_handle->res_handles) before g_free(mm_handle).
M15. Tizen 9+: resource manager re-registered on every ml_tizen_mm_res_acquire (leak + wrong handle on dealloc)
Fix: if (mm_handle->rm_h) return ML_ERROR_NONE; at the top, mirroring the pre-9 code.
LOW
L1.c/src/ml-api-service-agent-client.c:90, 164 — g_autoptr (GList) members reassigned inside the loop in _build_ml_info_from_json_cstr; n-1 lists leak for arrays with ≥2 elements (ml_service_model_get_all, ml_service_resource_get). Fix: declare inside the loop and g_list_free per iteration.
L2.c/src/ml-api-service-agent-client.c:73 (Tizen) — json_generator_set_root (gen, json_builder_get_root (builder)): the transfer-full root node is never unref'd. Fix: g_autoptr (JsonNode) root = json_builder_get_root (builder);.
L3.c/src/ml-api-common.c:1382-1391 — ml_strerror (INT_MIN): errnum * -1 overflows, both guards fail, strerrors[INT_MIN] read. Fix: reject INT_MIN or compute with unsigned.
L4.c/src/ml-api-common.c:160-179 — _ml_tensors_info_create_from: when ml_tensors_info_clone fails, *out is left allocated and returned with an error (ml_tensors_data_get_info does not clean it up). Fix: destroy *out and set NULL on failure.
L5.c/src/ml-api-common.c:1099-1112 — ml_tensors_data_set_tensor_data on a flexible sink-callback handle: tensors[i].data points into mapped GstMemory (ml-api-inference-pipeline.c:370), and a differing data_size triggers g_free of that interior pointer. Fix: track buffer ownership in ml_tensors_data_s and refuse to realloc non-owned buffers.
L6.c/src/ml-api-common-tizen-feature-check.c:269-285 — unsynchronized lazy init of feature_info; two first-time callers can double-allocate (leak) and unlock a different mutex than they locked. Fix: g_once_init_enter/leave or a static lock.
L7.c/src/ml-api-inference-pipeline.c:2872, 2928-2932 — ml_pipeline_custom_easy_filter_register: in_info/out_info (GstTensorsInfo copies with strdup'd names / extra) never gst_tensors_info_freed on any path. Fix: free both before exit:.
L8.c/src/ml-api-inference-single.c:187, 248-252 — _ml_get_nnfw_subplugin_name indexes ml_nnfw_subplugin_name[nnfw] without a range check; public ml_check_nnfw_availability_full only rejects ML_NNFW_TYPE_ANY, so an out-of-range enum reads past the array and passes garbage to nnstreamer_filter_find. Fix: bounds-check → return NULL.
L9.c/src/ml-api-inference-single.c:1580-1588, 1620 with :532-535 — if the timeout fires before invoke_thread picks up the job, line 1620 clears single_h->input/output; the thread then sees NULLs, so the cloned _in is never freed and _out stays in destroy_data_list until close. Fix: only clear when they still equal _in/_out, and destroy them in that case.
L10.c/src/ml-api-inference-pipeline.c:877-897 — iterate_element: gst_element_get_factory may return NULL; for is_internal pipelines get_elem_type_from_name (…, NULL) → g_str_hash (NULL) NULL deref. Fix: skip elements without a factory.
L11.c/src/ml-api-inference-pipeline.c:2152-2196 — ml_pipeline_switch_get_pad_list: on GST_ITERATOR_ERROR, done is not set and the final g_list_free (dllist) leaks every gst_pad_get_name string. Fix: set done = TRUE, g_list_free_full (dllist, g_free) on error.
L12.c/src/ml-api-inference-pipeline.c:1752-1778 — ml_pipeline_src_input_data: if _data->info == NULL, _ml_tensors_info_copy_from_ml returns early and gst_info is used/freed uninitialized. Fix: gst_tensors_info_init (&gst_info) and check the return.
L13.c/src/ml-api-inference-pipeline.c:137-150, 675-681 — pipe_custom_find_data returns the pipe_custom_data_s * after dropping g_ml_pipe_lock; a concurrent ml_pipeline_custom_easy_filter_unregister (ref_count 0) frees it before ml_pipeline_custom_filter_ref runs. Fix: take the ref inside the locked region.
L14.c/src/ml-api-inference-pipeline.c:1203-1207, 1225-1229 — ml_pipeline_destroy error returns leave a half-destroyed handle (namednodes == NULL, bus handler connected, p unfreed); a retry hits g_hash_table_destroy (NULL). Fix: make destroy idempotent / free unconditionally.
L15.c/src/ml-api-service-training-offloading.c:464-496 — _training_offloading_request_pipeline: in the "pipeline" branch pipeline = g_strdup (transfer_data); transfer_data = NULL; leaks the first copy; entries matching neither APP_RW_PATH nor "pipeline" are overwritten next iteration without free; a second "pipeline" entry leaks the previous service_name/pipeline. Fix: pipeline = transfer_data; transfer_data = NULL; and g_clear_pointer (&transfer_data, g_free) every iteration.
L16.c/src/ml-api-service-training-offloading.c:671-672, 726-727 — ml_pipeline_construct (..., &training_s->pipeline_h) on repeated ml_service_start overwrites a live pipeline handle (never destroyed). Fix: early-return or destroy the old pipeline if pipeline_h != NULL.
L17.c/src/ml-api-service-training-offloading.c:538-543, 618-627, 819-820 — receiver_pipe_json_str is read (checker thread), replaced with g_free (edge thread) and replaced via _ml_replace_string (API thread) with no lock → UAF / double free if the sender retransmits. Fix: protect all accesses with received_lock (or mls->lock).
L18.c/src/ml-api-service-training-offloading.c:160-162 — node-type is optional at the offloading level (ml-api-service-offloading.c:137), so val may be NULL in g_ascii_strcasecmp (val, "sender") (NULL deref with G_DISABLE_CHECKS; silently selects SENDER otherwise). Fix: if (!STR_IS_VALID (val)) return ML_ERROR_INVALID_PARAMETER;.
L19.c/src/ml-api-service-training-offloading.c:535 — usec = training_s->time_limit * 1000000; with gint time_limit from JSON overflows for values > 2147; negative values bypass the wait. Fix: gint64 math and validate time_limit > 0.
L20.c/src/ml-api-service-offloading.c:923-929 — _ml_service_offloading_convert_to_option: ml_option_set (tmp, key, g_strdup (val), g_free) leaks the dup when _ml_info_set_value rejects an empty key without taking ownership. Fix: dup into a local and free on failure.
L21.c/src/ml-api-service-extension.c:671-678 — g_cond_wait (&mls->cond, &mls->lock) after g_thread_new has no predicate; a spurious wakeup returns before the thread sets ext->running = TRUE, and a subsequent destroy (running = FALSE + join) can hang forever because the thread flips it back. Fix: while (!ext->running) g_cond_wait (...).
L22.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:18-66, 220 — threads attached via AttachCurrentThread in nns_get_jni_env are never detached (ART aborts when an attached native thread exits; GStreamer/GLib pool threads do exit), and pthread_key_create result is unchecked (on failure jni_env == 0 and pthread_setspecific (0, env) clobbers another key). Fix: a process-wide key created once with a destructor calling DetachCurrentThread; check the return value.
L23.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:545 — nns_parse_tensors_data overwrites data->num_tensors with the Java array length without checking it against data->info; a TensorsData whose list is shorter than its info (e.g. after TensorsData.close()) reaches ml_service_request → ml_tensors_data_clone (ml-api-common.c:889-909) → memcpy from tensors[i].data == NULL. Fix: fail if array length != info->info.num_tensors (and, for STATIC, capacity != tensors[i].size).
L24.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:455-461 — flexible-format error path deletes data_arr but not obj_data (local ref leak on a never-detached callback thread; 512-entry table can be exhausted). Fix: DeleteLocalRef (obj_data) before returning.
L25.java/android/nnstreamer/src/main/jni/nnstreamer-native-common.c:491-498 — mlops_db_path leaked when ml_agent_initialize fails (goto done skips g_free). Fix: free before the goto.
L26.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:452-453 — CallVoidMethod (…, mid_update_data, i, data->tensors[i].size): size_t passed through varargs for a (II)V method (UB; works by accident on LE 64-bit). Fix: cast to (jint).
Reviewed and found safe (for reference, so nobody re-audits these)
Recent commits 7ead26a, ca4725e, 9d600fb, e7fedae, 97227a8: _in is NULL-initialized and unlocked only if (_in); num_tensors is checked against NNS_TENSOR_SIZE_LIMIT before indexing; ml_information_h in _ml_service_invoke_event_message is destroyed on every path; the user callback is cleared under mls->lock before any teardown; _ml_service_offloading_request_raw's no-alloc handle is destroyed with free_data = FALSE.
Summary
A full read-through of every C source in this repository (
c/src/*.c,java/android/nnstreamer/src/main/jni/*.c,tests/capi/unittest_util.c, ~18k lines) for memory-safety defects. Every item below was verified by tracing the actual control flow; nothing here is speculative.All
file:linereferences are against commit7ead26abf0b72fa637f03f66fcb47a7802e6e462([C-Api] handle message callback, currentmain). Line numbers will drift as fixes land; use the function names to relocate.Severity:
Each task is independent; fix them one at a time (one PR per item or per small group), tick the box, and reference this issue in the PR. A suggested fix is given for each, but verify against the current code before applying.
Conventions used in this repo that matter for several fixes:
_ml_tensors_data_destroy_internal(data, free_data)(c/src/ml-api-common.c:692): ifdata->destroyis set it calls that callback; otherwise (whenfree_data) itg_frees everytensors[i].datafori < num_tensors.ml_pipeline_src_input_data()(c/src/ml-api-inference-pipeline.c:1683): on validation failures before the push it jumps todont_destroy_data:and does not freeML_PIPELINE_BUF_POLICY_AUTO_FREEdata; after the push it always frees it.ML_SINGLE_GET_VALID_HANDLE_LOCKED(c/src/ml-api-inference-single.c:50): takes globalG_LOCK(magic)thensingle_h->mutex(non-recursiveGMutex).HIGH
H1. single-shot: self-deadlock in
invoke_threadafter an invoke timeoutc/src/ml-api-inference-single.c:541-559(invoke_thread),:1587(ml_single_invoke_internaltimeout branch),:359-376(__destroy_notify),:382-413(ml_single_destroy_notify_cb)set_destroy_notify(single_h, _out, TRUE)sets_out->destroy = ml_single_destroy_notify_cb(when the sub-plugin hasallocate_in_invoke). Laterinvoke_threadholdssingle_h->mutex(line 541) and calls__process_output()→ml_tensors_data_destroy(output)→ml_single_destroy_notify_cb→ML_SINGLE_GET_VALID_HANDLE_LOCKED→g_mutex_lock(&single_h->mutex)again. Non-recursive mutex → permanent wait, while holding globalmagiclock, so every single-shot API in the process blocks. Same at lines 550 and 579.ml_single_set_timeout(h, N), model slower than N once, framework withallocate_in_invoke == TRUE(e.g. tensorflow, custom filters that allocate).ml_tensors_data_destroy()on a list-tracked output while holdingsingle_h->mutex. In the thread:__destroy_notify(output, single_h); ((ml_tensors_data_s*)output)->destroy = NULL; ml_tensors_data_destroy(output);or release the mutex around the destroy.H2. single-shot: double free of framework-allocated output when app follows documented order (close, then destroy)
c/src/ml-api-inference-single.c:359-376(__destroy_notify),:1389-1391(ml_single_close→g_list_foreach(destroy_data_list, __destroy_notify)),c/src/ml-api-common.c:713-715nnstreamer-single.h:116-118tells apps to callml_single_close()beforeml_tensors_data_destroy()on outputs.__destroy_notifyhands the buffers back viaklass->destroy_notify()and setsdata->destroy = NULLbut leavestensors[i].datapointing at the released memory. The app's laterml_tensors_data_destroy()then hits thedestroy == NULLbranch andg_frees eachtensors[i].data→ double free / free of non-GLib memory.__destroy_notify, afterdestroy_notify, setdata->tensors[i].data = NULL; data->tensors[i].size = 0;fori < data->num_tensors.H3. training-offloading: sink-callback
user_datafreed before the pipeline is stopped/destroyed (UAF)c/src/ml-api-service-training-offloading.c:914-926(_ml_service_training_offloading_destroy),:293-294(ml_pipeline_sink_register(..., _ml_service_pipeline_sink_cb, node_info, ...)),c/src/ml-api-service.c:954(_ml_service_pipeline_sink_cbdereferencesnode_info->mls,node_info->name)g_hash_table_destroy(training_s->node_table)(frees everyml_service_node_info_s) runs beforeml_pipeline_destroy(training_s->pipeline_h), andml_pipeline_stop()is never called. A buffer reaching the sink in that window uses freednode_info.ml_service_destroy()withoutml_service_stop()._ml_service_extension_destroy(c/src/ml-api-service-extension.c:716-725):ml_pipeline_stop→ml_pipeline_destroy→ then destroynode_table.H4. Android JNI: CustomFilter output size mismatch →
g_freeof GStreamer-mapped memory (heap corruption) + leakjava/android/nnstreamer/src/main/jni/nnstreamer-native-customfilter.c:125(nns_customfilter_invoke→nns_parse_tensors_data(..., clone=TRUE, priv->out_info, &out)),java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:555-564,c/src/ml-api-inference-pipeline.c:2847-2849(ml_pipeline_custom_invokesetsout_data->tensors[i].data = out[i].data)outhandle passed to the callback wraps memory owned by tensor_filter.nns_parse_tensors_datawithclone == TRUEand a pre-existing handle doesif (data->tensors[i].data && size != data_size) g_clear_pointer(&data->tensors[i].data, g_free)theng_mallocs a replacement. The freed pointer was GstMemory-mapped memory (invalid free); the new buffer is never seen by the framework (leak) and the original is later unmapped by GStreamer (UAF). If the Java array has more tensors thanout_info.num_tensors, the extra indices areg_malloced and leaked on every invoke.CustomFilter.invoke()returns aTensorsDatawhose buffer capacity differs from the declared output size (e.g. allocated from a differentTensorsInfo, or FLEXIBLE format where Java does not validate size).nns_customfilter_invoke, validate the returned object (tensor count ==out->num_tensors, eachGetDirectBufferCapacity == out->tensors[i].size) and return -1 on mismatch. Innns_parse_tensors_data, never free/reallocate a pre-existing buffer whencreated == FALSE; copyMIN(size, data_size)or fail.H5. Android JNI: private data freed before callbacks are stopped (UAF / NULL deref on streaming thread)
java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:71-111(nns_free_element_data:priv_destroy_funcruns beforeml_pipeline_sink_unregister),:238-291(nns_destroy_pipe_info:priv_data = NULLandg_hash_table_destroy(element_handles)run beforeml_pipeline_destroy/ml_service_destroy/ml_single_close),java/.../nnstreamer-native-pipeline.c:113, 153, 160, 196, 203,java/.../nnstreamer-native-service.c:179, 205Pipeline.close()callsnativeDestroywithout stopping.nns_sink_data_cbruns on the tensor_sink streaming thread and readsitem->priv_data(freed:ml_tensors_info_destroy(priv->out_info)clears its mutex,DeleteGlobalRef(out_info_obj)) andpipe_info->priv_data(NULL) →priv->mid_sink_cb. Same for the state callback andnns_service_event_cb(invoked from the ml-service message thread withoutmls->lock).nns_free_element_data, unregister/release the C-API handle first, thenpriv_destroy_func. Innns_destroy_pipe_info, destroy the underlying handle (ml_pipeline_destroyetc.) before freeingpriv_dataand the element table. AddprivNULL checks in the callbacks.H6. Android JNI:
CustomFilter.close()freespipe_infowhile the filter is still registered (unregister failure ignored)java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:262-264, 290,c/src/ml-api-inference-pipeline.c:2979-2985ml_pipeline_custom_easy_filter_unregister()returnsML_ERROR_INVALID_PARAMETERand keeps the filter registered (withc->pdata == pipe_info) whenref_count > 0. The JNI ignores the return value, deletes global refs andg_free(pipe_info). The next buffer callsnns_customfilter_invoke(in, out, user_data = freed pipe_info).CustomFilterwhile aPipelineusing it is alive.pipe_infoand propagate the error to Java.MEDIUM
M1.
ml_service_query_request/ml_service_pipeline_get_state: service type not checked → wrongprivstruct dereferencedc/src/ml-api-service.c:800-823,c/src/ml-api-service-query.c:205-207,c/src/ml-api-service-agent-client.c:380-386_ml_service_handle_is_valid()accepts everyml_service_type_e. Passing an EXTENSION handle (fromml_service_new) toml_service_query_requestinterpretsml_extension_sas_ml_service_query_s;query->src_his actually thetimeoutfield →ml_pipeline_src_input_datasegfaults inhandle_init.ml_service_pipeline_get_statesimilarly sends a bogusserver->idto the agent.mls->type != ML_SERVICE_TYPE_CLIENT_QUERY(resp.!= ML_SERVICE_TYPE_SERVER_PIPELINE) in the public API and/or the internal function.M2.
ml_service_get_information:g_strdup(val)outside the lock → UAF race withml_service_set_informationc/src/ml-api-service.c:687-696ml_option_getreturns the internal string; the lock is released; theng_strdup(val). A concurrentml_service_set_informationon the same key doesg_hash_table_insert→_ml_info_value_free→g_free(old).*value = g_strdup(val)inside the locked region.M3. Cloned input leaked when
ml_pipeline_src_input_datafails before the push (extension + Android JNI)c/src/ml-api-service-extension.c:224-226(msg->input = NULLunconditionally afterml_pipeline_src_input_data(..., AUTO_FREE)),java/android/nnstreamer/src/main/jni/nnstreamer-native-pipeline.c:651-661,c/src/ml-api-inference-pipeline.c:1706-1747(error paths →dont_destroy_data)num_tensorsout of range,ML_ERROR_TRY_AGAINwhen caps are not negotiated yet, tensor count/size mismatch) return without freeing AUTO_FREE data, but both callers assume ownership was transferred. Each failingml_service_request()/Pipeline.inputData()leaks the wholeml_tensors_data_clone(handle + all buffers + cloned info), and the extension gives the app no error return, so it repeats.ml_pipeline_src_input_datahonor AUTO_FREE on every error path so ownership is unambiguous; or (b) in the extension useif (status == ML_ERROR_NONE) msg->input = NULL;and in JNI pre-validate and free on the pre-push failures. Note that post-push errors (GST_FLOW_FLUSHING/EOS, lines 1788-1801) already freed the data, so callers cannot simply free on any error today.M4. offloading: curl download buffer used as a NUL-terminated string (heap over-read)
c/src/ml-api-service-offloading.c:221-232(curl_mem_write_cb),:455-461(ML_SERVICE_OFFLOADING_TYPE_PIPELINE_URI)g_byte_array_appendonly;GByteArrayis not zero-terminated.ml_service_pipeline_set(service_key, (gchar *) array->data)reads pastarray->len.g_byte_array_append(array, (const guint8 *) "", 1);or useg_strndup(array->data, array->len).M5. offloading: remote edge payload printed with
%s/ used as a C string (over-read)c/src/ml-api-service-offloading.c:401, 418, 428-429, 445-446, 469,c/src/ml-api-service-training-offloading.c:820data/data_lencome fromnns_edge_data_get(data_h, 0, &data, &data_len)(:364). For MODEL_RAW/REPLY the payload is binary;_ml_error_report("... %s", (gchar *) data)walks past the buffer. For PIPELINE_RAW/MODEL_URI the peer is not obliged to send a trailing NUL, yetml_service_pipeline_set,g_strdup(data)and curl URL parsing treat it as a string.datawith%s(logservice_key/nameinstead). For string payloads, requiredata_len > 0 && ((gchar *) data)[data_len - 1] == '\0'or useg_strndup(data, data_len).M6. training-offloading:
received_lock/received_condcleared before joining the checker threadc/src/ml-api-service-training-offloading.c:901-907,:535-545(_check_received_data_threadsleeps 100 ms then locksreceived_lock),:583-587g_cond_clear/g_mutex_clearrun beforeg_thread_join. The thread can outlive_training_offloading_check_received_data(secondml_service_starton a receiver skips the wait becauseis_receivedis still TRUE; spurious wakeup at:585-587returns early). Locking a clearedGMutexis UB.is_received = FALSEbefore spawning and replace the spurious-wakeupbreakwith a proper predicate loop.M7. offloading:
training_sfreed while the nnstreamer-edge receive thread can still dispatch into itc/src/ml-api-service-offloading.c:704-718(_ml_service_offloading_release_internal:_ml_service_training_offloading_destroybeforenns_edge_release_handle),:385(offloading_moderead without lock),c/src/ml-api-service-training-offloading.c:812-836(_ml_service_training_offloading_process_received_datawritestraining_s->receiver_pipe_json_str)training_s(:934) whileedge_his intentionally kept alive. A PIPELINE_RAW/REPLY message arriving duringml_service_destroywrites into freed memory.nns_edge_set_event_callback(edge_h, NULL, NULL)or arunningflag under a mutex) before_ml_service_training_offloading_destroy; guardoffloading_mode/privreads withmls->lock.M8. single-shot:
ml_single_set_info_in_handleoverwritesin_infowith the OUTPUT infoc/src/ml-api-inference-single.c:884-899,:670-698(ml_single_update_inforeturns output info in*out_info),:764-785(ml_single_set_gst_infoalready sets bothin_info/out_info)is_input == TRUEwith a non-matchingtensors_info,ml_single_update_info(single, tensors_info, &info)fillsinfowith the output info, then the code doesgst_tensors_info_free(dest); _ml_tensors_info_copy_from_ml(dest, info)withdest == &single_h->in_info. Afterwardssingle_h->in_info == out_info;__setup_in_out_tensorsand_ml_single_invoke_validate_datasize/validate input against output dims → an app that sizes input fromml_single_get_input_info()passes validation with a too-small buffer → over-read inside the sub-plugin. Hidden when in/out shapes coincide (e.g.add.tflite).ml_single_open*with aninput_infothat differs from the model's configured input on a framework whoseset_input_infosucceeds (tflite resize), with different input/output sizes.ml_single_update_info, justml_tensors_info_destroy(info)and skip the copy intodest(ml_single_set_gst_infoalready updated both).M9. pipeline sink callback: flexible-tensor header parse unchecked,
gsizeunderflowc/src/ml-api-inference-pipeline.c:363-372(cb_sink_event)gst_tensor_meta_info_parse_header(&meta, map[i].data)return value ignored;_data->tensors[i].size = map[i].size - hsizewraps when the memory is shorter than the flex header. The app callback receives a ~2^64 size; the parser reads past the mapped memory.other/tensors,format=flexiblecaps in front oftensor_sink/appsinkand a short buffer (e.g.filesrc ! capsfilter ... ! tensor_sink).map[i].size >= hsize;goto errorotherwise.M10. single-shot: uninitialized
GError *on thread-creation failurec/src/ml-api-inference-single.c:923(GError *error;),:961-967g_thread_try_new(..., &error)refuses to set a non-NULL*error; on failureerror->message/g_clear_error(&error)dereference stack garbage.GError *error = NULL;M11. single-shot:
ml_single_close(or spurious wakeup) wakes a pending timed invoke → dangling output handed to the appc/src/ml-api-inference-single.c:1580-1588,:1376-1377(ml_single_closesetsJOIN_REQUESTEDand broadcasts),:546-551(thread destroysoutputwhenJOIN_REQUESTED)if (g_cond_wait_until(...)) status = single_h->status;does not checkstate. Woken by close, the caller reads the stale status (e.g.ML_ERROR_NONE) and returns_outwhile the thread later destroys it.while (single_h->state == RUNNING); after waking, ifstate == JOIN_REQUESTEDreturnML_ERROR_STREAMS_PIPEand do not publish_out.M12. single-shot:
ml_single_invoke_fasttimeout → invoke thread keeps writing into the app-owned output bufferc/src/ml-api-inference-single.c:1555-1557, 1582-1588need_alloc == FALSE,_out = *output(user buffer). On timeout the API returns but__invoke(single_h, input, output, FALSE)continues writing into it.nnstreamer-single.h:155-173does not require the app to keepoutputalive after a timeout → write-after-free if the app frees/reuses it.!need_alloc, either clone the output descriptor and copy back on completion, or document the lifetime requirement and drop the result on the thread when the caller timed out.M13.
ml_pipeline_destroy: element nodes freed while the pipeline is still PLAYING (in-flight sink callback uses freedelem)c/src/ml-api-inference-pipeline.c:1193-1224,:544-606(cleanup_node),:307(cb_sink_eventlockselem->lock); existing@todo CRITICALcomments at:286and:549g_hash_table_destroy(p->namednodes)runs beforegst_element_set_state(PAUSED/NULL).cleanup_nodeunlockse->lock, clears it andg_free(e); a streaming thread waiting on that lock incb_sink_eventresumes on freed memory.g_signal_handler_disconnectdoes not wait for a running handler.namednodes, or refcountml_pipeline_elementand have callbacks hold a ref.M14. Tizen:
res_handleshash table and everypipeline_resource_svalue leaked per pipelinec/src/ml-api-inference-tizen-privilege-check.c:692-706(ml_tizen_mm_res_release),:749(table created with value destroyNULL),:809-819(g_new0(pipeline_resource_s)+g_strdupinserted)g_hash_table_remove_allfrees keys only; the table itself is never unref'd beforeg_free(mm_handle).mm_res->typeandmm_res;g_hash_table_unref(mm_handle->res_handles)beforeg_free(mm_handle).M15. Tizen 9+: resource manager re-registered on every
ml_tizen_mm_res_acquire(leak + wrong handle on dealloc)c/src/ml-api-inference-tizen-privilege-check.c:394-417(ml_tizen_mm_res_create_rm, TIZEN9PLUS variant),:867(called unconditionally fromml_tizen_mm_res_acquire), compare pre-9 variant:604-611(if (rm_h) return ML_ERROR_NONE;)ml_pipeline_constructregisters rm Create a mirroring repo at tizen.org #1;ml_pipeline_start→_ml_tizen_get_resource→ml_tizen_mm_res_acquire(MAX)registers rm Invite SNAP developers and let them migrate headers #2, overwritingrm_handpriv(rm_consumer_infoleak, rm Create a mirroring repo at tizen.org #1 neverrm_unregistered). At destroy,rm_deallocate_resourcesuses rm Invite SNAP developers and let them migrate headers #2 for devices allocated on Create a mirroring repo at tizen.org #1. Repeats every start/stop.if (mm_handle->rm_h) return ML_ERROR_NONE;at the top, mirroring the pre-9 code.LOW
c/src/ml-api-service-agent-client.c:90, 164—g_autoptr (GList) membersreassigned inside the loop in_build_ml_info_from_json_cstr; n-1 lists leak for arrays with ≥2 elements (ml_service_model_get_all,ml_service_resource_get). Fix: declare inside the loop andg_list_freeper iteration.c/src/ml-api-service-agent-client.c:73(Tizen) —json_generator_set_root (gen, json_builder_get_root (builder)): the transfer-full root node is never unref'd. Fix:g_autoptr (JsonNode) root = json_builder_get_root (builder);.c/src/ml-api-common.c:1382-1391—ml_strerror (INT_MIN):errnum * -1overflows, both guards fail,strerrors[INT_MIN]read. Fix: rejectINT_MINor compute with unsigned.c/src/ml-api-common.c:160-179—_ml_tensors_info_create_from: whenml_tensors_info_clonefails,*outis left allocated and returned with an error (ml_tensors_data_get_infodoes not clean it up). Fix: destroy*outand set NULL on failure.c/src/ml-api-common.c:1099-1112—ml_tensors_data_set_tensor_dataon a flexible sink-callback handle:tensors[i].datapoints into mapped GstMemory (ml-api-inference-pipeline.c:370), and a differingdata_sizetriggersg_freeof that interior pointer. Fix: track buffer ownership inml_tensors_data_sand refuse to realloc non-owned buffers.c/src/ml-api-common-tizen-feature-check.c:269-285— unsynchronized lazy init offeature_info; two first-time callers can double-allocate (leak) and unlock a different mutex than they locked. Fix:g_once_init_enter/leaveor a static lock.c/src/ml-api-inference-pipeline.c:2872, 2928-2932—ml_pipeline_custom_easy_filter_register:in_info/out_info(GstTensorsInfocopies with strdup'd names /extra) nevergst_tensors_info_freed on any path. Fix: free both beforeexit:.c/src/ml-api-inference-single.c:187, 248-252—_ml_get_nnfw_subplugin_nameindexesml_nnfw_subplugin_name[nnfw]without a range check; publicml_check_nnfw_availability_fullonly rejectsML_NNFW_TYPE_ANY, so an out-of-range enum reads past the array and passes garbage tonnstreamer_filter_find. Fix: bounds-check → return NULL.c/src/ml-api-inference-single.c:1580-1588, 1620with:532-535— if the timeout fires beforeinvoke_threadpicks up the job, line 1620 clearssingle_h->input/output; the thread then sees NULLs, so the cloned_inis never freed and_outstays indestroy_data_listuntil close. Fix: only clear when they still equal_in/_out, and destroy them in that case.c/src/ml-api-inference-pipeline.c:877-897—iterate_element:gst_element_get_factorymay return NULL; foris_internalpipelinesget_elem_type_from_name (…, NULL)→g_str_hash (NULL)NULL deref. Fix: skip elements without a factory.c/src/ml-api-inference-pipeline.c:2152-2196—ml_pipeline_switch_get_pad_list: onGST_ITERATOR_ERROR,doneis not set and the finalg_list_free (dllist)leaks everygst_pad_get_namestring. Fix: setdone = TRUE,g_list_free_full (dllist, g_free)on error.c/src/ml-api-inference-pipeline.c:1752-1778—ml_pipeline_src_input_data: if_data->info == NULL,_ml_tensors_info_copy_from_mlreturns early andgst_infois used/freed uninitialized. Fix:gst_tensors_info_init (&gst_info)and check the return.c/src/ml-api-inference-pipeline.c:137-150, 675-681—pipe_custom_find_datareturns thepipe_custom_data_s *after droppingg_ml_pipe_lock; a concurrentml_pipeline_custom_easy_filter_unregister(ref_count 0) frees it beforeml_pipeline_custom_filter_refruns. Fix: take the ref inside the locked region.c/src/ml-api-inference-pipeline.c:1203-1207, 1225-1229—ml_pipeline_destroyerror returns leave a half-destroyed handle (namednodes == NULL, bus handler connected,punfreed); a retry hitsg_hash_table_destroy (NULL). Fix: make destroy idempotent / free unconditionally.c/src/ml-api-service-training-offloading.c:464-496—_training_offloading_request_pipeline: in the"pipeline"branchpipeline = g_strdup (transfer_data); transfer_data = NULL;leaks the first copy; entries matching neitherAPP_RW_PATHnor"pipeline"are overwritten next iteration without free; a second"pipeline"entry leaks the previousservice_name/pipeline. Fix:pipeline = transfer_data; transfer_data = NULL;andg_clear_pointer (&transfer_data, g_free)every iteration.c/src/ml-api-service-training-offloading.c:671-672, 726-727—ml_pipeline_construct (..., &training_s->pipeline_h)on repeatedml_service_startoverwrites a live pipeline handle (never destroyed). Fix: early-return or destroy the old pipeline ifpipeline_h != NULL.c/src/ml-api-service-training-offloading.c:538-543, 618-627, 819-820—receiver_pipe_json_stris read (checker thread), replaced withg_free(edge thread) and replaced via_ml_replace_string(API thread) with no lock → UAF / double free if the sender retransmits. Fix: protect all accesses withreceived_lock(ormls->lock).c/src/ml-api-service-training-offloading.c:160-162—node-typeis optional at the offloading level (ml-api-service-offloading.c:137), sovalmay be NULL ing_ascii_strcasecmp (val, "sender")(NULL deref withG_DISABLE_CHECKS; silently selects SENDER otherwise). Fix:if (!STR_IS_VALID (val)) return ML_ERROR_INVALID_PARAMETER;.c/src/ml-api-service-training-offloading.c:535—usec = training_s->time_limit * 1000000;withgint time_limitfrom JSON overflows for values > 2147; negative values bypass the wait. Fix:gint64math and validatetime_limit > 0.c/src/ml-api-service-offloading.c:923-929—_ml_service_offloading_convert_to_option:ml_option_set (tmp, key, g_strdup (val), g_free)leaks the dup when_ml_info_set_valuerejects an empty key without taking ownership. Fix: dup into a local and free on failure.c/src/ml-api-service-extension.c:671-678—g_cond_wait (&mls->cond, &mls->lock)afterg_thread_newhas no predicate; a spurious wakeup returns before the thread setsext->running = TRUE, and a subsequent destroy (running = FALSE+ join) can hang forever because the thread flips it back. Fix:while (!ext->running) g_cond_wait (...).java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:18-66, 220— threads attached viaAttachCurrentThreadinnns_get_jni_envare never detached (ART aborts when an attached native thread exits; GStreamer/GLib pool threads do exit), andpthread_key_createresult is unchecked (on failurejni_env == 0andpthread_setspecific (0, env)clobbers another key). Fix: a process-wide key created once with a destructor callingDetachCurrentThread; check the return value.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:545—nns_parse_tensors_dataoverwritesdata->num_tensorswith the Java array length without checking it againstdata->info; aTensorsDatawhose list is shorter than its info (e.g. afterTensorsData.close()) reachesml_service_request→ml_tensors_data_clone(ml-api-common.c:889-909) →memcpyfromtensors[i].data == NULL. Fix: fail if array length !=info->info.num_tensors(and, for STATIC, capacity !=tensors[i].size).java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:455-461— flexible-format error path deletesdata_arrbut notobj_data(local ref leak on a never-detached callback thread; 512-entry table can be exhausted). Fix:DeleteLocalRef (obj_data)before returning.java/android/nnstreamer/src/main/jni/nnstreamer-native-common.c:491-498—mlops_db_pathleaked whenml_agent_initializefails (goto doneskipsg_free). Fix: free before the goto.java/android/nnstreamer/src/main/jni/nnstreamer-native-api.c:452-453—CallVoidMethod (…, mid_update_data, i, data->tensors[i].size):size_tpassed through varargs for a(II)Vmethod (UB; works by accident on LE 64-bit). Fix: cast to(jint).Reviewed and found safe (for reference, so nobody re-audits these)
7ead26a,ca4725e,9d600fb,e7fedae,97227a8:_inis NULL-initialized and unlocked onlyif (_in);num_tensorsis checked againstNNS_TENSOR_SIZE_LIMITbefore indexing;ml_information_hin_ml_service_invoke_event_messageis destroyed on every path; the user callback is cleared undermls->lockbefore any teardown;_ml_service_offloading_request_raw's no-alloc handle is destroyed withfree_data = FALSE.ml_tensors_info_set_tensor_dimension/set_countbounds;ml_tensors_data_clonememcpy sizes;cb_sink_eventmap/unmap balance on allgoto errorpaths; GStreamer ref balance initerate_element,ml_pipeline_element_get_handle,parse_tensors_info,switch_select,get_pad_list;ml_single_open_customerror paths;_ml_replace_stringownership idioms;_ml_info_set_valueduplicate-key handling;_ml_error_report_buffer arithmetic; JNIGetStringUTFChars/Releasepairs and in-loopDeleteLocalRefs;tests/capi/unittest_util.c.Generated with Claude Code (full read-through + control-flow verification of every finding at commit
7ead26a).