Security implications of exposed Go runtime anon pipes

115 views
Skip to first unread message

Moritz Sanft

unread,
Jan 9, 2025, 12:16:20 PM1/9/25
to golang-nuts
Hey there!

I've recently came across a Go application with an arbitrary file write vulnerability restricted to `/proc/self`. After researching for a little, I've found the following article which exploits such a vulnerability in a NodeJS application, escalating it into remote code execution by using anonymous pipes for control messages of the language runtime. [^1]

I wondered whether Go is susceptible to the same attacks, as it also utilizes anonymous pipes, and checked what is sent into the pipes by a benign exemplary Go application:

```
166301 epoll_create1(EPOLL_CLOEXEC <unfinished ...> 166301 <... epoll_create1 resumed>) = 3<anon_inode:[eventpoll]> 166301 epoll_ctl(3<anon_inode:[eventpoll]>, EPOLL_CTL_ADD, 4<pipe:[591683]>, {events=EPOLLIN, data={u32=11354728, u64=11354728}}) = 0 166307 epoll_pwait(3<anon_inode:[eventpoll]>, <unfinished ...> 166301 epoll_ctl(3<anon_inode:[eventpoll]>, EPOLL_CTL_ADD, 7</proc/sys/net/core/somaxconn>, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3260835688, u64=124595967125352}}) = 0 166301 epoll_ctl(3<anon_inode:[eventpoll]>, EPOLL_CTL_ADD, 6<socket:[591684]>, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=3260835688, u64=124595967125352}}) = 0
```
Values like `124595967125352 (0x7151c25c6768)` look like pointers, which generally look interesting depending on what the runtime does with them.

I quickly skimmed the source code to find the relevant handlers, but to no success.
Can anyone point me into the right direction here, or did someone even analyze the security of these anon pipes before?

Best,
Moritz

Ian Lance Taylor

unread,
Jan 9, 2025, 1:00:54 PM1/9/25
to Moritz Sanft, golang-nuts
Interesting vulnerability.

An arbitrary Go program can of course create anonymous pipes and use
them however it likes, which may possibly introduce a vulnerability in
some cases. I assume that your question is specifically about using
anonymous pipes in the Go runtime and standard library.

On macOS the Go runtime uses an anonymous pipe to wake up the thread
that manages the queue of incoming signals. The value sent on the pipe
is ignored so I don't see a security issue here (other than a possible
denial of service attack, but arbitrary file writes probably permit
that in other ways).

On NetBSD, OpenBSD, and AIX the Go runtime uses an anonymous pipe to
wake up the network poller when a newer timer is scheduled. Again the
value on the pipe is ignored.

I don't think there is any other use of anonymous pipes.

The pointers you show as the data of EPOLL_CTL_ADD, presumably on
Linux, are internal pointers that are passed into epoll and returned
from epoll. They are not sent on the network or received from the
network. They are used so that when some operation occurs on a file
descriptor the runtime poller knows what to do. These are not
anonymous pipes, and I don't see any vulnerability there.

So I don't see any security issue here for Go in general. Of course it
would be interesting to learn otherwise. If somebody sees a security
problem, please see https://go.dev/security for a safe way to report
it.

Ian

Moritz Sanft

unread,
Jan 9, 2025, 1:21:59 PM1/9/25
to golang-nuts
Hey Ian!

Thanks for your swift response. Let me clarify some things that I didn't seem to convey well in my initial question.

Probably the most important; I'm not looking to disclose a vulnerability in Go here. If I was to disclose that, I would use the points of contact you've mentioned.
I'm trying to understand the implications of an attacker being able to write into the inodes created by the Go runtime.

Also, I must apologize for the wrong usage of the term "anonymous pipe". I must have mixed it up with what's being used in the link I've mentioned while writing my question.
When I create a "Hello world" Go application, I see that it creates two anonymous inodes (not anonymous pipes) via epoll_create(2) and eventfd(2). These are, as you have mentioned correctly, what the pointers are being passed to in the example I've sent. Now, my question is, what can an attacker do if he gains write access to said inodes? Or, to phrase it more generally, what happens with the data that's being sent there?

