Skip to content

add utils for passing file descriptor between processes#4019

Open
echoechoin wants to merge 21 commits into
util-linux:masterfrom
echoechoin:feat/3683/fdsend
Open

add utils for passing file descriptor between processes#4019
echoechoin wants to merge 21 commits into
util-linux:masterfrom
echoechoin:feat/3683/fdsend

Conversation

@echoechoin
Copy link
Copy Markdown
Contributor

Based on the outcomes of Discussion #3683, I have developed this demo, with the relevant operation example as follows:

 ~/util-linux # ./build/fdrecv -f 0 test --run cat &
[1] 1200890
 ~/util-linux # echo "hello world" | ./build/fdsend -f 0 test
hello world
[1]  + 1200890 done       ./build/fdrecv -f 0 test --run cat

Comment thread misc-utils/fdsend-common.c Fixed
Comment thread misc-utils/fdsend-common.c Fixed
Comment thread misc-utils/fdsend-common.c Fixed
Comment thread misc-utils/fdsend-common.c Fixed
@echoechoin echoechoin changed the title [RFC] add utils for passing file descriptor between processes add utils for passing file descriptor between processes Feb 4, 2026
Comment thread .gitignore Outdated
Comment thread misc-utils/fdsend-common.c Outdated
}

/* get fd_to_send by open /proc/PID/fd/FD */
fd_to_send = open(proc_path, O_RDWR);
Copy link
Copy Markdown
Member

@masatake masatake Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

open(2) works for an fd opening a regular file.
For fds opening peculiar objects like sockets, we can use pidfd_getfd as a fallback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about using pidfd_getfd at first, then fall back to open if errno == ENOSYS || errno == EPERM.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantics of calling open(/proc/$pid/fd/$fd) and of calling pidfd_getfd($fd) differ.
open makes a new open file object in the kernel.
pidfd_getfd doesn't. pidfd_getfd makes a new reference to the open file.

If a receiver process calls lseek on a received file descriptor, it affects on the open file behind the fd.

open(2)

 [target process]<fd> ------ [file object] --- [dentry] --- [inode]
                                              /
 [receiver process]<fd> -----[file object] --/

pidfd_getfd(2)

 [target process]<fd> ------ [file object] --- [dentry] --- [inode]
                             /
 [receiver process]<fd> ----/

I would like to withdraw my comment.

I think users expect the behavior of open(2). So, using the open(2) method is better as the default method.

What about activating the pidfd_getfd(2) method only if --pidfd-getfd option is given?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I believe it is better to activate the pidfd_getfd(2) method only if --pidfd-getfd option is given. No fallback.

@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 4, 2026

It seems you need to add bash-completion files for your new commands.

Comment thread misc-utils/fdsend.c Outdated
Comment on lines +74 to +76
opt_pid = strtopid_or_err(optarg, _("invalid pid"));
if (opt_pid < 1)
errx(EXIT_FAILURE, _("pid must be positive"));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can also use this internal lib function. It would make way for a potential PID:inode support which could be interesting for fd{recv,send}, since it'll help avoid sending/receiving a fd from/to a wrong process (in cases where that is important of course).

Suggested change
opt_pid = strtopid_or_err(optarg, _("invalid pid"));
if (opt_pid < 1)
errx(EXIT_FAILURE, _("pid must be positive"));
opt_pid = ul_parse_pid_str_or_err(optarg, &opt_pid, NULL);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some context in regards to PID:inode: #3953 (comment)

@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 4, 2026

What about extending fdrecv to receive multiple fds?
We can add the implementation later, but we must design the CLI now in case.

@echoechoin
Copy link
Copy Markdown
Contributor Author

What about extending fdrecv to receive multiple fds? We can add the implementation later, but we must design the CLI now in case.

How about this:

# Send multiple fds:
fdsend --block --fd 0,1,2 -b pipe_test

# Receive multiple fds:
fdrecv --fd 0,1,2 pipe_test --run cat  # 0 <-> 0, 1 <-> 1, 2 <-> 2 (full mapping)
fdrecv --fd 0,1   pipe_test --run cat  # 0 <-> 0, 1 <-> 1 (2 is ignored)
fdrecv --fd 0     pipe_test --run cat  # 0 <-> 0 (1 and 2 are ignored)

Also ensure that --fd, --stdin, --stdout, and --stderr are mutually exclusive,
and --stdin, --stdout, and --stderr accepting only the first single fd.

# Send multiple fds:
fdsend --block --fd 0,1,2 -b pipe_test

# Receive only first fd via --stdin, --stdout, or --stderr:
fdrecv --stdin pipe_test --run cat # 0 <-> stdin, ignore other fds.

@echoechoin echoechoin marked this pull request as ready for review February 6, 2026 03:08
@echoechoin echoechoin force-pushed the feat/3683/fdsend branch 3 times, most recently from 4f0da87 to 215f250 Compare February 6, 2026 06:38
@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 7, 2026

