Skip to content

ldap_exop_sync() crashes when the optional $response_data argument is omitted #25005

Closed

Description

@apoc

Description

php_ldap_exop() always takes the synchronous branch for ldap_exop_sync() (force_sync = true), but assigns to retdata unconditionally. When the optional by-ref $response_data is omitted, retdata is NULL and ZEND_TRY_ASSIGN_REF_* dereferences it (ext/ldap/ldap.c ~L4103/L4107).

The following code (any LDAP server that answers WHO_AM_I with success; minimal responder below):

<?php
$link = ldap_connect('ldap://127.0.0.1:3389');
ldap_set_option($link, LDAP_OPT_PROTOCOL_VERSION, 3);
var_dump(ldap_bind($link, '', ''));
var_dump(ldap_exop_sync($link, LDAP_EXOP_WHO_AM_I));

Resulted in this output:

bool(true)
<access violation, exit code 0xC0000005>

But I expected this output instead:

bool(true)
bool(true)

With ldap_exop_sync($link, LDAP_EXOP_WHO_AM_I, null, null, $data) it works ($data === "").

Suggested fix: guard both assignments on retdata, and still free lretdata when retdata is NULL. (Do not gate the whole sync block on retdata; that would turn the call asynchronous.) ldap_exop.phpt always passes $retdata, so no test covers this.

Minimal LDAP responder (python3 server.py 3389)
"""Minimal LDAP server: answers BindRequest with success and any ExtendedRequest
with an ExtendedResponse of resultCode 0 and no responseValue. Enough to drive
ldap_exop_sync() into the success path."""
import socket, sys, threading

def tlv(tag, body):
    n = len(body)
    if n < 128:
        return bytes([tag, n]) + body
    b = n.to_bytes((n.bit_length() + 7) // 8, 'big')
    return bytes([tag, 0x80 | len(b)]) + b + body

def enc_int(n):
    if n == 0:
        return tlv(0x02, b'\x00')
    return tlv(0x02, n.to_bytes((n.bit_length() + 7) // 8, 'big'))

def read_msg(conn):
    hdr = conn.recv(2)
    if len(hdr) < 2:
        return None
    ln = hdr[1]
    if ln & 0x80:
        k = ln & 0x7f
        ln = int.from_bytes(conn.recv(k), 'big')
    buf = b''
    while len(buf) < ln:
        d = conn.recv(ln - len(buf))
        if not d:
            return None
        buf += d
    return hdr + buf

def parse_msg(raw):
    i = 2 if raw[1] < 0x80 else 2 + (raw[1] & 0x7f)
    assert raw[i] == 0x02
    n = raw[i + 1]
    mid = int.from_bytes(raw[i + 2:i + 2 + n], 'big')
    i += 2 + n
    return mid, raw[i]

def handle(conn):
    try:
        while True:
            raw = read_msg(conn)
            if raw is None:
                break
            mid, op = parse_msg(raw)
            if op == 0x60:                                   # BindRequest
                body = tlv(0x0a, b'\x00') + tlv(0x04, b'') + tlv(0x04, b'')
                conn.sendall(tlv(0x30, enc_int(mid) + tlv(0x61, body)))
            elif op == 0x50:                                 # UnbindRequest
                break
            else:                                            # ExtendedRequest, etc.
                body = tlv(0x0a, b'\x00') + tlv(0x04, b'') + tlv(0x04, b'')
                conn.sendall(tlv(0x30, enc_int(mid) + tlv(0x78, body)))
    except Exception as e:
        print('conn error:', e, file=sys.stderr)
    finally:
        conn.close()

def main():
    port = int(sys.argv[1])
    srv = socket.socket()
    srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    srv.bind(('127.0.0.1', port))
    srv.listen(8)
    print(f'listening on {port}', flush=True)
    while True:
        c, _ = srv.accept()
        threading.Thread(target=handle, args=(c,), daemon=True).start()

main()

PHP Version

PHP 8.7.0-dev (cli) (built: Oct 27 2026 17:51:17) (ZTS Visual C++ 2026 x64)
Zend Engine v4.7.0-dev (master @ 0af8a091eec28513a6a0b758bcb10b430ba6c6de)

The code is the same in PHP-8.3 .. PHP-8.5 by source inspection (not run there).

Operating System

Windows 11 x64

Activity

  1. assigned and unassigned on Oct 28, 2026
  2. ndossche commented on Oct 30, 2026

    @ndossche
    Member

    Fixed via #23339

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions