Skip to content

Persistent abstract unix domain sockets resolved to wrong resource #22617

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)

Metadata

Metadata

Assignees

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions