Repository navigation
Hide benchmark arguments from the imported kernel - #54
Open
LaelaZorana wants to merge 1 commit into
Open
LaelaZorana wants to merge 1 commit into
LaelaZorana wants to merge 1 commit into
Conversation
The benchmark call arguments were reachable from an imported kernel. The compiled do_bench runs the kernel on a second thread while _do_bench_impl is still on the stack of the main thread, so sys._current_frames hands the kernel that frame, with the result pipe and the expected output generator bound as locals. One frame further down BaseProcess.run holds self, and self._args is the tuple the child was started with. Issue gpu-mode#3. _do_bench_impl now hands the runner duplicated descriptors of its own and closes the three connection objects, clears _args and _kwargs on the child Process object, deletes the argument names from its own frame, and passes the packed call by popping it from a list. The tuple lives on the interpreter stack and on the C++ side while the kernel runs, and every frame is clear of it. os.dup returns descriptors with FD_CLOEXEC set, so an exec from user code leaves the pipes behind. The traceback pipe stays on the frame because the except handler needs it. test/test_hidden_arguments.py runs on CPU with the extension stubbed. It calls _do_bench_impl through a stand in for the child Process object and, in one test, in a real spawned child. From a worker thread it walks every thread through sys._current_frames and looks for the five names, and for the generator, the arguments dict and any Connection object by identity, in frame locals, inside tuples and lists, and inside _args. Before the change it finds all of them, after the change it finds none of them, and the result pipe still writes.
LaelaZorana
force-pushed
the
fix/issue-3-hide-args
branch
from
September 17, 2026 17:18
0f0755b to
3f519be
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue #3.
An imported kernel puts the benchmark arguments in easy reach.
do_benchofbinding.cppdispatches the kernel to a second thread,run_thread, yet_do_bench_implis left on the main thread stack. A walk from the kernel's own thread can not see that frame since #43, butsys._current_framesstill puts the top one for every thread at your disposal. The main thread has as its localsout_fd, the result pipe, andin_fd, along withsupervisor_sock,test_argsandtest_generator. Step down a frame toBaseProcess.runand there isself._args, the tuple used to start the child.In order to separate the runner from these descriptors,
_do_bench_implwill now make duplicates of them and then close the three connection objects. It also does away with the argument names in the frame and clears_kwargsand_argsof the child Process. The call is made by popping it off a list so the tuple resides on the C++ side and interpreter stack during the kernel run but off every frame. Any exec in user code will leave the pipes be asos.dupreturns FD_CLOEXEC descriptors. The except handler can still make use of the traceback pipe on the frame. It is text bound for the parent.I tried a helper to get at the caller's frame with
f_locals. It went nowhere. 3.12 treatsf_localsas a snapshot which precludes writing to fast locals and the proxy in 3.13 refuses to delete a name. With 3.12 being the floor for this repo I have resorted to plaindelin_do_bench_impl.This is what the suite in
test/test_hidden_arguments.pyis for, using an extension stub on CPU. A stand in for the child Process is employed to call_do_bench_implin one test to have the stack, with itsrunframe and_args, be as it would be in a spawn. Another will do the same in a proper spawned child. The worker thread then goes throughsys._current_framesto look for the five names, and for the generator, the arguments dict and any Connection object by identity, in frame locals, tuples, lists and_args. Prior to this change they are all there. Now the walk is empty but the result pipe continues to write. I have put it to the test on 3.12, 3.13 and 3.14. Revert the fix and four of the five fail while the control still passes.The exploits under
exploits/such asheap_sig_scan,stack_sig_scan,pipe_interpose,file_structandseccomp_trapare unaffected. They use their own thread forsys._getframeand #43 has already put them off this frame. Ditto foraes_key_recoveryandproc_mem_bypasswhich are dependent ongc.get_objects.Then there is
gc.get_objects. So long as the runner has them a heap scan will find the live python objects of the tuple and generator. The only way to put them out of sight is to keep the expected output out of python before the import, like the approach of the deleted file in the issue, but that is more of an undertaking.