8000 Apply FD_CLOEXEC on sockets by lennyly · Pull Request #1301 · redis/hiredis · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Apply FD_CLOEXEC on sockets #1301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 25, 2025

Conversation

lennyly
Copy link
Contributor
@lennyly lennyly commented May 22, 2025

This PR sets the FD_CLOEXEC flag on all sockets created by hiredis to prevent file descriptor leakage across fork() and exec() boundaries.

When a program using hiredis forks, the file descriptors for sockets are inherited by the child process unless FD_CLOEXEC is set. This can lead to hard to diagnose issues, especially in environments that use epoll or changelist-based polling optimizations.

In one such case, the parent process initiated a reconnection, which reused the same file descriptor. However because the original socket object was still "alive" due to being duplicated in the child process, epoll continued to monitor the old underlying file object associated with the same file descriptor. Since epoll watches file objects, not just file descriptors, this caused the epoll instance to incorrectly associate events with the wrong socket.

Under a changelist optimization, this is particularly problematic because epoll_ctl() calls can be skipped, and the internal epoll state may remain stale. As a result, the event loop may enter a tight loop, leading to 100% CPU usage due to busy looping.

By marking sockets with FD_CLOEXEC, we ensure that the file descriptor is automatically closed in any exec()'d child process, allowing the file object to be fully cleaned up and avoiding the above issue.

@michael-grunder
Copy link
Collaborator
michael-grunder commented May 22, 2025

This seems like a nice improvement. I guess the only question is whether or not it should be set unconditionally or optionally like TCP_KEEPALIVE.

I'm pretty sure FD_CLOEXEC is basically everywhere but I'm thinking of exotic systems or very niche use cases. Hiredis is absolutely everywhere so is there an edge case where someone would not want this flag set?

edit: Thinking about this a bit more, I can see use cases where we would not want this flag set, so personally I think we should put it behind an option.

@lennyly
Copy link
Contributor Author
lennyly commented May 22, 2025

To support all use cases I've added an options flag, and also switched to SOCK_CLOEXEC so that the option will be set atomically in socket() instead of later on, thus avoiding the risk of having another thread fork in between socket() and fcntl().

@collinfunk
Copy link
Contributor

To support all use cases I've added an options flag, and also switched to SOCK_CLOEXEC so that the option will be set atomically in socket() instead of later on, thus avoiding the risk of having another thread fork in between socket() and fcntl().

Yes, that is much better. And they are hidden behind #ifdef SOCK_CLOEXEC. Thanks!

One thing. This:

#ifdef SOCK_CLOEXEC
    int flags = SOCK_STREAM;
    if (c->flags & REDIS_OPT_SET_SOCK_CLOEXEC) {
        flags |= SOCK_CLOEXEC;
    }
    if ((s = socket(type, flags, 0)) == REDIS_INVALID_FD) {
        __redisSetErrorFromErrno(c, REDIS_ERR_IO, NULL);
        return REDIS_ERR;
    }
#else
    if ((s = socket(type, SOCK_STREAM, 0)) == REDIS_INVALID_FD) {
        __redisSetErrorFromErrno(c,REDIS_ERR_IO,NULL);
        __redisSetErrorFromErrno(c, REDIS_ERR_IO, NULL);
        return REDIS_ERR;
    }
#endif /* SOCK_CLOEXEC */

Can be made shorter by doing:

   int flags = SOCK_STREAM;
#ifdef SOCK_CLOEXEC
    if (c->flags & REDIS_OPT_SET_SOCK_CLOEXEC) {
        flags |= SOCK_CLOEXEC;
    }
#endif /* SOCK_CLOEXEC */
    if ((s = socket(type, flags, 0)) == REDIS_INVALID_FD) {
        __redisSetErrorFromErrno(c, REDIS_ERR_IO, NULL);
        return REDIS_ERR;
    }

But will let @michael-grunder what he thinks.

@michael-grunder
Copy link
Collaborator

Yeah I think that shorter snippit is good. Less duplication.

@lennyly
Copy link
Contributor Author
lennyly commented May 23, 2025

Good feedback, thanks!

Copy link
Collaborator
@michael-grunder michael-grunder left a comment

Choose a reason for hiding this comment

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

Just those two minor changes and then it looks good to me.

lennyly and others added 2 commits May 23, 2025 20:25
Co-authored-by: Michael Grunder <michael.grunder@gmail.com>
Co-authored-by: Michael Grunder <michael.grunder@gmail.com>
@lennyly lennyly requested a review from michael-grunder May 23, 2025 17:26
@collinfunk
Copy link
Contributor

Looks good to me, thanks!

@lennyly
Copy link
Contributor Author
lennyly commented May 24, 2025

@michael-grunder do you have any additional comments? The PR currently requires your approval.

Copy link
Collaborator
@michael-grunder michael-grunder left a comment

Choose a reason for hiding this comment

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

LGTM.

Gave it a local spin and it works as expected. Thanks!

@michael-grunder michael-grunder merged commit 2947820 into redis:master May 25, 2025
15 checks passed
7360
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0