You've mentioned the following:
> They are used so that when some operation occurs on a file descriptor the runtime poller knows what to do.

Can you point to a code location in the runtime I could look at to better understand what's going on there?

Thanks in advance!

Best,
Moritz

Ian Lance Taylor

unread,
Jan 9, 2025, 1:54:21 PM1/9/25
to Moritz Sanft, golang-nuts
On Thu, Jan 9, 2025 at 10:22 AM Moritz Sanft <gree...@gmail.com> wrote:
>
> Thanks for your swift response. Let me clarify some things that I didn't seem to convey well in my initial question.
>
> Probably the most important; I'm not looking to disclose a vulnerability in Go here. If I was to disclose that, I would use the points of contact you've mentioned.
> I'm trying to understand the implications of an attacker being able to write into the inodes created by the Go runtime.
>
> Also, I must apologize for the wrong usage of the term "anonymous pipe". I must have mixed it up with what's being used in the link I've mentioned while writing my question.
> When I create a "Hello world" Go application, I see that it creates two anonymous inodes (not anonymous pipes) via epoll_create(2) and eventfd(2). These are, as you have mentioned correctly, what the pointers are being passed to in the example I've sent. Now, my question is, what can an attacker do if he gains write access to said inodes? Or, to phrase it more generally, what happens with the data that's being sent there?

I see. I don't know what access Linux provides to an anonymous epoll
inode. I don't know what it would mean to write to an epoll
descriptor.

I also don't know what it would mean to have write access to an
eventfd, but that is used similarly to the anonymous pipes on other
systems. The runtime writes to the eventfd when it needs to wake up
the network poller due to a timer change. The actual data sent on the
eventfd is ignored.

The epoll descriptor is used as epoll normally is: to record a list of
descriptors of interest, and to track I/O events on those descriptors.


> Can you point to a code location in the runtime I could look at to better understand what's going on there?

See runtime/netpoll_epoll.go, which is the Linux-specific side of the
more general API in runtime/netpoll.go.

Ian

TheDiveO

unread,
Jan 9, 2025, 3:20:42 PM1/9/25
to golang-nuts
On Linux, you can interfere with any process via the process filesystem typically mounted at /proc in several creative ways, given you have access rights to a particular process. CAP_SYS_PTRACE isn't just for the ptrace syscall, but also for access inside /proc. The open file descriptors of Linux processes are exposed via /proc/$PID/fd/$FD as pseudo symlinks, further meta data about the fds then via /proc/$PID/fdinfo/$FD. This information is quite useful, for instance, for detecting "leaked fds" in Go unit tests, using the fdooze package. The way /proc works in Linux, you can even detect fd leakage of a separate process under test.

As the /proc/$PID/fd/$FD elements in the procfs are (pseudo) symlinks, you can readlink them to see what they're connecting to (with some caveats), as well as you can use them as path references in opening them. For instance,  CLI programs that cannot read from stdin, but always want a file name and don't understand "-", the usual way around is to specify /proc/self/fd/0. However, in this case you simply open a new fd using a resource's name referenced by an fd of the process.

But the Linux kernel since quite some time has pidfd_getfd(2) to clone an fd of a "victim" process into a new fd of your own "interested" process. For instance, this allows my lxkns discovery engine to pick up Linux kernel namespace references from the sockets and other open fds which processes currently have open, even if there is no other reference left in the system to a particular namespace.

Some elements are difficult to mess with: while you might know their inode number you simply cant access/open them knowing only the inode. For instance, you cannot access Linux kernel namespace by their inode numbers. Instead, you either need an already referencing fd or the special /proc/$PID/ns/* paths, that, then opened, reference the particular namespace.

Finally, Linux additionally uses "anonymous inodes" that don't have an associated inode, such as IIRC eventfd, epoll, and others.

Reply all
Reply to author
Forward
0 new messages