What about supporting an abstract Unix socket (https://man7.org/linux/man-pages/man7/unix.7.html)?
It may help users pass a file descriptor between processes in different mount namespaces.

@echoechoin
Copy link
Copy Markdown
Contributor Author

What about supporting an abstract Unix socket (https://man7.org/linux/man-pages/man7/unix.7.html)? It may help users pass a file descriptor between processes in different mount namespaces.

How about add a new command option for abstract unix socket?

fdsend --abstract --fd 1 sockspec
fdrecv --abstract --fd 1 sockspec run cat

or add an @ sign before sockspec to indicate it is a abstract unix socket:

fdsend --fd 1 @sockspec
fdrecv --fd 1 @sockspec run cat

@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 9, 2026

Adding --abstract looks better for me.

Rationals:

--abstract option requires more type than @socket. It implies that passing an fd via an abstract socket requires care.

For abstract sockets, we cannot use inotify to detect the appearance of a listening socket. So the usability of these commands with --abstract differs from the commands without the option.

--abstract option allows users passing an fd via /run/user/$UID/fdsend/@fname :-)

@echoechoin
Copy link
Copy Markdown
Contributor Author

In abstract and blocking mode, may be we can retry on ECONNREFUSED at intervals (like 500ms) until connect succeed, then send the file descriptor. Or other approaches?

@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 9, 2026

I don't have any other idea than the polling approach.
For implementing private tools, watching the unix_bind() invocation with BPF can be a choice. However, I think it is not suitable for a tool in util-linux.

* Receiver: socket/bind/listen/accept, recvmsg with SCM_RIGHTS.
* On success sets *out_fd to the received fd and returns 0.
*/
static int fdrecv_accept_and_recv_fd(const char *sockpath, int *out_fd, int abstract)

Check warning

Code scanning / CodeQL

Poorly documented large function Warning

Poorly documented function: fewer than 2% comments for a function of 114 lines.
Comment thread bash-completion/fdrecv
Comment thread misc-utils/fdsend.c Outdated
@echoechoin echoechoin force-pushed the feat/3683/fdsend branch 2 times, most recently from f865d19 to 993f975 Compare February 10, 2026 06:14
@echoechoin echoechoin requested a review from cgoesche February 10, 2026 06:29
Comment thread misc-utils/fdrecv.c Outdated
FILE *out = stdout;

fputs(USAGE_HEADER, out);
fprintf(out, _(" %s [options] SOCKSPEC --run command args...\n"), program_invocation_short_name);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find that --run is a bit unnecessary. Since SOCKSPEC can not be specified an arbitrary amount of time, we can simply design the CLI like this:

fdrecv [options] SOCKSPEC COMMAND

COMMAND will have an arbitrary amount of arguments, which is fine as these will be the remaining elements of argv and we'll just pass them to execvp(2).

What do you think ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#4019 (comment)

If we extend fdrecv to accept fds from multiple sockets, the fdcecv command line can become complicated. In these cases, the --run option helps people understand the command line.

If the action that fdrecv takes after receiving all fds is only "run", we can choose -- (or -) instead of --run.

Comment thread misc-utils/fdrecv.c Outdated
Comment thread misc-utils/fdrecv.c Outdated
Comment thread misc-utils/fdrecv.c Outdated
Comment thread misc-utils/fdrecv.c Outdated
Comment thread misc-utils/fdrecv.c Outdated
Comment thread misc-utils/fdsend.c Outdated
Comment thread misc-utils/fdsend.c Outdated
Comment thread misc-utils/fdsend.c Outdated
Comment thread misc-utils/fdsend-common.c Outdated
Comment thread misc-utils/fdsend-common.c Outdated
@masatake
Copy link
Copy Markdown
Member

It would be nice if fdrecv could receive multiple fds from different sockets at once.
This is just an idea.

terminal A

fdsend --fd 0 sockA

terminal B

fdsend --pid 9999 --fd 1 sockB

terminal C

fdsend --pid 9999 --fd 32 sockC

trminal recv:

fdrecv -f 3 sockA -f 2 sockB -f 0 sockC --run CMD

@masatake
Copy link
Copy Markdown
Member

masatake commented Feb 17, 2026

This pull request sparks many ideas.

I find an alternative action --forward.
If --forward another-socket is given, fdrecv sends the received fds to another fdrecv instead of running the command specified with --run
So I think it is better to name the option verbs: --run or --forward.


(Added after submitting this comment)

Instead of adding --forward as a new action to fdrecv, we can implement fdfwd command specialized to fd forwarding. In this case, the option doesn't need a verb.

@echoechoin echoechoin force-pushed the feat/3683/fdsend branch 2 times, most recently from 2eb8f68 to 3d9f5c2 Compare February 27, 2026 03:01
@echoechoin
Copy link
Copy Markdown
Contributor Author

echoechoin commented Feb 27, 2026

  • Support receive multiple fds from different sockets at once.

@karelzak
Copy link
Copy Markdown
Collaborator

Please note that I am currently focusing on stabilizing the new release. I will work on this PR later.

@karelzak
Copy link
Copy Markdown
Collaborator

Review of the final state

Overall the idea is solid and the SCM_RIGHTS handling is correct. A few things
need attention before this can be merged.

Critical

Compilation error — ul_parse_pid_str_or_err called with wrong arity (fdsend.c)

ul_parse_pid_str_or_err(optarg, &opts.pid, NULL);

The actual signature in include/pidutils.h is:

extern void ul_parse_pid_str_or_err(char *pidstr, pid_t *pid_num, uint64_t *pfd_ino, int flags);

Missing the 4th flags argument. Should be:

ul_parse_pid_str_or_err(optarg, &opts.pid, NULL, 0);

fdrecv must use getopt_long()

The hand-rolled strcmp() option parser in fdrecv.c is a significant deviation
from every other util-linux tool. It's fragile, hard to extend, and it's already
caused fdrecv to be added to unsupported_programs in tools/get-options.sh
(which is a red flag).

The multi-socket syntax (-f N SOCKSPEC)... --run CMD is solvable with
getopt_long(). You can loop: call getopt_long(), when you hit -f grab
its required_argument, then consume argv[optind] as the SOCKSPEC (advancing
optind manually). --run becomes the sentinel that stops the option loop
and everything after it is the command — same pattern as env or chroot use.

This matters for maintainability: every new flag will require hand-coding all
interactions, -- handling is missing entirely, and is_option() misidentifies
filenames starting with - as options.


Security / Correctness

  1. Socket directory created with 0755 (fdsend-common.c, ul_mkdir_p(dir, 0755)
    for /run/user/UID/fdsend/) — lets other users enumerate socket names.
    Should be 0700 for per-user directories.

  2. Abstract sockets have no access control — path-based sockets get
    chmod(path, 0600), but abstract sockets have no filesystem-level
    permissions. Any process in the same network namespace can connect and
    send an fd to fdrecv. Worth documenting in the man page at minimum;
    consider SO_PEERCRED verification.

  3. Abstract socket --blocking is an infinite busy-loop (fdsend-common.c) —
    retries connect() every 100ms forever, no timeout, no backoff, no max
    attempts. If the receiver never starts, the only exit is SIGINT. The man page
    for --blocking doesn't mention this differs from path-based sockets
    (which use inotify).

  4. fdsend_wait_for_socket() initial timeout logicpoll_timeout_ms
    starts at 2000, then switches to -1 (infinite). The 2-second window seems
    arbitrary; inotify + infinite poll is sufficient from the start.


Code quality nits

  • #include <err.h> in fdsend.c is unnecessary — c.h already provides
    err()/warn(). fdrecv.c doesn't include it, so this is inconsistent.

  • strncpy(path, spec, size) in sockpath_from_spec() — prefer
    snprintf(path, size, "%s", spec) or xstrncpy() which are more idiomatic
    for util-linux.

  • -a/--abstract positioning logic in fdrecv.c is subtle — the condition
    sockspec_idx < n_pairs (meaning "current pair doesn't have a sockspec yet")
    is non-obvious and deserves a comment.

  • Signal handlers are set up per-socket inside fdrecv_accept_and_recv_fd()
    rather than once in main(). They're idempotent so it works, but it's
    cleaner to initialize once.


Man pages

  • Both man pages lack an EXAMPLES section. For tools with non-trivial CLI
    syntax (especially fdrecv's multi-socket mode), concrete examples are
    essential. The PR description already has a good one — move it into the
    man pages.

  • --blocking description says "Wait for the socket to appear before
    connecting" — this is only true for path-based sockets. For abstract sockets,
    it's a connect-retry loop. Users need to know.


Tests

  • sleep 1 for synchronization between fdrecv and fdsend is race-prone on
    slow CI. Consider increasing or using a wait loop.

  • No negative test cases (duplicate target fds, missing sockspec, invalid fd,
    abstract socket mode, --pidfd-getfd mode).

@karelzak karelzak added the TODO We going to think about it ;-) label May 26, 2026
echoechoin added 21 commits June 3, 2026 15:32
This commit implements the fdsend and fdrecv utilities as proposed in
Discussion util-linux#3683. It adds:

- fdsend: send a file descriptor to a Unix domain socket (path or
  abstract). Supports blocking wait for receiver, optional pid/fd
  resolution, and pidfd_getfd(2) where available.
- fdrecv: receive a file descriptor from a Unix domain socket and
  optionally exec a program with the received fd.

Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Allow using pidfd_getfd(2) instead of open(/proc/PID/fd/FD) to obtain
the file descriptor from the process.

Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
…usage message for clarity

Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
- Support multiple -f N SOCKSPEC (and -i/-o/-e SOCKSPEC) before --run CMD.
- Add `relocate_conflicting_fds` to avoid target FD and received FD conflicts.
- Add fdrecv doc update and tests.

Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Signed-off-by: WanBingjiang <wanbingjiang@webray.com.cn>
Comment thread misc-utils/fdrecv.c
* If recv_fds[j] equals pairs[i].target_fd for some i != j and
* recv_fds[j] != pairs[j].target_fd, dup it to a fd number above
* all target fds so it cannot collide with any target.
*/

Check notice

Code scanning / CodeQL

For loop variable changed in body Note

Loop counters should not be modified in the body of the
loop
.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TODO We going to think about it ;-)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants