Fix for memory leaks and resource management bugs - #109
Fix for memory leaks and resource management bugs#109Lokesh Muthuraj (lokeshmuthuraj) wants to merge 6 commits into
Conversation
|
Copilot review |
|
Simon Xiao (@simonxiaoss) lubaihua33 Please review the PR. I don't have permission to add Copilot as reviewer as well. |
Hi Lokesh Muthuraj (@lokeshmuthuraj), thanks for your fix. I have requested copilot to review. |
There was a problem hiding this comment.
Pull request overview
This PR targets issue #108 by addressing a set of memory leaks and resource-management bugs (heap allocations and file descriptors) across sender/receiver, TCP/UDP, sync, and OS counter paths.
Changes:
- Fixes multiple
asprintf/log-string leaks by switching toPRINT_*_FREE()and avoidingASPRINTFoverwrite leaks. - Closes leaked sockets / file handles in several error paths and on function exit (TCP/UDP streams, sender sync,
/proc/interruptsreader). - Fixes a UDP sender socket creation bug by using the correct socket type (
SOCK_DGRAM).
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
src/udpstream.c |
Fixes UDP sender log leaks, corrects UDP socket type, and adds missing close/free in receiver paths. |
src/tcpstream.c |
Fixes TCP listener error-path cleanup and closes accepted sockets on failure paths in epoll/select loops. |
src/parameter.c |
Adjusts mapping parsing cleanup on error paths and adds an allocation failure check. |
src/oscounter.c |
Ensures /proc/interrupts FILE* is closed on early return. |
src/ntttcp.c |
Frees the thread-result pointer array (e->results->threads). |
src/main.c |
Closes the sender sync socket on additional sender error/exit paths. |
src/endpointsync.c |
Improves receiver sync socket cleanup and adds missing frees/closes in failure paths. |
src/const.h |
Bumps tool version to 1.4.4. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.
Comments suppressed due to low confidence (1)
src/parameter.c:1
free(e->test->bind_address)assumesbind_addressis always heap-allocated. Inverify_args(parameter.c around line 274), ifstrdup("::")fails,test->bind_addressis left as NULL (after the precedingfree) — that case is fine. However, more importantly, please verify all writes totest->bind_addressconsistently usestrdup(including any code paths not shown here, e.g., when reading from config). One assignment to a string literal anywhere (such as the pre-existingtest->client_address = "0.0.0.0"pattern) would make thisfreecrash. Consider adding a helper likeset_bind_address(test, value)to centralize the free+strdup logic and avoid future regressions.
// ----------------------------------------------------------------------------------
|
|
||
| if __name__ == "__main__": | ||
| pytest.main() |
|
lubaihua33 / Simon Xiao (@simonxiaoss) Copilot keeps on raising more code review comments and this is leading to increase in line of code. I have broken down this single PR #109 into multiple small PRs which are focused. Please check and review the following PRs: I will close this PR and it will be continued as part of the PRs #110 , #111 , #112 , #113 |
Fix: Memory Leaks and Resource Management (17 Issues)
Overview
This PR was initiated to fix issue #108.
However, this PR now fixes 17 memory and resource management bugs discovered through systematic code review, AddressSanitizer (ASAN/LSan), and Valgrind analysis of the ntttcp-for-linux codebase. The bugs fall into three categories:
malloc,strdup,ASPRINTF)socket()in UDP senderAll issues were found on the unfixed codebase. Evidence sections below reproduce each leak using the commands shown. Later, after fixing the bugs, AddressSanitizer and Valgrind tests were executed again on the fixed codebase to check if the leaks were fixed.
Test Setup
ASAN build:
Valgrind build:
Files Changed
src/parameter.cprocess_mappings()src/main.csrc/udpstream.csrc/endpointsync.csrc/tcpstream.csrc/ntttcp.csrc/oscounter.c/proc/interruptsFD not closed on early returnIssues Fixed
Bug1 ·
parameter.c—strdupresult leaked inprocess_mappings()How found: ASAN, static analysis
Location:
parameter.c:204Root cause:
process_mappings()callsstrdup(test->mapping)and stores the result inelement.strsep()then advances theelementpointer. When an invalid CPU ID is detected, the function returnsERROR_ARGSwithout freeing the original allocation.Fix: Save the original pointer on entry and
free()it in every return path.Evidence (ASAN):
Bug2 ·
main.c— sync socket not closed inrun_ntttcp_sender()error pathsHow found: Static analysis,
valgrind --track-fds=yesLocation:
main.c:38–47Root cause:
synch_socketis opened at the start ofrun_ntttcp_sender()but is not closed before thereturn ERROR_GENERALstatements in the error paths at lines ~43 and ~47.Fix: Added
close(tep->synch_socket)before eachreturn ERROR_GENERALin the error paths.Evidence: File descriptor leak confirmed with
valgrind --track-fds=yes ./ntttcp -s 127.0.0.1. LSAN does not track FD leaks.Bug3 ·
udpstream.c—ASPRINTFlog leaked on early return inrun_ntttcp_sender_udp4_stream()How found: ASAN, static analysis
Location:
udpstream.c:60, 68Root cause: When
get_interface_name_by_ip()orupdate_client_info()fails, the function allocates a log string withASPRINTF(&log, ...)and then callsPRINT_ERR(log). ThePRINT_ERRmacro prints but does not freelog.Fix: Changed to
PRINT_ERR_FREE(log), which prints and frees the string.Evidence (ASAN):
./ntttcp -s 127.0.0.1 -u -a 192.0.2.1 -N -t 1 -P 1 # Exit 23(208 bytes = ~52-byte log string × 4 sender threads)
Bug4 ·
udpstream.c—ASPRINTFlog leaked in socket error pathsHow found: Static analysis
Location:
udpstream.c:89, 99, 119Root cause: Same
ASPRINTF+PRINT_ERR(log)pattern as issue Bug3. Triggered whensocket(),setsockopt(), orconnect()fails.Fix: Changed all three occurrences from
PRINT_ERR(log)toPRINT_ERR_FREE(log).Bug5 ·
udpstream.c— log string leaked viaASPRINTFoverwrite in receiver bind pathHow found: Static analysis
Location:
udpstream.c:252Root cause: The pattern
ASPRINTF(&log, "%s. errcode=%d", log, errno)overwrites thelogpointer with a new allocation without freeing the old one first. The original allocation atlogis lost.Fix:
Bug6 ·
udpstream.c— receive buffer and socket not freed on function exitHow found: ASAN
Location:
udpstream.c:268(buffer allocation), end ofrun_ntttcp_receiver_udp4_stream()Root cause:
buffer = malloc(udp_recv_size)(65,536 bytes) is allocated for the receive loop but never freed before the function returns.sockfdis also not closed.Fix: Added
free(buffer)andclose(sockfd)before the function returns.Evidence (ASAN):
./ntttcp -r 127.0.0.1 -t 3 -P 1 -u -N # Exit 23(65,544 = 65,536-byte buffer + 8-byte threads array from issue Bug14)
Bug7 ·
endpointsync.c— redundantip_address_max_sizeassignment removedHow found: Static analysis
Location:
endpointsync.c:45Root cause:
ip_address_max_sizewas assigned twice — the second assignment was redundant and had no effect. Not a leak, but a code clarity fix.Fix: Removed the duplicate assignment.
Bug8 ·
endpointsync.c— double malloc ofip_address_str+ missingfreeaddrinfo()How found: Valgrind, static analysis
Location:
endpointsync.c:327, 336increate_receiver_sync_socket()Root cause:
ip_address_stris allocated withmalloc(INET_ADDRSTRLEN)at line 327, then immediately allocated again at line 336. The first allocation is overwritten and leaked (16 bytes).getaddrinfo()result (serv_info) allocated at line 320 is never freed withfreeaddrinfo()(64 bytes).Fix: Removed the duplicate
ip_address_strallocation; addedfreeaddrinfo(serv_info)after use.Evidence (Valgrind):
Bug9 ·
tcpstream.c—sockfdnot closed in bind-retry loop inntttcp_server_listen()How found: Static analysis,
valgrind --track-fds=yesLocation:
tcpstream.c:375Root cause:
ntttcp_server_listen()iterates throughgetaddrinfo()results trying to bind. A newsockfdis opened on each iteration viasocket(). Whenbind()fails the loop callscontinuewithout closingsockfdfirst.Fix: Added
close(sockfd)beforecontinuein the bind-failure path.Bug10 ·
tcpstream.c— log string leaked viaASPRINTFoverwrite inntttcp_server_listen()How found: Valgrind, static analysis
Location:
tcpstream.c:367Root cause: Same overwrite pattern as issue Bug5. When
bind()fails, the function builds an error string by overwritinglogwithASPRINTF(&log, "%s. errcode=%d", log, errno), losing the original pointer.Fix: Same pattern — save the old pointer before overwriting and free it afterwards.
Evidence (Valgrind):
Bug11 ·
tcpstream.c—newfdnot closed inntttcp_server_epoll()error pathsHow found: Static analysis,
valgrind --track-fds=yesLocation:
tcpstream.c:500–511, 532Root cause:
accept()returnsnewfd. Ifset_socket_non_blocking(),set_socket_tcp_nodelay(), orepoll_ctl()subsequently fails, the code logs an error andcontinues without callingclose(newfd). Each such failure leaks a socket descriptor.Fix: Added
close(newfd)before eachcontinuein the failure paths.Evidence (ASAN — epoll receiver, no-sync):
Receiver ASAN output shows only the threads array leak (Bug14) — no new heap leaks. The
newfdFD leak is not detectable by LSAN; usevalgrind --track-fds=yesto confirm.Bug12 ·
tcpstream.c—newfdnot closed inntttcp_server_select()error pathsHow found: Valgrind, static analysis
Location:
tcpstream.c:647–656Root cause: Same pattern as issue Bug11, in the
select()-based path.newfdfromaccept()is not closed whenset_socket_non_blocking()orset_socket_tcp_nodelay()fails.Fix: Added
close(newfd)before eachcontinuein the failure paths.Evidence (Valgrind):
Bug13 ·
endpointsync.c—newfdnot closed in sync socket accept handlingHow found: Static analysis
Location:
endpointsync.c:399–407increate_receiver_sync_socket()Root cause:
newfdfromaccept()in the sync connection handler is not closed whenset_socket_non_blocking()orepoll_ctl()fails, leaking the descriptor.Fix: Added
close(newfd)before eachcontinuein the failure paths.Bug14 ·
ntttcp.c—e->results->threadsarray pointer never freedHow found: ASAN, Valgrind
Location:
ntttcp.c:160(alloc),ntttcp.c:229(teardown)Root cause:
new_ntttcp_test_endpoint()allocates an array ofntttcp_test_endpoint_thread_result*pointers at line 160.free_ntttcp_test_endpoint_and_test()correctly frees each individual element in the array, but never frees the array itself before freeinge->results.Fix: Added
free(e->results->threads)after the loop, beforefree(e->results).Evidence (ASAN) — present in every test run:
./ntttcp -s 127.0.0.1 -t 3 -P 2 -n 1 # TCP sender, 2 ports × 1 thread = 16 bytesEvidence (Valgrind) — all four roles:
./ntttcp -s 127.0.0.1 -t 5 -P 1 -n 1./ntttcp -r 127.0.0.1 -t 5 -P 1 -n 1./ntttcp -s 127.0.0.1 -t 5 -P 1 -u -N./ntttcp -r 127.0.0.1 -t 5 -P 1 -u -NLeak size =
sizeof(void *) × total_threads(8 bytes per pointer on 64-bit).Bug15 ·
udpstream.c— wrongsocket()type parameter in UDP senderHow found: Static analysis
Location:
udpstream.c:72inrun_ntttcp_sender_udp4_stream()Root cause: The
socket()system call receivessc->domain(the address family, e.g.AF_INET = 2) as both the first and second argument. The second argument should be the socket type (SOCK_DGRAM).On Linux
AF_INET == SOCK_DGRAM == 2, so IPv4 UDP works by coincidence. With IPv6 (AF_INET6 = 10), this becomessocket(AF_INET6, 10, 0)=socket(AF_INET6, SOCK_RAW, 0), silently creating a raw socket instead of a datagram socket.Fix:
Bug16 ·
udpstream.c—sockfdnot closed in bind-retry loop in UDP receiverHow found: Static analysis,
valgrind --track-fds=yesLocation:
udpstream.c:247inrun_ntttcp_receiver_udp4_stream()Root cause: The function iterates through
getaddrinfo()results trying to bind. A new socket is created on each iteration. Whenbind()fails the loop callscontinuewithout closingsockfdfirst.Fix: Added
close(sockfd)beforecontinue.Evidence: Confirmed by
valgrind --track-fds=yes ./ntttcp -r -u. Valgrind reports the successfully-bound socket as open at exit (sinceclose(sockfd)is also missing at function return — fixed by issue Bug6).Bug17 ·
oscounter.c—/proc/interruptsfile descriptor not closed on early returnHow found: Valgrind (new bug discovered during testing)
Location:
oscounter.c:85inget_interrupts_from_proc_by_dev()Root cause: The function opens
/proc/interruptswithfopen()at line 85, then checks whetherdev_nameis empty. The default value ofshow_dev_interruptsis""(seentttcp.c:53), so this early-return path is taken on every normal test execution. Thefclose()at the end of the function is never reached.run_ntttcp_throughput_management()calls this function twice (lines 161 and 194), so 2 FDs and 944 bytes are leaked on every test run by both sender and receiver.Fix:
Evidence (Valgrind — TCP sender, default flags):
Confirmed in all four roles: TCP sender, TCP receiver, UDP sender, UDP receiver.
When
--show-dev-interruptsis given a non-empty device name, the FDs are closed correctly and no leak appears.Detection Methods Summary
--leak-check=full--track-fds=yesHow to Verify After This PR
Post-Fix Verification Results
All tests run on the fixed codebase. ASAN build +
ASAN_OPTIONS="detect_leaks=1:leak_check_at_exit=1:exitcode=23".ASAN Results
TCP sender (no-sync) — exit 0, no leaks:
UDP sender + receiver (no-sync) — both exit 0:
1
process_mappingsstrdup — no longer leaks (was exit 23):Bug3 UDP bogus interface log leak — no longer leaks (was exit 23):
Valgrind Results
Build:
make CFLAGS="-g -O0 -fno-omit-frame-pointer"Flags:
--track-fds=yes --leak-check=full --show-leak-kinds=all --error-exitcode=1UDP sender — clean (previously: threads array + socket FD leaks):
UDP receiver — clean (previously: 65,536-byte buffer + threads array leaked):
TCP sender — clean (previously: Bug17
/proc/interruptsFDs open, 944 bytes reachable):Previously (unfixed TCP sender, Bug17):
Functional Tests Pass