Bug 18203

Summary: realpath() does not handle unreachable paths correctly
Product: glibc Reporter: Jann Horn <jannhorn>
Component: libcAssignee: Dmitry V. Levin <ldv>
Status: RESOLVED FIXED    
Severity: normal CC: drepper.fsp, fweimer, jeremip11
Priority: P2 Flags: fweimer: security-
Version: 2.19   
Target Milestone: 2.27   
Host: Target:
Build: Last reconfirmed:
Bug Depends on: 22679    
Bug Blocks:    

Description Jann Horn 2015-04-05 16:05:32 UTC
On Linux, getcwd(2) can succeed without returning an absolute path. I just documented that in a man-pages patch I sent to the man-pages ML and maintainer, a copy is at <http://var.thejh.net/0001-getcwd.3-behavior-for-unreachable-cwd.patch>. This behavior causes realpath to behave inconsistently when dealing with unreachable paths:

$ cat getcwd.c
#define _GNU_SOURCE
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <limits.h>
#include <stdlib.h>

int main(void) {
  unshare(CLONE_NEWUSER | CLONE_NEWNS);
  chdir("/usr");
  chroot("bin");

  printf("current directory: \"%s\"\n", get_current_dir_name());

  char *real = realpath(".", NULL);
  printf("realpath of .: \"%s\"\n", real ? real : "{none}");
  real = realpath("../home/jann/.ssh", NULL);
  printf("realpath of path: \"%s\"\n", real ? real : "{none}");

  return 0;
}
$ gcc -o getcwd getcwd.c 
$ ./getcwd
current directory: "(unreachable)/usr"
realpath of .: "(unreachable)/usr"
realpath of path: "{none}"
$ strace ./getcwd
[...]
unshare(CLONE_NEWNS|CLONE_NEWUSER)      = 0
chdir("/usr")                           = 0
chroot("bin")                           = 0
stat(".", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/media/wdgreen/home/jann/tmp", 0x7fff685a5af0) = -1 ENOENT (No such file or directory)
brk(0)                                  = 0x7c6000
brk(0x7e8000)                           = 0x7e8000
getcwd("(unreachable)/usr", 4096)       = 18
brk(0x7e7000)                           = 0x7e7000
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 4), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f58a5941000
write(1, "current directory: \"(unreachable"..., 39current directory: "(unreachable)/usr"
) = 39
getcwd("(unreachable)/usr", 4096)       = 18
write(1, "realpath of .: \"(unreachable)/us"..., 35realpath of .: "(unreachable)/usr"
) = 35
getcwd("(unreachable)/usr", 4096)       = 18
lstat("(unreachable)/home", 0x7fff685a5ac0) = -1 ENOENT (No such file or directory)
write(1, "realpath of path: \"{none}\"\n", 27realpath of path: "{none}"
) = 27
exit_group(0)                           = ?
+++ exited with 0 +++

As you can see, realpath attempts to lstat() the unreachable path returned by getcwd() in the last invocation. I think the sanest option to deal with this would be to let realpath() error out if getcwd() returns a path that does not start with a slash, but I'm not sure about which errno value to use. Maybe ENOENT, with the reasoning "if it's not under the root, it doesn't really exist to us"?