unix: support Faccessat flags argument

The Linux kernel faccessat system call does not take a flags parameter.
The flag parameter to the C library faccessat function is implemented in C.
The unix.Faccessat function takes a flags parameter. In older releases
we have passed the flags parameter to the kernel, which ignored it.
In CL 119495 we started returning an error if any flags were set.
That seems clearly better than ignoring them, but it turns out that some
code was using the flags. The code was previously subtly broken.
Now it is obviously broken. That is better, but we can do better still:
we can implement the flags as the C library does. That is what this CL does.

Change-Id: I02d4bb981ebd39eb35e47c6e5281f85eaea68016
Reviewed-on: https://go-review.googlesource.com/126516
Run-TryBot: Ian Lance Taylor <iant@golang.org>
TryBot-Result: Gobot Gobot <gobot@golang.org>
Reviewed-by: Brad Fitzpatrick <bradfitz@golang.org>
diff --git a/unix/syscall_linux.go b/unix/syscall_linux.go
index 3514c1e..712d172 100644
--- a/unix/syscall_linux.go
+++ b/unix/syscall_linux.go
@@ -1415,10 +1415,70 @@
 func Faccessat(dirfd int, path string, mode uint32, flags int) (err error) {
 	if flags & ^(AT_SYMLINK_NOFOLLOW|AT_EACCESS) != 0 {
 		return EINVAL
-	} else if flags&(AT_SYMLINK_NOFOLLOW|AT_EACCESS) != 0 {
-		return EOPNOTSUPP
 	}
-	return faccessat(dirfd, path, mode)
+
+	// The Linux kernel faccessat system call does not take any flags.
+	// The glibc faccessat implements the flags itself; see
+	// https://sourceware.org/git/?p=glibc.git;a=blob;f=sysdeps/unix/sysv/linux/faccessat.c;hb=HEAD
+	// Because people naturally expect syscall.Faccessat to act
+	// like C faccessat, we do the same.
+
+	if flags == 0 {
+		return faccessat(dirfd, path, mode)
+	}
+
+	var st Stat_t
+	if err := Fstatat(dirfd, path, &st, flags&AT_SYMLINK_NOFOLLOW); err != nil {
+		return err
+	}
+
+	mode &= 7
+	if mode == 0 {
+		return nil
+	}
+
+	var uid int
+	if flags&AT_EACCESS != 0 {
+		uid = Geteuid()
+	} else {
+		uid = Getuid()
+	}
+
+	if uid == 0 {
+		if mode&1 == 0 {
+			// Root can read and write any file.
+			return nil
+		}
+		if st.Mode&0111 != 0 {
+			// Root can execute any file that anybody can execute.
+			return nil
+		}
+		return EACCES
+	}
+
+	var fmode uint32
+	if uint32(uid) == st.Uid {
+		fmode = (st.Mode >> 6) & 7
+	} else {
+		var gid int
+		if flags&AT_EACCESS != 0 {
+			gid = Getegid()
+		} else {
+			gid = Getgid()
+		}
+
+		if uint32(gid) == st.Gid {
+			fmode = (st.Mode >> 3) & 7
+		} else {
+			fmode = st.Mode & 7
+		}
+	}
+
+	if fmode&mode == mode {
+		return nil
+	}
+
+	return EACCES
 }
 
 /*
diff --git a/unix/syscall_linux_test.go b/unix/syscall_linux_test.go
index eed1726..7fb5804 100644
--- a/unix/syscall_linux_test.go
+++ b/unix/syscall_linux_test.go
@@ -394,19 +394,19 @@
 	defer chtmpdir(t)()
 	touch(t, "file1")
 
-	err := unix.Faccessat(unix.AT_FDCWD, "file1", unix.O_RDONLY, 0)
+	err := unix.Faccessat(unix.AT_FDCWD, "file1", unix.R_OK, 0)
 	if err != nil {
 		t.Errorf("Faccessat: unexpected error: %v", err)
 	}
 
-	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.O_RDONLY, 2)
+	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.R_OK, 2)
 	if err != unix.EINVAL {
 		t.Errorf("Faccessat: unexpected error: %v, want EINVAL", err)
 	}
 
-	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.O_RDONLY, unix.AT_EACCESS)
-	if err != unix.EOPNOTSUPP {
-		t.Errorf("Faccessat: unexpected error: %v, want EOPNOTSUPP", err)
+	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.R_OK, unix.AT_EACCESS)
+	if err != nil {
+		t.Errorf("Faccessat: unexpected error: %v", err)
 	}
 
 	err = os.Symlink("file1", "symlink1")
@@ -414,8 +414,30 @@
 		t.Fatal(err)
 	}
 
-	err = unix.Faccessat(unix.AT_FDCWD, "symlink1", unix.O_RDONLY, unix.AT_SYMLINK_NOFOLLOW)
-	if err != unix.EOPNOTSUPP {
-		t.Errorf("Faccessat: unexpected error: %v, want EOPNOTSUPP", err)
+	err = unix.Faccessat(unix.AT_FDCWD, "symlink1", unix.R_OK, unix.AT_SYMLINK_NOFOLLOW)
+	if err != nil {
+		t.Errorf("Faccessat SYMLINK_NOFOLLOW: unexpected error %v", err)
+	}
+
+	// We can't really test AT_SYMLINK_NOFOLLOW, because there
+	// doesn't seem to be any way to change the mode of a symlink.
+	// We don't test AT_EACCESS because such tests are only
+	// meaningful if run as root.
+
+	err = unix.Fchmodat(unix.AT_FDCWD, "file1", 0, 0)
+	if err != nil {
+		t.Errorf("Fchmodat: unexpected error %v", err)
+	}
+
+	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.F_OK, unix.AT_SYMLINK_NOFOLLOW)
+	if err != nil {
+		t.Errorf("Faccessat: unexpected error: %v", err)
+	}
+
+	err = unix.Faccessat(unix.AT_FDCWD, "file1", unix.R_OK, unix.AT_SYMLINK_NOFOLLOW)
+	if err != unix.EACCES {
+		if unix.Getuid() != 0 {
+			t.Errorf("Faccessat: unexpected error: %v, want EACCES", err)
+		}
 	}
 }