VAITP Dataset

← Back to the dataset

CVE-2025-48945

pycares < 4.9.0: Use-after-free in Channel object during garbage collection.

  • CVSS 8.2
  • CWE-416
  • Memory Corruption
  • Remote

pycares is a Python module which provides an interface to c-ares. c-ares is a C library that performs DNS requests and name resolutions asynchronously. Prior to version 4.9.0, pycares is vulnerable to a use-after-free condition that occurs when a Channel object is garbage collected while DNS queries are still pending. This results in a fatal Python error and interpreter crash. The vulnerability has been fixed in pycares 4.9.0 by implementing a safe channel destruction mechanism.

CVSS base score
8.2
Published
2025-06-20
OWASP
A04:2021 Insecure Design
Orthogonal defect classification
Timing/Serialization
Code defect classification
Timing Issues
Category
Memory Corruption
Subcategory
Use-After-Free Errors
Accessibility scope
Remote
Impact
Denial of Service (DoS)
Affected component
pycares
Fixed by upgrading
Yes

Solution

Upgrade to pycares >= 4.9.0

Vulnerable code sample

import pycares
import asyncio

async def resolve(hostname, loop, results):
    """Resolves a hostname asynchronously using pycares."""
    channel = pycares.Channel(loop=loop)
    try:
        result = await loop.run_in_executor(None, channel.gethostbyname, hostname, pycares.socket.AF_INET)
        results.append(result)
        # Simulate holding onto the channel longer than necessary, increasing the chance of GC.
        await asyncio.sleep(0.01)  # Short sleep to let other tasks run and GC to potentially trigger.

    except pycares.AresError as e:
        print(f"Resolution failed for {hostname}: {e}")
    finally:
        #Explicitly deleting the channel, to increase chances of garbage collection 
        #This is crucial for demonstrating the vulnerability.
        del channel

async def main():
    """Main function to demonstrate the use-after-free vulnerability."""
    loop = asyncio.get_running_loop()
    hostnames = ["google.com", "example.com", "invalid-domain-that-hopefully-does-not-exist.com"]
    results = []

    tasks = [resolve(hostname, loop, results) for hostname in hostnames]

    try:
        await asyncio.gather(*tasks)
        print("Resolution results:", results)
    except Exception as e:
        print(f"An error occurred: {e}")


if __name__ == "__main__":
    #Ensure an older vulnerable version of pycares is installed.
    # pip install pycares==4.8.0 #Or any version below 4.9.0
    asyncio.run(main())

Patched code sample

import pycares
import asyncio
import gc

class SafeChannel:
    def __init__(self):
        self._channel = pycares.Channel()
        self._pending_queries = set()
        self._loop = asyncio.get_event_loop()

    def query(self, host, query_type, callback):
        """Submits a DNS query and tracks it."""
        try:
            query_id = self._channel.query(host, query_type, callback)
            self._pending_queries.add(query_id) # Assuming query() returns a unique ID
        except pycares.error.AresError as e:
            print(f"Query failed: {e}")
        
        return query_id


    def _query_done_callback(self, query_id):
        """Removes a query from the pending set when it's done."""
        if query_id in self._pending_queries:
            self._pending_queries.remove(query_id)

    async def close(self):
        """Safely closes the channel, cancelling pending queries."""
        # Cancel any pending queries before closing the channel.
        # Cancellation might not be possible with all DNS queries.  Handle errors gracefully.

        while self._pending_queries:
            try:
                 query_id = self._pending_queries.pop()
            except KeyError:
                 break  # The set could be empty because of concurent task completion.

            # Attempt to cancel or ignore if cancellation fails
            try:
                #cancellation not directly supported by pycares, simulating
                #the effect by simply waiting
                await asyncio.sleep(0.001)
            except Exception as e:
                print(f"Error cancelling query {query_id}: {e}")

        self._channel.cancel()
        self._channel = None # prevent access after closing
        self._pending_queries = None  #Prevent access after closing
        self._loop = None


    def __del__(self):
        """Destructor to ensure channel is closed when garbage collected."""
        if hasattr(self, '_channel') and self._channel is not None:
            print("WARNING: Channel was not explicitly closed.  Closing now...")
            #try to clean up.  This *may* still cause issues if there are callbacks actively using the object.
            if hasattr(self, '_loop') and self._loop is not None and self._loop.is_running():
                asyncio.run(self.close())
            else:
                self._channel.cancel()  # Try cancelling even without an event loop.
                self._channel = None

async def main():
    safe_channel = SafeChannel()

    def my_callback(result, error):
        print(f"Result: {result}, Error: {error}")
        safe_channel._query_done_callback(query_id)

    query_id = safe_channel.query("example.com", pycares.QUERY_TYPE_A, my_callback)


    await asyncio.sleep(0.1) #Simulate waiting for some time for the request to complete.
    await safe_channel.close()  # Explicitly close the channel


if __name__ == "__main__":
    asyncio.run(main())

Cite this entry

@misc{vaitp:cve202548945,
  title        = {{pycares < 4.9.0: Use-after-free in Channel object during garbage collection.
}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2025},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2025-48945},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2025-48945/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::