The vulnerability described here is caused by Linux kernel behaviour change in the syscall API (returning relative pathnames in getcwd()) and non-defensive function implementation in libc (failing to process that pathname correctly). Other libraries are very likely to be affected as well. On affected systems this vulnerability can be used to gain root privileges via SUID binaries.

The return value specification change in getcwd() was introduced in Linux kernel Linux 2.6.36. It has already caused troubles, even in realpath(), but at different location (see bug report) and was not identified as security issue.

Linux kernel side:

One of the weaknesses of Linux kernel is, that it is not fully POSIX compliant (see Wikipedia POSIX). To allow programmers to produce clean and secure code, meticulous documentation would be needed, especially to write cross-platform software. Changes in specification and documentation after software was already written always pose an extra risk. This is also true for commit vfs: show unreachable paths in getcwd and proc changing the behaviour of getcwd(). The new specification made it finally to the manpages (see getcwd(2)), but at that time glibc was already written. From the somehow contradictory man page:

These functions return a null-terminated string containing an _absolute_ pathname that is the current working directory of the calling process. The pathname is returned as the function result and via the argument buf, if present.

If the current directory is not below the root directory of the current process (e.g., because the process set a new filesystem root using chroot(2) without changing its current directory into the new root), then, since Linux 2.6.36, the returned path will be prefixed with the string "(unreachable)". Such behavior can also be caused by an unprivileged user by changing the current directory into another mount namespace. When dealing with paths from untrusted sources, callers of these functions should consider checking whether the returned path starts with '/' or '(' to avoid misinterpreting an unreachable path as a relative path....

...getcwd() conforms to POSIX.1-2001. Note however that POSIX.1-2001 leaves the behavior of getcwd() unspecified if buf is NULL.

The documentation is accurate regarding use of (unreachable) but most likely not according POSIX compliance. At least POSIX 2004 and 2008 are violated, 2001 version of standard seems not available for free. According to IEEE Std 1003.1-2008 specification of getcwd():

The getcwd() function shall place an absolute pathname of the current working directory in the array pointed to by buf, and return buf. The pathname shall contain no components that are dot or dot-dot, or are symbolic links.

As it seems, that consequences from the change of interface specification on Linux kernel side only were not recognized by all affected parties. The realpath() function, which relies on using getcwd() to resolve relative path names still required the old behaviour. Also the manpage does not reflect the changes in underlying getcwd() call, see realpath(3).

Libc side:

glibc still assumes that kernel getcwd() would return absolute pathnames and relies on that behaviour when realpath() attempts to create a canonicalized absolute pathname:

realpath() expands all symbolic links and resolves references to /./, /../ and extra '/' characters in the null-terminated string named by path to produce a canonicalized absolute pathname...

When resolving a relative symbolic link, e.g. ../../x, realpath() will use the current working directory, assuming it will start with a /. The function starts at the end of the getcwd pathname to jump forward from slash to slash for each ../ found in the symbolic link to resolve. It does not check the boundaries of the buffer, thus may end up at a slash before the string buffer used to create the canonicalized absolute pathname. So resolving the link named above with getcwd() returning (unreachable)/, the second ../ will have moved the pointer before the buffer, the next part x is then copied to this memory location. As realpath usually operates on heap buffers.