Skip to content
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

Socket gets unlinked when children closes the socket file descriptor #16

Closed
aszlig opened this issue May 29, 2020 · 12 comments · Fixed by #23
Closed

Socket gets unlinked when children closes the socket file descriptor #16

aszlig opened this issue May 29, 2020 · 12 comments · Fixed by #23
Labels
bug Something isn't working
Milestone

Comments

@aszlig
Copy link
Contributor

aszlig commented May 29, 2020

strace output provided by @riedel in #13 (comment):

https://gist.github.com/riedel/043936e3c80da0f2607d48d8205d63e0#file-rsession-strace-L3960

My answer:

Okay, so the interesting part here is that it happens prior to executing /bin/sh -c 'git "--version"', where all file descriptors are closed. However, since after the clone the file descriptor is still active in the parent, we should not unlink the socket file.

To fix this, we need to implement some kind of reference counting... oh geesh...

Originally posted by @aszlig in #13 (comment)

@aszlig
Copy link
Contributor Author

aszlig commented Jun 1, 2020

Some more thoughts on this:

The naive way to solve this would be to just wrap clone/clone3 (except if using CLONE_FILES or CLONE_VM), daemon and fork/vfork and make sure the the socket file descriptors controlled by ip2unix aren't unlinked when close is called in the child process.

This will probably work with the rsession case mentioned/reported above, since the clone call is followed up by execve (after closing the file descriptors ranging from 3 to 4095) and thus the socket is not really being used in the child process.

However, this falls short as soon as the child process is actively using the socket and it's closed in the parent, for example (in pseudocode):

fd = socket(...);
pid = fork();
if (pid == 0) { // children
    accept(fd, ...);
    ...
}
// parent
close(fd);
waitpid(pid, ...);

In this case, the socket would be unlinked in the parent way too early and would leave users baffled on why there is no socket file. Note that unlinking the socket doesn't cause any errors in the socket interface and it's a bit similar to our own blackhole rule action.

We could get around this by having a side-channel which keeps track of the processes using a socket controlled by us. One very error-prone, non-portable and racy way would be by checking /proc/PID/net/unix of the parent and/or other processes in the same pgroup. Another way would be to fork off a dedicated ip2unix process that's solely responsible for keeping track of socket file descriptors.

Both however doesn't sound like the right solution here and I'd rather have a more simple solution where we even wouldn't need to wrap fork or clone.

Another way would be to not unlink socket files at all or even add an option to allow the user to control the behaviour. Unfortunately, especially the former would constitute a backwards-incompatible change and I personally would also prefer not to have countless of old socket files laying around.

@aszlig aszlig modified the milestones: 2.1.3, 2.1.4 Jun 1, 2020
@riedel
Copy link

riedel commented Jun 1, 2020

I was trying to reproduce the bug reliably on centos:7 with glibc-2.17-307.el7.1.x86_64 using docker. One really needs to install git for the problem to appear (so it gets executed with all the sockets closed)

FROM centos:7
RUN yum install -y epel-release centos-release-scl
RUN yum install -y meson yaml-cpp-devel python3-pytest R devtoolset-7-gcc-c++ git
RUN yum install -y https://download2.rstudio.org/server/centos6/x86_64/rstudio-server-rhel-1.3.959-x86_64.rpm
COPY . /ip2unix
WORKDIR /ip2unix
RUN scl enable devtoolset-7 'meson build && ninja -C build && ninja -C build install'
#RUN ninja -C build test
RUN adduser testuser
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/lib64
USER testuser
RUN R_HOME=/usr/lib64/R  R_SHARE_DIR=/usr/share/R R_INCLUDE_DIR=/usr/include/R R_DOC_DIR=/usr/share/doc/R-3.6.0 RSTUDIO_DEFAULT_R_VERSION_HOME=/usr/lib64/R  RSTUDIO_DEFAULT_R_VERSION=3.6.0 timeout 10 ip2unix -r path=/tmp/rsession.sock /usr/lib/rstudio-server/bin/rsession --standalone=1 --program-mode=server --log-stderr=1 --www-address=127.0.0.1 --www-port=12345 & sleep 3 && curl --unix-socket /tmp/rsession.sock http://127.0.0.1:12345

