WLBF / single-instance

A rust library for single instance application.
MIT License
34 stars 8 forks source link

fix: Set SOCK_CLOEXEC on Linux to prevent socket from being passed down to children. #15

Closed jwalton closed 1 month ago

jwalton commented 10 months ago

This fixes a bug where forking and running a command from a Rust program could cause single-instance to mistakenly think a copy of the app is running when it isn't, when running on Linux. We can recreate this problem with a very simple example:

$ cargo init single-instance-problem
$ cd single-instance-problem
$ cargo add single-instance

And then in our main.rs:

use std::process::Command;

use single_instance::SingleInstance;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let instance = SingleInstance::new("my-cool-app")?;
    if !instance.is_single() {
        eprintln!("Instance already running!");
    } else {
        println!("Forking...");
        Command::new("sleep").arg("6000").spawn()?.wait()?;
    }

    Ok(())
}

Let's build and run this:

$ cargo build
$ ./target/debug/single-instance-problem
Forking...

At this point, if you CTRL-C, then everything works exactly as you expect it would - you can restart single-instance-problem and it will print out "Forking..." again. But, if we run single-instance-problem, then open a second terminal and run:

$ ps -ef | grep single-instance-problem
ubuntu    568517  564675  0 21:16 pts/4    00:00:00 ./target/debug/single-instance-problem
$ kill -9 568517

Then we go back to our first window:

$ ./target/debug/single-instance-problem
Instance already running!

Ohs noes! We can use lsof to see who has the abstract socket still open:

$ lsof | grep my-cool-app
sleep     568518                           ubuntu    3u     unix 0x0000000000000000      0t0    4218288 @my-cool-app type=STREAM
$ ps -ef | grep 568518
ubuntu    568518       1  0 21:16 pts/4    00:00:00 sleep 6000

Yup! It's the sleep 6000 we spawned in our Rust app.

To understand what's going on here (you probably know a lot of this already, but just in case...) when our app does Command::new(...).spawn(), what's actually happening in Linux is that we're doing a fork and exec. When we fork in Linux, as the man page says:

The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent.

This means the sleep 6000 gets a copy of the open file descriptor for our abstract socket, which we probably don't want. The fix here is to set SOCK_CLOEXEC on the abstract socket. To quote from the socket man page:

SOCK_CLOEXEC - Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.

What this means is that when we run our child command, we'll still fork and get a copy of the file descriptor, but when the child execs the "sleep 6000" this file descriptor will automatically be closed. The parent process will still have it open, so we'll still get our "single instance" behavior, but if someone kill -9s our single instance, we can recreate it immediately instead of having to wait for all the execed child processes to die.