Skip to content

Persistent abstract unix domain sockets resolved to wrong resource #22617

Closed

Description

@Roysten

Description

The host of an abstract unix domain socket starts with a null byte. This null byte trips up PHP when it tries to resolve the address to a resource. PHP always returns the same resource.

This is reproducible using the following server and client scripts. The server creates two server sockets (\0test_socket_1 and \0test_socket_2). The client connects with both sockets.

Server:

<?php
// server.php
// This script creates two abstract Unix domain sockets and listens on both

function create_server($socket_name) {
    // Abstract socket: prefix with null byte
    $address = "unix://\0{$socket_name}";
    
    $socket = stream_socket_server($address, $errno, $errstr);
    if (!$socket) {
        echo "Failed to create socket at \\0{$socket_name}: $errstr ($errno)\n";
        return null;
    }
    
    echo "Server listening on abstract socket: \\0{$socket_name}\n";
    return $socket;
}

function handle_client($client_socket, $server_name) {
    $data = fread($client_socket, 1024);
    echo "[$server_name] Received: $data";
    
    fclose($client_socket);
}

// Create two abstract servers
$server1 = create_server('test_socket_1');
$server2 = create_server('test_socket_2');

if (!$server1 || !$server2) {
    exit(1);
}

echo "Both servers started. Waiting for connections...\n";
echo "Press Ctrl+C to stop.\n\n";

// Keep accepting connections
while (true) {
    $read = array($server1, $server2);
    $write = null;
    $except = null;
    
    $changed = stream_select($read, $write, $except, 1);
    
    if ($changed === false) {
        echo "stream_select error\n";
        break;
    }
    
    foreach ($read as $socket) {
        if ($socket === $server1) {
            $client = stream_socket_accept($server1);
            if ($client) {
                handle_client($client, "Server1");
            }
        } elseif ($socket === $server2) {
            $client = stream_socket_accept($server2);
            if ($client) {
                handle_client($client, "Server2");
            }
        }
    }
}

?>

Client:

<?php

// Connect to first abstract socket
echo "Connecting to abstract socket 1...\n";
$socket1 = pfsockopen("unix://\0test_socket_1", 0, $errno1, $errstr1);

if (!$socket1) {
    echo "Failed to connect to socket1: $errstr1 ($errno1)\n";
    exit(1);
}

echo "Connected to abstract socket 1\n";
echo "Socket 1 resource type: " . get_resource_type($socket1) . "\n";
echo "Socket 1 resource ID: " . (int)$socket1 . "\n\n";

// Connect to second abstract socket
echo "Connecting to abstract socket 2...\n";
$socket2 = pfsockopen("unix://\0test_socket_2", 0, $errno2, $errstr2);

if (!$socket2) {
    echo "Failed to connect to socket2: $errstr2 ($errno2)\n";
    exit(1);
}

echo "Connected to abstract socket 2\n";
echo "Socket 2 resource type: " . get_resource_type($socket2) . "\n";
echo "Socket 2 resource ID: " . (int)$socket2 . "\n\n";

?>

Output:

Connecting to abstract socket 1...
Connected to abstract socket 1
Socket 1 resource type: persistent stream
Socket 1 resource ID: 4

Connecting to abstract socket 2...
Connected to abstract socket 2
Socket 2 resource type: persistent stream
Socket 2 resource ID: 4

I expect the resource IDs for socket 1 and socket 2 to be distinct.

The problem is probably the function php_stream_from_persistent_id. It accepts a persistent ID, which is of the format pfsockopen__<hostname>. This function does a strlen on the persistent ID. Because of the leading null byte in the hostname it always returns only the length of the pfsockopen__ prefix (i.e. the actual hostname is not taken into account).

PHP Version

PHP 8.4.23 (cli) (built: Jul  3 2026 12:26:56) (NTS)
Copyright (c) The PHP Group
Built by Debian
Zend Engine v4.4.23, Copyright (c) Zend Technologies
    with Zend OPcache v8.4.23, Copyright (c), by Zend Technologies
    with Xdebug v3.4.5, Copyright (c) 2002-2025, by Derick Rethans

Operating System

Debian 13 (trixie)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions