container: DockerPort should hold a strong ref to ContainerClient (mirror DockerProcessHandle) - #6991
Open
Lougarou wants to merge 1 commit into
Open
Conversation
DockerPort held its owner as a bare ContainerClient& and dereferenced it (network, byteStreamFactory) after the co_await points in connect(). The sibling DockerProcessHandle holds a kj::Own<ContainerClient> via addRef() for exactly this reason. The bare reference is safe only while nothing drops the last ContainerClient ref during a suspended connect(), an ordering the type system does not enforce. Hold containerClient.addRef() (a kj::Own<ContainerClient>) like DockerProcessHandle. No behavior change in the safe case; removes the teardown-ordering dependency. Adds an ASAN regression test that drives connect() to a suspension point, frees the ContainerClient, then resumes: heap-use-after-free without the fix, passes with it.
|
All contributors have signed the CLA ✍️ ✅ |
Author
|
I have read the CLA Document and I hereby sign the CLA |
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.
ContainerClient::DockerPortholds its owner as a bareContainerClient&and dereferences it (containerClient.network,containerClient.byteStreamFactory) after theco_awaitpoints inconnect()(parse the ingress address, connect to the sidecar, read the CONNECT status). The sibling capability serverDockerProcessHandle— created fromexec()in the same file — holds its owner as akj::Own<ContainerClient>viacontainerClient.addRef()and even attaches a ref to its background task, precisely so it can outlive teardown races.DockerPortdoes not.Today
DockerPort's bare reference is safe only because of an unenforced coupling: nothing drops the lastContainerClientreference while aconnect()is suspended. In the normal actor-teardown paths the JS-heldPortcapability is dropped first, which cancels the in-flightconnect()before theContainerClientis freed (container.capnpsets a file-level$Cxx.allowCancellation, so the cancellation does unwind the coroutine). Nothing in the type system enforces that ordering, though. Any future path that frees theContainerClientwithout first cancelling an in-flightconnect()— for example wiring up the currently-deadActorContainer::containerClientmember (see below), or introducing aco_awaitbetween actor destruction and the container-client drop — resumesconnect()into freed memory.This PR makes
DockerPortholdcontainerClient.addRef()(akj::Own<ContainerClient>), exactly likeDockerProcessHandle. It removes the dependency on teardown ordering, is a no-op in the normal case, and does not create a reference cycle:getTcpPort()hands theDockerPortto the RPC layer viaresults.setPort()andContainerClientkeeps no reference back to it, so the new edge is released when the client drops thePortcapability (identical to howDockerProcessHandlealready behaves).This fix is independent of the exact body of
connect(): it holds the reference structurally, so it stays correct under refactors that relocate the connect logic (e.g. into aContainerClienthelper) as long asDockerPortkeeps borrowingContainerClient.Regression test
container-client-test.c++addsKJ_TEST("DockerPort connect() use-after-free on ContainerClient freed mid-call"). It vends aDockerPortviagetTcpPort, drivesconnect()to its first suspension point (aSuspendNetworkwhoseparseAddressparks on a fulfiller), drops the only strongContainerClientreference, then resumesconnect(). Under ASAN, with a bareContainerClient&the test aborts with a heap-use-after-free inDockerPort::connect() (.resume)(freed by~ContainerClient()when the lastContainer::Clientcapability reference is dropped mid-call); with theaddRef()fix it passes. The test runs no JavaScript, so it exercises only the kj/capnp capability-lifetime path.A one-line test-only accessor,
ContainerClient::setSidecarIngressHostPortForTest, lets the test driveconnect()without a live sidecar container.Separate observation: the dead
ActorContainer::containerClientmemberWhile tracing the container lifetime for this fix,
ActorContainer::containerClient(server.c++, declaredkj::Maybe<kj::Own<ContainerClient>>) appears to be dead: it is never assigned a value anywhere inserver.c++; thecontainerClient = kj::none;statements are no-ops on an always-emptyMaybe, and the comments around them describe ownership that is not wired up. The live container-client references are the rawContainerClient*entries in thecontainerClientsHashMap and theOwnhanded to the actor. Flagging it because it is exactly the kind of member that, if "fixed" to actually own aContainerClientwithout also givingDockerPorta strong ref, would make theconnect()use-after-free above reachable. Happy to split this into a separate issue/PR if the team prefers.