@aszlig
Copy link
Contributor Author

aszlig commented Jun 2, 2020

@riedel: Just to make sure my assumptions are correct:

If you uncomment the following line:

this->unlink_sockpath = newpath;

... does it work?

@riedel
Copy link

riedel commented Jun 2, 2020

... does it work?
yes!

@aszlig aszlig modified the milestones: 2.1.4, 2.2.0 Jul 8, 2020
@aszlig aszlig added the bug Something isn't working label Jul 10, 2020
@aszlig
Copy link
Contributor Author

aszlig commented Jul 13, 2020

Comment moved to #23.

aszlig added a commit that referenced this issue Jul 13, 2020
While I haven't yet figured out how to properly solve this issue[1],
let's at least provide a test case so that we can start experimenting
with different solutions and quickly see whether it addresses the main
issue or whether we need to get back to the drawing table.

[1]: #16

Signed-off-by: aszlig <aszlig@nix.build>
@aszlig aszlig linked a pull request Jul 13, 2020 that will close this issue
@ghost
Copy link

ghost commented Nov 22, 2021

... does it work?
yes!

Hi, I can report that I got bitten by this bug and the fix above works for me too. Please please consider adding a command-line switch to enable this behavior for users who need it! Leaking socket inodes is better than not working at all.

Specifically, firefox has a low-level debugger protocol called "marionette" which only knows how to listen on TCP sockets. This is a lower-level and more powerful protocol than "chrome devtools" or "webdriver" (in fact, those are implemented using marionette). I don't want to leave such a powerful debugger interface to every random process on the same machine as the browser (including, potentially, website javascripts running inside the browser itself!) and ip2unix does a great job at that -- but it needs this patch. Details: https://bugzilla.mozilla.org/show_bug.cgi?id=1525126#c71

@aszlig
Copy link
Contributor Author

aszlig commented Nov 22, 2021

Please please consider adding a command-line switch to enable this behavior for users who need it! Leaking socket inodes is better than not working at all.

The issue is that adding a command line switch doesn't necessarily make the problem go away, because people do not necessarily know that they need such a switch, so I think we need to have a better interim solution.

One that I could think of would be to unlink the socket file during bind instead of close, which would avoid some shady command line flag that we need to deprecate in version 3. We already do this whenever SO_REUSEADDR is set for the socket.

Long-term however I think we need to switch to fork+execve+wait instead of just execve right now, which we need to do anyway if we want to use seccomp BPF. This model would leave ip2unix running for the whole runtime of the program in question but the above issue would not apply because we can also track child processes.

@ghost

This comment was marked as off-topic.

@aszlig

This comment was marked as off-topic.

aszlig added a commit that referenced this issue Nov 25, 2021
This is something that came up during a discussion[1] on how to solve
our current socket cleanup issue:

> Here is the case you can't handle: a server bind()s a socket, then
> fork()s, and then accept()s in *both* the parent and child process
> after the fork(). This is legitimate, and I'm sure somebody somewhere
> is using it to get a crude form of multiprocess load-balancing.

My point was that it doesn't matter in our case, but to make sure that
I'm not missing something, I decided to add a test case to see whether
my point is valid.

[1]: #16 (comment)

Signed-off-by: aszlig <aszlig@nix.build>
@ghost

This comment was marked as off-topic.

@aszlig

This comment was marked as off-topic.

@ghost
Copy link

ghost commented Aug 16, 2023

Please please consider adding a command-line switch to enable this behavior for users who need it!

The non-solution I went with is basically adding a new flag called "noremove", which prevents the socket path from being unlinked when the socket is closed.

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants