Skip to content

Commit f94e445

Browse files
author
Shanire
committed
Close the transfer socket and set CLOEXEC on received listener fds
get_fds_from() leaks two kinds of file descriptors on every graceful upgrade: - The connection returned by accept() is a bare RawFd that is never closed; only listen_fd is. Every completed upgrade therefore leaks one connected unix socket for the lifetime of the process. - recvmsg() is called with MsgFlags::empty(), so the listening sockets received over SCM_RIGHTS do not have FD_CLOEXEC set. Those fds are held for the lifetime of the process, so they are inherited by every child it later execs. The second one also interacts with the unclaimed-fd cleanup added in "Close unclaimed inherited listening sockets on graceful upgrade": an fd that reaches a process through exec() is not in the Fds table, so listen_addresses() cannot close it. Deployments that start the new generation by forking from the old one (needed when the service manager tracks a cgroup) receive each listening socket twice, and only one of the two copies is visible to that cleanup. Take ownership of the accepted connection with OwnedFd so it is closed on every path out of the function, including the early return from cmsgs()?, and pass MSG_CMSG_CLOEXEC so the received descriptors get FD_CLOEXEC atomically rather than through a follow-up fcntl(). The added test covers both defects independently: reverting MSG_CMSG_CLOEXEC makes it report the missing flag, and reverting the OwnedFd makes it find the accepted socket still open. Signed-off-by: Shanire <shanire86@gmail.com>
1 parent 0046038 commit f94e445

1 file changed

Lines changed: 99 additions & 3 deletions

File tree

  • pingora-core/src/server/transfer_fd

pingora-core/src/server/transfer_fd/mod.rs

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ use std::io::Write;
2525
#[cfg(target_os = "linux")]
2626
use std::io::{IoSlice, IoSliceMut};
2727
#[cfg(target_os = "linux")]
28-
use std::os::fd::{AsRawFd, BorrowedFd};
28+
use std::os::fd::{AsRawFd, BorrowedFd, FromRawFd, OwnedFd};
2929
use std::os::unix::io::RawFd;
3030
#[cfg(target_os = "linux")]
3131
use std::{thread, time};
@@ -188,13 +188,24 @@ where
188188
}
189189
};
190190

191+
// The accepted connection is only needed for this transfer. Take ownership of it so
192+
// that it is closed on every path out of this function, including the early return
193+
// from `cmsgs()?` below; otherwise every graceful upgrade leaks one unix socket.
194+
//
195+
// SAFETY: `fd` was just returned by accept(2) and is not owned or closed anywhere else.
196+
let conn = unsafe { OwnedFd::from_raw_fd(fd) };
197+
191198
let mut io_vec = [IoSliceMut::new(payload); 1];
192199
let mut cmsg_buf = nix::cmsg_space!([RawFd; MAX_FDS]);
200+
// MSG_CMSG_CLOEXEC sets FD_CLOEXEC on the received descriptors atomically. These
201+
// listening sockets are kept for the rest of the process's lifetime, so without it any
202+
// subprocess an application built on pingora execs inherits them, and can keep the
203+
// port bound after the server itself is gone.
193204
let msg: RecvMsg<UnixAddr> = socket::recvmsg(
194-
fd,
205+
conn.as_raw_fd(),
195206
&mut io_vec,
196207
Some(&mut cmsg_buf),
197-
socket::MsgFlags::empty(),
208+
socket::MsgFlags::MSG_CMSG_CLOEXEC,
198209
)
199210
.unwrap();
200211

@@ -387,6 +398,7 @@ mod tests {
387398

388399
use super::*;
389400
use log::{debug, error};
401+
use nix::fcntl;
390402

391403
fn init_log() {
392404
let _ = env_logger::builder().is_test(true).try_init();
@@ -468,6 +480,90 @@ mod tests {
468480
child.join().unwrap();
469481
}
470482

483+
/// How many fds in this process refer to a unix socket bound to `path`.
484+
///
485+
/// Reads the socket inodes bound to `path` from /proc/net/unix, then scans
486+
/// /proc/self/fd for descriptors pointing at them. Filtering by path keeps this
487+
/// unaffected by unrelated descriptors opened by tests running in parallel.
488+
fn unix_socket_fds_bound_to(path: &str) -> usize {
489+
let unix = std::fs::read_to_string("/proc/net/unix").unwrap();
490+
let inodes: HashSet<&str> = unix
491+
.lines()
492+
.filter_map(|line| {
493+
let mut cols = line.split_whitespace();
494+
let inode = cols.nth(6)?;
495+
(cols.next() == Some(path)).then_some(inode)
496+
})
497+
.collect();
498+
if inodes.is_empty() {
499+
return 0;
500+
}
501+
std::fs::read_dir("/proc/self/fd")
502+
.unwrap()
503+
.filter_map(|entry| std::fs::read_link(entry.ok()?.path()).ok())
504+
.filter(|target| {
505+
target
506+
.to_str()
507+
.and_then(|t| t.strip_prefix("socket:["))
508+
.and_then(|t| t.strip_suffix(']'))
509+
.is_some_and(|inode| inodes.contains(inode))
510+
})
511+
.count()
512+
}
513+
514+
#[test]
515+
fn test_receive_does_not_leak_fds() {
516+
init_log();
517+
const SOCK: &str = "/tmp/pingora_fds_receive3.sock";
518+
519+
let dumb_fd = socket::socket(
520+
AddressFamily::Unix,
521+
SockType::Stream,
522+
SockFlag::empty(),
523+
None,
524+
)
525+
.unwrap();
526+
527+
// receiver need to start in another thread since it is blocking
528+
let child = thread::spawn(move || {
529+
let mut buf: [u8; 32] = [0; 32];
530+
let (fds, _) = get_fds_from(SOCK, &mut buf, None).unwrap();
531+
assert_eq!(1, fds.len());
532+
533+
// The received listener is kept for the lifetime of the process, so it must
534+
// not be inherited by unrelated children across exec().
535+
let flags = fcntl::fcntl(
536+
// SAFETY: the fd was just received and stays open for this call.
537+
unsafe { BorrowedFd::borrow_raw(fds[0]) },
538+
fcntl::FcntlArg::F_GETFD,
539+
)
540+
.unwrap();
541+
assert!(
542+
fcntl::FdFlag::from_bits_truncate(flags).contains(fcntl::FdFlag::FD_CLOEXEC),
543+
"fd received over SCM_RIGHTS is missing FD_CLOEXEC"
544+
);
545+
546+
// The accepted connection is only needed during the transfer itself.
547+
assert_eq!(
548+
0,
549+
unix_socket_fds_bound_to(SOCK),
550+
"the accepted transfer socket was left open"
551+
);
552+
553+
// Don't leak the descriptors this test just received.
554+
for fd in fds {
555+
// SAFETY: received over SCM_RIGHTS just above and not owned anywhere else.
556+
drop(unsafe { OwnedFd::from_raw_fd(fd) });
557+
}
558+
});
559+
560+
let fds = vec![dumb_fd.as_raw_fd()];
561+
let buf: [u8; 32] = [1; 32];
562+
send_fds_to(fds, &buf, SOCK, None).unwrap();
563+
564+
child.join().unwrap();
565+
}
566+
471567
#[test]
472568
fn test_serde_via_socket() {
473569
init_log();

0 commit comments

Comments
 (0)