ptools 0.2.23

Utilities for inspecting Linux processes
//
//   Copyright (c) 2026 Basil Crow
//
//   Licensed under the Apache License, Version 2.0 (the "License");
//   you may not use this file except in compliance with the License.
//   You may obtain a copy of the License at
//
//       http://www.apache.org/licenses/LICENSE-2.0
//
//   Unless required by applicable law or agreed to in writing, software
//   distributed under the License is distributed on an "AS IS" BASIS,
//   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//   See the License for the specific language governing permissions and
//   limitations under the License.
//

use std::env;
use std::ffi::CString;
use std::fs::File;
use std::fs::{self};
use std::net::TcpListener;
use std::net::TcpStream;
use std::net::UdpSocket;
use std::os::fd::AsFd;
use std::os::fd::RawFd;
use std::os::unix::fs::FileTypeExt;
use std::thread;
use std::time::Duration;

use nix::fcntl::open;
use nix::fcntl::OFlag;
use nix::sys::epoll::Epoll;
use nix::sys::epoll::EpollCreateFlags;
use nix::sys::epoll::EpollEvent;
use nix::sys::epoll::EpollFlags;
use nix::sys::eventfd::EfdFlags;
use nix::sys::eventfd::EventFd;
use nix::sys::stat::Mode;
use nix::unistd::pipe2;

fn open_signalfd() -> RawFd {
    unsafe {
        let mut mask = std::mem::zeroed::<nix::libc::sigset_t>();
        if nix::libc::sigemptyset(&mut mask) != 0 {
            panic!("sigemptyset failed: {}", std::io::Error::last_os_error());
        }
        if nix::libc::sigaddset(&mut mask, nix::libc::SIGUSR1) != 0 {
            panic!("sigaddset failed: {}", std::io::Error::last_os_error());
        }
        if nix::libc::sigprocmask(nix::libc::SIG_BLOCK, &mask, std::ptr::null_mut()) != 0 {
            panic!("sigprocmask failed: {}", std::io::Error::last_os_error());
        }

        let fd = nix::libc::signalfd(-1, &mask, nix::libc::SFD_CLOEXEC | nix::libc::SFD_NONBLOCK);
        if fd < 0 {
            panic!("signalfd failed: {}", std::io::Error::last_os_error());
        }
        fd
    }
}

fn open_timerfd() -> RawFd {
    unsafe {
        let fd = nix::libc::timerfd_create(
            nix::libc::CLOCK_MONOTONIC,
            nix::libc::TFD_CLOEXEC | nix::libc::TFD_NONBLOCK,
        );
        if fd < 0 {
            panic!("timerfd_create failed: {}", std::io::Error::last_os_error());
        }

        let mut spec = std::mem::zeroed::<nix::libc::itimerspec>();
        spec.it_value.tv_sec = 5;
        if nix::libc::timerfd_settime(fd, 0, &spec, std::ptr::null_mut()) != 0 {
            panic!(
                "timerfd_settime failed: {}",
                std::io::Error::last_os_error()
            );
        }

        fd
    }
}

fn open_inotify() -> RawFd {
    unsafe {
        let fd = nix::libc::inotify_init1(nix::libc::IN_CLOEXEC | nix::libc::IN_NONBLOCK);
        if fd < 0 {
            panic!("inotify_init1 failed: {}", std::io::Error::last_os_error());
        }
        fd
    }
}

fn find_block_device_path() -> Option<String> {
    fs::read_dir("/dev").ok()?.flatten().find_map(|entry| {
        let path = entry.path();
        let metadata = fs::metadata(&path).ok()?;
        if metadata.file_type().is_block_device() {
            Some(path.to_string_lossy().to_string())
        } else {
            None
        }
    })
}

fn allow_ptrace_for_tests() {
    unsafe {
        nix::libc::prctl(
            nix::libc::PR_SET_PTRACER,
            nix::libc::PR_SET_PTRACER_ANY,
            0,
            0,
            0,
        );
    }
}

fn main() {
    allow_ptrace_for_tests();

    let signal_path =
        env::var("PTOOLS_TEST_READY_FILE").expect("PTOOLS_TEST_READY_FILE must be set");
    let block_device = find_block_device_path().and_then(|path| {
        open(&*path, OFlag::O_PATH | OFlag::O_CLOEXEC, Mode::empty())
            .ok()
            .map(|fd| (fd, path))
    });

    let dirfd = open(".", OFlag::O_RDONLY | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
    let (pipe_read, pipe_write) = pipe2(OFlag::O_CLOEXEC).unwrap();

    let epoll = Epoll::new(EpollCreateFlags::empty()).unwrap();
    let eventfd = EventFd::from_value_and_flags(0, EfdFlags::EFD_NONBLOCK).unwrap();
    let event = EpollEvent::new(EpollFlags::EPOLLIN, 0);
    epoll.add(eventfd.as_fd(), event).unwrap();
    let signalfd = open_signalfd();
    let timerfd = open_timerfd();
    let inotify_fd = open_inotify();
    unsafe {
        let watch_path = CString::new("/tmp").unwrap();
        if nix::libc::inotify_add_watch(inotify_fd, watch_path.as_ptr(), nix::libc::IN_CREATE) < 0 {
            panic!(
                "inotify_add_watch failed: {}",
                std::io::Error::last_os_error()
            );
        }
    }

    let tcp_listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let tcp_addr = tcp_listener.local_addr().unwrap();
    let _tcp_client = TcpStream::connect(tcp_addr).unwrap();
    let (_tcp_server_conn, _peer) = tcp_listener.accept().unwrap();

    let tcp6_listener = TcpListener::bind("[::1]:0").unwrap();
    let tcp6_addr = tcp6_listener.local_addr().unwrap();
    let _tcp6_client = TcpStream::connect(tcp6_addr).unwrap();
    let (_tcp6_server_conn, _peer6) = tcp6_listener.accept().unwrap();

    let _udp_socket = UdpSocket::bind("127.0.0.1:0").unwrap();
    let _udp6_socket = UdpSocket::bind("[::1]:0").unwrap();

    // Signal parent process (the test process) that this process is ready to be observed by the
    // ptool being tested.
    File::create(signal_path).unwrap();

    // Keep all descriptors alive until killed by test harness.
    let _keep_alive = (
        block_device,
        dirfd,
        pipe_read,
        pipe_write,
        epoll,
        eventfd,
        signalfd,
        timerfd,
        inotify_fd,
        tcp_listener,
        tcp6_listener,
    );

    loop {
        thread::sleep(Duration::from_millis(100));
    }
}