[Fix] free custom connection when the custom library fails to load - #255
Conversation
|
이 리뷰는 별도 컨텍스트의 코드 리뷰 에이전트가 작성한 내용을 옮긴 것입니다. ( 검토 범위 및 방법
주제 해결 여부
지적 사항[major]
|
babf33f to
4aeefb8
Compare
|
리뷰 감사합니다. 두 지적 모두 이번 PR 안에서 처리했습니다. [major]
|
|
이 리뷰는 별도 컨텍스트의 코드 리뷰 에이전트가 작성한 내용을 옮긴 것입니다. 1차 리뷰(major/minor 두 건)에 대한 대응을 검증하는 2차 리뷰입니다. 1차 리뷰 내용과 중복되는 부분은 다시 적지 않고, 갱신된 코드에 대한 판단과 새로 확인한 내용 위주로 남깁니다. 검증 방법
[major] → 해결 확인
[minor] → 부분 해결, 판단은 적절함
위쪽 로그(
회귀 점검 (NULL 가드 관련, 추가 확인)
결론**Merge 가능(approve)**으로 판단합니다. 1차 리뷰의 major 지적(ASan이 merge를 막지 못함)은 코드/설정 양쪽 다 확인한 결과 실질적으로 해결되었고, minor 지적도 핵심 노이즈(이중 경고)는 제거되었으며 남은 한 줄에 대한 판단(문서화로 계약 명시, 실제 동작 변경은 보류)도 타당합니다. 새로 발견한 문제는 없으며, 위 nit은 후속 개선 제안일 뿐 이 PR을 막을 이유는 아닙니다. |
4aeefb8 to
f7c11ca
Compare
nns_edge_custom_load() allocates custom_connection_s before it loads the library, and its error path handed that partially constructed handle to nns_edge_custom_release(). That function requires a fully constructed handle: it returns NNS_EDGE_ERROR_INVALID_PARAMETER as soon as it sees a NULL instance, which is exactly the state a dlopen()/dlsym() failure leaves behind, so the struct was never freed. An application retrying nns_edge_custom_create_handle() with an unavailable lib_path leaked sizeof (custom_connection_s) on every attempt. Split the two failure paths so each cleans up what it actually owns: - the library did not load, so nothing but the struct exists; free it directly (_load_custom_library() already closed the dl handle on its own error path). - the plugin's create() failed, so the instance is valid and nns_edge_custom_release() is the correct cleanup; keep it. Add unit tests for both failure paths, including the create() failure path which had no coverage at all. The test connection library gains an NNS_EDGE_CUSTOM_TEST_FAIL_CREATE environment switch to make that path reachable, and the tests repeat the failures so a leak shows up as a multi-allocation report instead of a single 24-byte one. Two more tests cover what the split touches from the caller side: that a failed attempt does not disturb a later successful create on the same library, and that a live handle survives repeated failed loads of the library it uses. The leak is invisible to a plain unit test run - every assertion passes either way - so run the unit tests under AddressSanitizer in the ubuntu cmake workflow. The sanitizer steps go in the existing build job rather than a job of their own because only "build" is a required status check on main; a separate job would report a leak without blocking the merge. The rest of the test suite is already sanitizer-clean. nns_edge_release_handle() no longer warns about a custom connection it never had. The failure paths above leave eh->custom_connection_h NULL, so the warning fired on every failed create, pointing at a close that was never attempted. Document the close()-after-failed-create() contract in the custom connection header. The behaviour is unchanged; it was simply never stated what custom library authors may expect there. The test connection library, which the header points at as the example to follow, now accepts a NULL priv in close() as that contract requires. Fixes: nnstreamer#252 Fixes: nnstreamer#253 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: MyungJoo Ham <myungjoo.ham@samsung.com>
f7c11ca to
d3e1189
Compare
|
2차 리뷰 감사합니다. 남기신 nit도 반영했습니다. 지적하신 대로, 헤더에 " 결과적으로 실패 경로에서 나오던 오해 소지 있는 로그 세 종류가 모두 사라졌습니다. 테스트 1회 실행 기준: 검증: plain / ASan 빌드 모두 통과, |
|
이 리뷰는 별도 컨텍스트의 코드 리뷰 에이전트가 작성한 내용을 옮긴 것입니다. 1차 리뷰(major 1 + minor 1)와 2차 리뷰(approve + nit 1)에 이어지는 3차 리뷰이며, 2차 리뷰의 nit에 대한 이번 대응분만 집중 검토했습니다. 이전에 다룬 내용은 반복하지 않습니다. 검증 방법
이번 변경(
|
myungjoo-bot
left a comment
There was a problem hiding this comment.
Note: this review was produced by an automated review agent and is transcribed here by the bot account.
이 리뷰는 자동 코드 리뷰 에이전트(
github-code-review스킬)가 작성한 결과를 봇 계정이 옮겨 적은 것입니다.
d3e11898 기준 리뷰입니다.
리뷰 범위
src/libnnstreamer-edge/nnstreamer-edge-custom-impl.c (+6/-8), src/libnnstreamer-edge/nnstreamer-edge-internal.c (+2/-1), include/nnstreamer-edge-custom.h (+4), tests/nnstreamer-edge-custom-test.c (+8/-5), tests/unittest_nnstreamer-edge-custom.cc (+125), .github/workflows/ubuntu_clean_cmake_build.yml (+20/-1).
확인한 것
- 주제 해결: 릭 분석이 코드상 정확합니다. 기존
error:라벨은 두 실패를 한데 모아nns_edge_custom_release (custom)로 넘겼는데, 이 함수는!custom->instance이면NNS_EDGE_ERROR_INVALID_PARAMETER로 조기 반환해 아래쪽free()에 닿지 못합니다. dlopen/dlsym 실패 시instance는 항상 NULL이므로 매 실패마다custom_connection_s가 샙니다. 두 경로를 분리해SAFE_FREE (custom)/nns_edge_custom_release (custom)로 나눈 것이 최소 수정이면서 정확합니다. - 이중 해제·dangling 없음:
_load_custom_library()는 자신의error:경로에서 이미dlclose()를 하므로, 그 뒤SAFE_FREE (custom)만 하는 것이 맞습니다.nns_edge_custom_release()본체는 손대지 않아 다른 호출자(nns_edge_release_handle(),releaseInvalidParam*_n)의 동작이 그대로입니다. - out-param 계약 유지:
*handle은 성공 시에만 기록되고, 실패 시 호출자 변수는 그대로 남습니다. #250에서 세운nns_edge_custom_create_handle()의*edge_h = NULL계약과도 정합합니다. nns_edge_release_handle()가드:eh->custom_connection_h가 NULL이면 release를 건너뛰도록 한 변경은, 커스텀 생성 실패 핸들을 정리할 때마다 뜨던 "Failed to close custom connection." 오탐 경고를 없앱니다.NULL일 때 기존 동작은 "INVALID_PARAMETER 반환 → 경고 로그"뿐이었으므로 기능 회귀가 없습니다.ENABLE_CUSTOM_CONNECTION=0빌드에서도custom_connection_h가 항상 NULL이라 동작이 동일합니다.- CI가 회귀를 실제로 막습니다 — 이 PR의 가장 중요한 부분: 릭은 모든 gtest 단언을 통과하므로 일반 실행으로는 절대 잡히지 않습니다. ASan 스텝을 별도 job이 아니라 required check인
buildjob 안에 넣은 판단이 정확합니다(이 리포의 required check는DCO,build둘뿐이므로 별도 job은 빨개져도 머지를 막지 못합니다). 실행 로그로 확인했습니다:build-asan에서 4개 타깃이 모두 새로 빌드되고(Built target unittest_nnstreamer-edge-custom),ASAN_OPTIONS: detect_leaks=1로100% tests passed, 0 tests failed out of 2. 즉 스텝이 형식적으로만 도는 것이 아니라 sanitizer 바이너리를 실제로 실행합니다. ENVIRONMENT속성과의 상호작용 확인: #251이 커스텀 테스트에 건ENVIRONMENT "LD_LIBRARY_PATH=..."는 해당 변수만 덮어쓰므로ASAN_OPTIONS는 그대로 전달됩니다.build-asan디렉터리 기준으로 경로가 다시 생성되는 것도 맞습니다.- 테스트 설계:
loadFail_n/loadCreateFail_n의 100회 반복은 sanitizer 리포트를 24바이트 1건이 아니라 100건으로 키워 눈에 띄게 하려는 의도이고,liveHandleAfterFailedLoads는 살아 있는 핸들이 dlopen 참조를 잡고 있어 50회 실패 로드가 그 핸들을 무너뜨리지 않음을 확인합니다. 실패 경로에서dlclose()가 도는데도 안전하다는 것을 짚는 좋은 케이스입니다. - 문서:
nns_edge_custom_create()실패 시에도nns_edge_custom_close()가 호출된다는 기존 동작이 공개 헤더에 처음으로 명시되었습니다. 동작 변경 없이 계약만 적은 것이라 out-of-tree 플러그인에 영향이 없습니다. 구조체 레이아웃/시그니처 변경이 없어 ABI 영향도 없습니다. - 범위: production 변경은 8줄이고
src/의 다른 모듈을 건드리지 않습니다. 테스트 라이브러리의nns_edge_custom_close()가 NULL priv를 에러 대신 성공으로 처리하도록 바뀌었는데, 이는 새로 문서화한 계약("close는 만들어진 적 없는 priv를 견뎌야 한다")을 예제 구현이 따르도록 맞춘 것이라 일관됩니다. - 보안: 백도어로 볼 만한 코드 없음. 릭 제거는 보안상 개선입니다. CI 워크플로의 트리거·권한·시크릿 사용에 변화가 없습니다.
- CI 전 항목 green (GBS 3종, pdebuild 3종, static checks 포함).
참고 제안 (blocking 아님)
getenv()검사가!priv검증보다 앞에 있습니다.nns_edge_custom_create()에서 스위치가 켜져 있으면priv == NULL이어도INVALID_PARAMETER대신UNKNOWN을 반환합니다. 지금 테스트 중에 이를 밟는 것은 없지만, 이 파일은 공개 헤더가 "Refer to the example in nnstreamer-edge-custom-test.c"로 가리키는 참조 구현이라 파라미터 검증 순서는 그대로 두는 편이 낫습니다.if (!priv)뒤로 한 칸 내리면 해결됩니다.- 환경변수 스위치는 프로세스 전역이고 fatal 단언에 취약합니다. 지금은
setenv()와unsetenv()사이에ASSERT_*가 없어 안전하지만(확인했습니다), 나중에 누가 그 구간에ASSERT_를 하나 넣으면unsetenv()가 실행되지 않고 이후 모든 테스트가 실패 주입 상태로 돌게 됩니다. RAII 가드(소멸자에서unsetenv)나TearDown()을 쓰는 fixture로 감싸면 구조적으로 막힙니다 — 참고로 #254가 같은 목적을 fixture + 제어 블록으로 풀고 있습니다. - #254와 겹칩니다. 이 PR은 #252와 #253을 함께 닫고, #254는 #253만 닫습니다. 두 브랜치 모두
tests/nnstreamer-edge-custom-test.c의create()/close()와tests/unittest_nnstreamer-edge-custom.cc의 같은 영역을 고쳐서 실제로 텍스트 충돌합니다(로컬에서 두 head를 merge해 확인:tests/nnstreamer-edge-custom-test.c,tests/unittest_nnstreamer-edge-custom.cc두 파일 CONFLICT). 릭 수정이 들어 있는 이쪽을 먼저 머지하고, #254는 이 PR이 커버하지 못하는 부분(close_count/close_had_priv로 close 호출을 관측하는 것, 환경변수 대신 fixture로 주입하는 것)만 남겨 리베이스하는 순서를 권합니다. 판단은 메인테이너 몫입니다.
릭 수정이 정확하고, 일반 테스트로는 잡히지 않는 회귀를 required check 안에서 실제로 막도록 CI가 구성된 것을 로그로 확인했으므로 approve합니다.
nns_edge_custom_load()leaks thecustom_connection_sit allocated whenever_load_custom_library()fails: theerror:label handed the partially constructed handle tonns_edge_custom_release(), which early-returns on a NULLinstanceand therefore never reaches itsfree(). 24 bytes per failed attempt, on a path any application reaches by retryingnns_edge_custom_create_handle()with a bad or unavailablelib_path.Reproduced under LeakSanitizer against the existing test suite before any change:
What changed
The fix.
nns_edge_custom_load()now handles its two failure paths separately instead of funnelling both through oneerror:label:_load_custom_library()failedinstanceis NULL,dl_handlealready closed by_load_custom_library()'s own error pathSAFE_FREE (custom)create()failedinstanceis validnns_edge_custom_release (custom)— unchangednns_edge_custom_release()itself is untouched, so nothing else that calls it changes. Return values and the out-param contract (*handleis written only on success) are identical to before.Tests. The create()-failure branch had no coverage at all, so
tests/nnstreamer-edge-custom-test.cgains anNNS_EDGE_CUSTOM_TEST_FAIL_CREATEenvironment switch that makes itsnns_edge_custom_create()return an error. Five tests:loadFail_n/loadCreateFail_n— repeat each failure 100x, so a returning leak reports as 100 allocations rather than one easily-missed 24-byte record. Both also assert the out-param is not left pointing at freed memory, which is exactly whatnns_edge_release_handle()relies on.createHandleCreateFail_n— the public-API negative test for the create() path, mirroring the existingcreateHandleLoadFail_n.createHandleAfterFailure— a failed load and a failed create must not disturb a later successful create on the same library (covers thedlclose()on the failure path).liveHandleAfterFailedLoads— a live handle keeps working through 50 failed loads of the library it is using.CI. The leak is invisible to a normal test run: every gtest assertion passes with or without the fix. Without a sanitizer run, this and any future leak merges silently.
ubuntu_clean_cmake_build.ymlnow builds and runs the suite a second time under AddressSanitizer. The sanitizer steps sit inside the existingbuildjob on purpose:main's required status checks are["DCO", "build"], so a separate job would report a leak without actually blocking the merge. Verified: againstmainthe sanitizer step fails with the 24-byte leak above; with this branch it is clean, and the rest of the suite is already sanitizer-clean, so it does not go red for unrelated reasons.Log noise.
nns_edge_release_handle()warnedFailed to close custom connection.whenever it released a handle whosecustom_connection_hwas NULL - which is every failed custom create, i.e. exactly the path this PR governs. It pointed at a close that was never attempted. Guarded.Docs.
include/nnstreamer-edge-custom.hnow states thatnns_edge_custom_close()is still called with theprivof a failedcreate(). This is existing behaviour on the create()-failure path, deliberately left as is; it was just never written down for custom library authors. Changing it would alter the contract for out-of-tree plugins and belongs in its own change if wanted.Verification
Also run with
--gtest_shuffle --gtest_repeat=3to confirm the environment switch never leaks between tests. clang-format and GNU indent checked locally against the repo config.Fixes #252
Fixes #253
🤖 Generated with Claude Code