php_readpipe() builds an fd_set from the script server's read descriptor without checking it against FD_SETSIZE:
php.c:196 (develop):
FD_ZERO(&fds);
FD_SET(php_processes[php_process].php_read_fd, &fds);
switch (select(php_processes[php_process].php_read_fd + 1, &fds, NULL, NULL, &timeout)) {
FD_SET does not range check. A descriptor at or above FD_SETSIZE (1024 on glibc) sets a bit past the end of the fd_set, which lives on php_readpipe()'s stack. Where the C library implements the __FD_SETSIZE fortification check the process aborts instead; where it does not, this is a stack write at an attacker-influenced offset in a binary that is installed setuid root.
Observed while adding tests for this file. With the descriptor set above the limit the whole test binary dies:
*** bit out of range 0 - FD_SETSIZE on fd_set ***: terminated
ping_icmp() already guards its own select() the same way, at ping.c:478, so the pattern is established in the tree:
if (icmp_socket >= FD_SETSIZE) {
SPINE_LOG(("ERROR: Device[%i] ICMP socket %d exceeds FD_SETSIZE %d", host->id, icmp_socket, FD_SETSIZE));
...
}
On reachability: the script server pipes are created by pipe() at poller startup, so on a small installation they sit low. Two things push them up. Cacti's own sizing guidance has operators raise RLIMIT_NOFILE well past 1024, and spine is thread-per-device with SNMP sockets, database connections and script pipes open concurrently. php_readpipe() also restarts a script server from inside itself on timeout and on select failure, calling php_close() then php_init(), so a replacement pipe is allocated mid cycle when the descriptor table is at its fullest rather than at startup when it is empty.
Suggested fix is the ping_icmp() treatment: refuse the descriptor, log which one and the limit, and restart the server so it gets a fresh allocation.
Present on develop and 1.2.x. Fix and a regression test are in #597.
php_readpipe()builds anfd_setfrom the script server's read descriptor without checking it againstFD_SETSIZE:php.c:196(develop):FD_SETdoes not range check. A descriptor at or aboveFD_SETSIZE(1024 on glibc) sets a bit past the end of thefd_set, which lives onphp_readpipe()'s stack. Where the C library implements the__FD_SETSIZEfortification check the process aborts instead; where it does not, this is a stack write at an attacker-influenced offset in a binary that is installed setuid root.Observed while adding tests for this file. With the descriptor set above the limit the whole test binary dies:
ping_icmp()already guards its ownselect()the same way, atping.c:478, so the pattern is established in the tree:On reachability: the script server pipes are created by
pipe()at poller startup, so on a small installation they sit low. Two things push them up. Cacti's own sizing guidance has operators raiseRLIMIT_NOFILEwell past 1024, and spine is thread-per-device with SNMP sockets, database connections and script pipes open concurrently.php_readpipe()also restarts a script server from inside itself on timeout and on select failure, callingphp_close()thenphp_init(), so a replacement pipe is allocated mid cycle when the descriptor table is at its fullest rather than at startup when it is empty.Suggested fix is the
ping_icmp()treatment: refuse the descriptor, log which one and the limit, and restart the server so it gets a fresh allocation.Present on
developand1.2.x. Fix and a regression test are in #597.