Repository navigation
ldap_exop_sync() crashes when the optional $response_data argument is omitted #25005
Description
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
ndossche commentedon Oct 30, 2026
Fixed via #23339