Conversation
myungjoo-bot
left a comment
There was a problem hiding this comment.
Automated review of this draft (transcribed from an AI review agent's report; please verify before acting).
Summary: The PR reorders the Android JNI teardown so C-API handles are released before the private data their callbacks read (nns_free_element_data: unregister / release the handle, then priv_destroy_func; nns_destroy_pipe_info: element table -> ml_pipeline_destroy / ml_service_destroy / ml_single_close -> priv_data -> global refs / pipe_info), and makes CustomFilter.close() fail while keeping pipe_info and mHandle when ml_pipeline_custom_easy_filter_unregister() refuses because ref_count > 0. H5 and H6 are confirmed on base from the diff hunks and c/src/ml-api-inference-pipeline.c: cb_sink_event() holds elem->lock across the user callback; ml_pipeline_sink_unregister() takes p->lock + elem->lock, removes the handle from elem->handles and frees it, so a later cleanup_node() / free_element_handle() cannot double-free it and no sink callback can be running or start once unregister returns; src / switch / valve / element release do the same list removal; ml_pipeline_destroy() NULLs state_cb.cb under p->lock before the state change and joins the streaming threads, so priv_data (freed last) outlives the state callback; _ml_service_destroy_internal() clears cb_info under mls->lock before releasing the pipeline; the JNI single-shot path registers no callbacks. The sink-callback path never takes pipe_info->lock, so holding it while unregistering cannot deadlock. Custom unregister returns ML_ERROR_INVALID_PARAMETER and keeps the registration when ref_count > 0 (pinned by the existing register_filter_11_n); on the FALSE path pipe_info->lock stays initialized and priv_data intact, g_clear_pointer makes a second call safe, and the other three callers (pipeline / single / service) can only see FALSE for a NULL pipe_info, so ignoring the return there cannot leak. nativeDestroy is registered via the JNINativeMethod table with (J)Z matching boolean nativeDestroy (long) and returns JNI_TRUE / JNI_FALSE.
CI: all 10 checks pass. The GBS x86_64 unit_test 1 log (job 102769220332) shows nnstreamer_capi_sink.unregister_wait_callback OK (503 ms, consistent with a ~300 ms blocked unregister plus the 200 ms quiet window) and nnstreamer_capi_custom.register_filter_11_n OK, 241 tests passed. git merge-tree against upstream/main and against #695's head both produce clean trees, although both PRs append to APITestCustomFilter.java and unittest_capi_inference.cc and edit nnstreamer-native-api.c. Both commits are DCO-signed with accurate messages, and the [Test] commit stands alone since it tests the C-API contract rather than the fix. Approving; the items below are non-blocking.
- [Low]
nnstreamer-native-api.c:254-260andCustomFilter.java:141:ml_pipeline_custom_easy_filter_unregister()returns the sameML_ERROR_INVALID_PARAMETERwhenNNS_custom_easy_unregister()fails withref_count == 0(internal / already-unregistered case). The log and the Java exception then claim "it is used in a pipeline", and sinceclose()keepsmHandle,pipe_infoand its JNI global refs (which also pin the Java object) can never be released on that path — a leak on an internal-error path, not corruption. Suggest softening the messages ("in use or already unregistered"), or a distinct C-API code for the in-use case sopipe_infois kept only then. - [Low]
nnstreamer-native-api.c:245-247: on the FALSE path the element table is already gone (element_handles == NULL). Harmless forNNS_PIPE_TYPE_CUSTOM, butnns_get / add / remove_element_datawould then callg_hash_table_*on NULL. Moving theg_clear_pointerafter the custom-unregister check would leavepipe_infofully untouched on failure; optional. - [Low]
tests/capi/unittest_capi_inference.cc:815-824, 870-877: the test proves the blocking contract only if the main thread reachesml_pipeline_sink_unregister()within the callback's 300 ms sleep; under heavy CI load it can pass vacuously (it never fails spuriously, so no CI-breakage risk). Optional hardening: have the callback wait on a second condition that the main thread signals right before calling unregister, or lengthen the sleep. - [Low]
APITestPipeline.java(testCloseWithoutStop*):mInvalidStateis written from native callback threads and read afterclose()withoutvolatile; it works becauseml_pipeline_destroy()joins the streaming threads first, butvolatilewould make the device tests strictly correct. These tests cannot run in CI, as the PR states. - [Note, no change]
CustomFilter.finalize()->close()throwing is ignored per JLS 12.6 andsuper.finalize()still runs viafinally; in practice the finalizer never runs while the filter is registered becausepipe_info->instanceis a JNI global ref to the object.IllegalStateExceptionfromAutoCloseable.close()is legal and consistent with the class's other methods.Pipeline.close()has no Javadoc on base, and no public C-API doc change is needed.
No back-door or suspicious behavior found.
|
Addressing review 5194134533. All four items were valid and are fixed in this PR:
Item 5 needs no change. Local checks after the change: the JNI sources pass |
|
This is a code review by another agent (a Claude code-review sub-agent), transcribed onto this PR. Please verify before acting on it. Scope reviewed: the 4 commits on this PR ( Does it solve H5/H6 (issue #690)? Yes, independently verified, not just taken on faith from the PR description:
Regression check on other modules: Size / focus: proportionate to the bug — JNI teardown files, the one Java API class involved, and tests. No unrelated modules touched. Prior review (5194134533) follow-through: I checked all 4 of its items against the current diff, they are genuinely fixed, not just claimed fixed in the follow-up comment:
No back-door or otherwise suspicious code found in this diff. Findings1. [High] The tests that actually exercise this PR's JNI changes cannot fail CI. 2. [Low] Commit history contains two "fix a previous commit in this PR" commits. 3. [Low] 4. [Nit] Verdict: changes requested |
nns_destroy_pipe_info() freed the private data of the pipe info before it released the native handle, and nns_free_element_data() freed the private data of an element before it unregistered the element handle. Pipeline.close() does not stop the pipeline, so a sink callback can still be running on a streaming thread at that point. nns_sink_data_cb() reads both pipe_info->priv_data (the Java method ID, NULL by then) and the private data of the sink (tensors info and a global reference, already released), and the state callback and the ml-service event callback read pipe_info->priv_data the same way. Release the handles first. ml_pipeline_sink_unregister() takes the element lock that cb_sink_event() holds for the whole user callback, so once it returns no sink callback is running or can start, and only then is the private data of the element freed. The element table still goes before ml_pipeline_destroy(), which frees the sink handles itself. The private data of the pipe info is freed after ml_pipeline_destroy(), ml_service_destroy() and ml_single_close(), which stop the remaining callbacks. CustomFilter.close() ignored the result of ml_pipeline_custom_easy_filter_unregister(). The unregister fails and keeps the filter registered while a constructed pipeline uses it, with the pipe info as its user data, but the pipe info was freed anyway and the next buffer invoked the filter on released memory. Now the custom-filter is unregistered before anything else is touched; if that fails the pipe info is left as it was, nativeDestroy() reports it, and close() throws IllegalStateException and keeps the handle, so the filter keeps working and can be closed again after the pipeline is closed. The unregister also fails, keeping the C handle, when the filter is not in use but nnstreamer refuses to unregister it. Keeping the pipe info is still the safe choice there, so the log and the exception say the filter may be in use. This addresses H5 and H6 of nnstreamer#690. Signed-off-by: MyungJoo Ham <myungjoo.ham@samsung.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The C unittest locks down what the JNI binding now relies on: ml_pipeline_sink_unregister() does not return while the sink callback is running, and no callback is called after it returns even though the pipeline keeps playing. The callback holds the streaming thread at its first call until the test announces the unregister, then sleeps briefly, so the unregister always overlaps the running callback. It runs in CI, so a change in cb_sink_event() that invokes the callback without the element lock fails there rather than reopening the use-after-free on a device. The other half, that ml_pipeline_custom_easy_filter_unregister() fails while a pipeline uses the filter, is covered by nnstreamer_capi_custom.register_filter_11_n already. The Android cases close a custom-filter while a pipeline uses it, started or not: close() throws, the filter still processes buffers, and closing it again after the pipeline succeeds and frees the name. Closing twice and closing after the pipeline are the positive cases. Two stress cases close a playing pipeline without stop() while buffers still reach one or two sinks, with a state callback on the two-sink case. The fields written from native callback threads are volatile. Signed-off-by: MyungJoo Ham <myungjoo.ham@samsung.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8ba5b08 to
588353f
Compare
|
Addressing the review in #696 (comment).
All 10 CI checks pass on |
|
This is a code review by another agent (a Claude code-review sub-agent), transcribed onto this PR. Please verify before acting on it. Scope reviewed: the current 2-commit diff ( Re-review of the two earlier roundsRound 1 (approving review, 5 items) — all confirmed genuinely fixed, not just claimed:
Round 2 (changes-requested review, 4 items):
Independent verification of the H5/H6 fix itselfI did not just re-read the two prior reviews' reasoning — I pulled the current
No back-door or otherwise suspicious code found. Size and focus are proportionate to the bug: JNI teardown, the one Java class involved, and tests; nothing outside that footprint is touched. Findings1. [Medium] The regression protection for this PR's own fix has not yet been demonstrated by an actual CI run that includes both this diff and #697's harness. 2. [Low] The deliberate "leak beats double-free" trade-off on the internal-unregister-failure path still has no in-code note. 3. [Nit] No action needed — the single- No architecture or public-API documentation gap: the one real API-visible change ( Verdict: ok to merge |
Both failure paths of ml_pipeline_custom_easy_filter_unregister() keep the filter handle, so releasing the pipe info there would leave a registered filter invoking a released user data. Say so where the function contract is documented, as the leak looks like an oversight otherwise. Signed-off-by: MyungJoo Ham <myungjoo.ham@samsung.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Addressing the review in #696 (comment).
GNU |
|
This is a code review by another agent (a Claude code-review sub-agent), transcribed onto this PR. Please verify before acting on it. Scope of this round: the final diff at Verification of
|
|
The final review round is clean (#696 (comment), ok to merge, no action requested) and all 10 checks pass on Merge order: please merge #697 first, then rebase this branch onto |
Addresses items H5 and H6 of #690.
H5: private data freed before the callbacks stop
Pipeline.close()goes straight tonativeDestroy()without stopping the pipeline, so a sink callback can still be running on a streaming thread while the JNI layer tears the pipe info down. The teardown freed the callback data first:nns_destroy_pipe_info()freedpipe_info->priv_data(the Java method IDs) beforeml_pipeline_destroy()/ml_service_destroy()/ml_single_close().nns_free_element_data()freed the element's private data (out_info, theout_info_objglobal ref) beforeml_pipeline_sink_unregister().nns_sink_data_cb()reads both, so a buffer arriving in that window dereferences a NULLpriv(priv->mid_sink_cb) or released sink data.nns_pipeline_state_cb()andnns_service_event_cb()readpipe_info->priv_datathe same way.Fix: release the handles first, then the data they deliver to.
nns_free_element_data()unregisters/releases the C-API handle, then frees the private data.ml_pipeline_sink_unregister()takeselem->lock, whichcb_sink_event()holds for the whole user callback, so once it returns no sink callback is running and none can start.nns_destroy_pipe_info()the element table still goes beforeml_pipeline_destroy(). This is a constraint the issue text did not mention:ml_pipeline_destroy()frees the sink handles itself (cleanup_node()→free_element_handle()), so unregistering them afterwards would be a double free.pipe_info->priv_datais freed last, after the native handle is gone.No NULL checks were added to the callbacks. After the reorder
priv_dataoutlives every callback, so a check would be dead code, and it would hide a future ordering mistake instead of exposing it.H6:
CustomFilter.close()freespipe_infowhile the filter is still registeredml_pipeline_custom_easy_filter_unregister()returnsML_ERROR_INVALID_PARAMETERand keeps the filter registered while a constructed pipeline uses it (ref_count > 0), withpipe_infoas its user data. The JNI layer ignored the result and freedpipe_infoanyway, so the next buffer rannns_customfilter_invoke()on freed memory.Fix:
nns_destroy_pipe_info()now returnsgboolean. It unregisters the custom-filter before touching anything else, so if the unregister fails it returnsFALSEand leavespipe_infoexactly as it was (element table, private data, global refs). A later call runs the normal teardown.NNS_custom_easy_unregister()refuses. Keepingpipe_infois still the safe choice there, so the log and the exception say the filter "may be" in use.CustomFilter.nativeDestroyreturnsboolean(JNI signature(J)V→(J)Z).CustomFilter.close()throwsIllegalStateExceptionand keepsmHandlewhen the unregister fails. The filter keeps working, andclose()can be called again after the pipeline is closed. This is documented in the Javadoc ofclose().API behavior change: closing a
CustomFilterbefore thePipelinethat uses it now throws instead of corrupting memory. The documented order (create the filter before the pipeline, close the pipeline first) and every existing test are unaffected.close()fromfinalize()is also safe: the JVM ignores exceptions thrown from finalizers.Tests
nnstreamer_capi_sink.unregister_wait_callbacktests/capi/unittest_capi_inference.ccml_pipeline_sink_unregister()does not return while the sink callback runs, and no callback arrives afterwards while the pipeline keeps playing. The H5 fix depends on this. The callback is held until the test announces the unregister, so the two always overlap.nnstreamer_capi_custom.register_filter_11_n(existing)testCloseWhileUsed_n,testCloseWhileUsedNotStarted_nAPITestCustomFilterclose()throws while in use; the filter still processes buffers afterwards; closing again after the pipeline succeeds and frees the nametestCloseAfterPipeline,testCloseTwiceAPITestCustomFiltertestCloseWithoutStop,testCloseWithoutStopSingleSinkAPITestPipelinestop()while buffers reach one or two sinks (and a state callback), 10 iterations eachandroidTestnever runs in CI; the Android jobs only build. That is why the C-API contract the JNI change relies on is also covered by a C unittest, which does run in CI.Local verification
-Denable-ml-service=false -Denable-tizen=false).nnstreamer_capi_sink.*andnnstreamer_capi_custom.*pass: 21/21, and the new test 5/5 on repeated runs. The fullunittest_capi_inferencerun gave 236 passed, 1 failed. The failure isnnstreamer_capi_src.pngfile, which cannot findorange.pngfrom the ad-hoc run directory; it is an environment issue and unrelated to this change.cb_sink_event()to dropelem->lockaround the user callback. The new test then fails onreturned(3/3 runs; unregister returned while the callback was still running). With the original code it passes: 10/10 runs, plus 5/5 with 8 busy-loop processes loading the CPU.nnstreamer-native-*.cfiles passgcc -fsyntax-only -Wall -Wextraagainst host GLib/GStreamer headers in three configurations: default,NNS_SINGLE_ONLY, andENABLE_ML_SERVICE.androidTestfiles type-check withjavac(Android and JUnit classes stubbed).indent(CI options) reports no new diffs in the touched.cfiles;clang-formatreports no diff on the unittest.Review follow-up
Review 5194134533 raised four Low items; all four are addressed in
58d1439and8ba5b08:pipe_infois still kept, because the C handle is kept on that path too.pipe_infowas partially torn down on failure: the unregister now runs first, and the element table destroy is back to the base code.mReceived/mInvalidStatein both Android test classes are nowvolatile.Relation to other PRs
#695 (H4) also touches
nnstreamer-native-api.c,CustomFilter.javaandAPITestCustomFilter.java, but in different functions and hunks (nns_parse_tensors_data()and theCallbackJavadoc). The two PRs should rebase onto each other trivially.🤖 Generated with Claude Code