Bug 14749 - Dangerous race condition with vfork in posix_spawn
Summary: Dangerous race condition with vfork in posix_spawn
Status: NEW
Alias: None
Product: glibc
Classification: Unclassified
Component: libc (show other bugs)
Version: unspecified
: P2 normal
Target Milestone: ---
Assignee: Not yet assigned to anyone
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2012-10-21 16:58 UTC by Rich Felker
Modified: 2018-03-07 09:53 UTC (History)
5 users (show)

See Also:
Host:
Target:
Build:
Last reconfirmed:


Attachments

Note You need to log in before you can comment on or make changes to this bug.
Description Rich Felker 2012-10-21 16:58:05 UTC
posix_spawn uses vfork (to avoid momentary doubling commit charge and improve performance) in cases where it seems "safe", or when explictly requested. However, at least one major race condition seems to have been missed:

Consider a program running with elevated privileges (perhaps a daemon or suid program which initially has root) which is multi-threaded, and which will drop privileged and then execute untrusted code (perhaps a user-provided script or module). The scenario looks like:

Thread A is calling posix_spawn to run a fixed external command (call it child C) that will work fine, and which is safe to invoke, with either the initial privileges or the reduced privileges. Think of something stupid like running "date" to get the current date and time.

Thread B is calling setuid() to drop privileges, then executing untrusted code.

And let's suppose events happen in the following order:

A: vfork
C: vfork returns in child
B: setuid
B: untrusted code runs and pokes at memory A is using
C: now running arbitrary code as root
C: ...
A: vfork returns in parent

Fundamentally, the danger of this race is the possibility of it giving rise to two threads/processes sharing an address space, but with different privileges; this kind of situation must never be allowed to arise.

The simplest way to avoid the race is by using fork instead of vfork, unless vfork is specifically requested. However, that brings back the double-commit-charge issue. An alternative fix is to hold a lock that prevents changing uids/gids during the vfork window. This is also easy since NPTL is already doing a global lock for set*id() to synchronize the id changes across all the threads (since Linux requires each thread to make its own set*id() syscall).
Comment 1 Carlos O'Donell 2014-09-20 04:13:08 UTC
I agree this should be fixed. I think posix_spawn and setXid functions should serialize against eachother. Note that vfork should not serialize against setXid functions becuase there I think users need to be clever enough to know what they are doing, but we should still provide better documentation and example code.
Comment 2 Daniel Drake 2018-03-06 22:55:25 UTC
Thanks for the work done on identifying vfork issues like these and greatly improving posix_spawn() in recent years.

Took me a little while to get my head around this, so here is a comment that may help others reading, and may be useful when writing those docs.

> Fundamentally, the danger of this race is the possibility of it giving rise
> to two threads/processes sharing an address space, but with different
> privileges; this kind of situation must never be allowed to arise.

The Linux kernel setuid system call explicitly allows this undesirable situation to happen (credentials can be changed on a per-thread basis), so my first thought was that "this is not a vfork-specific problem, can't unprivileged thread B already directly attack privileged thread A?" (in the above example)

But the dominant glibc threading implementation indeed goes to some lengths to avoid this. The nptl man page explains that a glibc setuid() call will cause the credentials to be changed over all threads in synchronized fashion. So this situation *is* ordinarily avoided when you use the standard threading API.

The water only gets muddy when you throw some other thread/process API calls into the mix - like vfork(), clone(), or posix_spawn() - all of which can create thread-like entities in an inopportune moment that will escape the attempt to drop privileges throughout the whole process.

With that understood, it seems clear that a higher level API like posix_spawn() should have assurances to avoid this race.
Comment 3 Florian Weimer 2018-03-07 09:53:19 UTC
(In reply to Rich Felker from comment #0)
> Fundamentally, the danger of this race is the possibility of it giving rise
> to two threads/processes sharing an address space, but with different
> privileges; this kind of situation must never be allowed to arise.

Why?  The kernel does this, too, and user-space file servers really want this functionality as well.