You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
If fileno parameter is given then the file descriptor will be used to connect to an LDAP server. The fileno must either be a socket file descriptor as int or a file-like object with a fileno() method that returns a socket file descriptor. The socket file descriptor must already be connected. LDAPObjectdoes not take ownership of the file descriptor. It must be kept open during operations and explicitly closed after the LDAPObject is unbound.
However it seemingly is the case that it does take ownership
1490e99, #460, #543 are likely due to this confusion as well.
Following the code shows it goes like this.
ldap.initalize(*args, fileno=fd) will result in ldap_init_fd() being called which executes code that essentially creates a new ldap connection object that is then bound to the passed in fileno it then based on the protocol passed (LDAP_PROTO_TCP, LDAP_PROTO_UDP, LDAP_PROTO_IPC, LDAP_PROTO_EXT) will assign the protocols respective sockbuf_IO(s) set of {setup, ctrl, read, write, close} functions (except for LDAP_PROTO_EXT which the user specifies a custom struct elsewhere). These functions dictate how to interact with the underlying communication mechanism. Most relevant to this issue is the “close” functions
For TCP/UDP sockbuf_IO it calls “tcp_close()” which is system dependent macro but for Linux is likely the below
define tcp_close( s ) (shutdown( s, SHUT_RDWR ), close( s ))
For IPC it uses ber_sockbuf_io_fd (where sb_fd_close() is just close(2)
In all cases except LDAP_PROTO_EXT(where it’s user dependent), when a connection is freed some form of close() is called on the fd.
I’m not sure why the documentation thinks it doesn’t “take ownership”, but maybe it’s because of this section in ldap_init_fd()
However this just means the connection won’t be reclaimed while LDAP object is in use, but that doesn’t stop it from being cleaned up on free.
If I’m following things correctly: ldap.initialize(*args, fileno=fileno) will create a ldap object with an ldap connection from the connected socket fd we pass in.
But on ldap_unbind_ext() which ldap_destroy(), ldap_unbind(), dealloc(LDAPObject *self) etc, eventually call. ldap_unbind_ext() will call ldap_ld_free which if the ref count for the ldap object is < 2, it will perform
/* free and unbind from all open connections */
while ( ld->ld_conns != NULL ) {
ldap_free_connection( ld, ld->ld_conns, /*force=*/1, /*unbind=*/close);
}
Sockbuf_IO_Desc *p;
assert( sb != NULL);
p = sb->sb_iod;
while ( p ) {
if ( p->sbiod_io->sbi_close && p->sbiod_io->sbi_close( p ) < 0 ) {
return -1;
}
p = p->sbiod_next;
}
sb->sb_fd = AC_SOCKET_INVALID;
return 0;
Going way back to the Sockbuf_IO functions that were bound during the initalize discussed above.
Which means for all the supported protocols in python-ldap (TCP, UDP, IPC) at minimum close(fd) is called.
LDAPObject’s deconstructor will call ldap_unbind_ext() which as described above means it will always lead to closing the FD passed in to ldap.initalize(*args, fileno=fileno)
ldap_unbind_ext(3)
synchronously unbind from the LDAP server and close the
connection
So it seems like this is somewhat expected.
Why do the Docs believe libopenldap does not take ownership?
Steps to reproduce:
The simplest repro is
import ldap
import socket
# set openldap debugging levels
ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
# monkey patch __del__ method to see when it is called
ldap.ldapobject.LDAPObject.__del__ = lambda self: print(f"calling __del__ of {self}")
def simple_repro():
# Connect to an "LDAP server" (that doesn't exist)
client, server = socket.socketpair(family=socket.AF_UNIX, type=socket.SOCK_STREAM)
print(f"client={client.fileno()}, server={server.fileno()}")
l = ldap.initialize('ldap://localhost:1389', trace_level=1, fileno=client.fileno())
print(f"l's fileno={l.fileno()}")
del l # force l's deconstruction
server.close()
# this will fail due to l's deconstruction resulting in close(client.fileno())
client.close()
simple_repro()
Which outputs
client=3, server=4
ldap_url_parse_ext(ldap://localhost:1389)
ldap_create
ldap_url_parse_ext(ldap://localhost:1389)
ldap_new_connection 1 0 0
*** <ldap.ldapobject.SimpleLDAPObject object at 0x7fbcde13f3d0> ldap://localhost:1389 - SimpleLDAPObject.set_option
((17, 3), {})
*** <ldap.ldapobject.SimpleLDAPObject object at 0x7fbcde13f3d0> ldap://localhost:1389 - SimpleLDAPObject.get_option
((1,), {})
l's fileno=3
calling __del__ of <ldap.ldapobject.SimpleLDAPObject object at 0x7fbcde13f3d0>
ldap_free_connection 1 1
ldap_send_unbind
ldap_free_connection: actually freed
Traceback (most recent call last):
File "examples/repro.py", line 19, in <module>
simple_repro()
File "examples/repro.py", line 17, in simple_repro
client.close()
File "/opt/python/python-3.11/lib64/python3.11/socket.py", line 503, in close
self._real_close()
File "/opt/python/python-3.11/lib64/python3.11/socket.py", line 497, in _real_close
_ss.close(self)
OSError: [Errno 9] Bad file descriptor
Thanks for digging into this, I stumbled upon this in #544 but never clocked that we have documentation to the contrary.
There is no record of why the code suggests that ownership will not be claimed, libldap certainly suggests nothing either way (and as you figured, all builtin sockbuf implementations will close the fd passed in).
I would be OK with a documentation change + removal of the refcnt increment code, making it the caller's responsibility to ensure they don't touch/close the fd themselves.
Uh oh!
There was an error while loading. Please reload this page.
Issue description:
The ldap.initialize() documents say
However it seemingly is the case that it does take ownership
1490e99, #460, #543 are likely due to this confusion as well.
Following the code shows it goes like this.
ldap.initalize(*args, fileno=fd)
will result inldap_init_fd()
being called which executes code that essentially creates a new ldap connection object that is then bound to the passed in fileno it then based on the protocol passed (LDAP_PROTO_TCP, LDAP_PROTO_UDP, LDAP_PROTO_IPC, LDAP_PROTO_EXT) will assign the protocols respective sockbuf_IO(s) set of {setup, ctrl, read, write, close} functions (except for LDAP_PROTO_EXT which the user specifies a custom struct elsewhere). These functions dictate how to interact with the underlying communication mechanism. Most relevant to this issue is the “close” functionsFor TCP/UDP sockbuf_IO it calls “tcp_close()” which is system dependent macro but for Linux is likely the below
For IPC it uses ber_sockbuf_io_fd (where sb_fd_close() is just close(2)
In all cases except LDAP_PROTO_EXT(where it’s user dependent), when a connection is freed some form of close() is called on the fd.
I’m not sure why the documentation thinks it doesn’t “take ownership”, but maybe it’s because of this section in ldap_init_fd()
However this just means the connection won’t be reclaimed while LDAP object is in use, but that doesn’t stop it from being cleaned up on free.
If I’m following things correctly:
ldap.initialize(*args, fileno=fileno)
will create a ldap object with an ldap connection from the connected socket fd we pass in.But on ldap_unbind_ext() which ldap_destroy(), ldap_unbind(),
dealloc(LDAPObject *self)
etc, eventually call.ldap_unbind_ext()
will callldap_ld_free
which if the ref count for the ldap object is < 2, it will performWell ldap_free_connection() when force=1, will ignore the connection refcount and free the ldap connection anyway.
Specifically it frees/closes the fd by calling either of these functions
Where
_sockbuf_free()
also callsber_int_sb_close()
And that function is
Going way back to the Sockbuf_IO functions that were bound during the initalize discussed above.
Which means for all the supported protocols in
python-ldap
(TCP, UDP, IPC) at minimumclose(fd)
is called.LDAPObject’s deconstructor will call
ldap_unbind_ext()
which as described above means it will always lead to closing the FD passed in toldap.initalize(*args, fileno=fileno)
ldap_unbind_ext(3)
is defined asWhy do the Docs believe libopenldap does not take ownership?
Steps to reproduce:
The simplest repro is
Which outputs
If you run
strace
on it the relevant evidence isOperating system:
Linux
Python version:
Python 3.11.8
python-ldap version:
3.4.0
The text was updated successfully, but these errors were encountered: