Why setuid java programs don't work
Its a feature not a bug.
We do odd things with java, that most places would probably use C or python for. Having said that we have some very talented people who can make java do wonderful things.
However when those wonderful things require elevated privilege like CAP_NET_RAW for packet capture, we run into a problem, which is that the kernel treats any executable which has a capability assigned as being equivalent to one with the "setuid" bit set.
As Java loads shared libraries by relying on glibc doing expansion of the environment variable $ORIGIN this can get broken by the setuid bit and some rules introduced to close an exploit.
So, at the command line as root;
root@foo:/tmp# ls -l /usr/lib/jvm/java-6-sun/jre/bin/java
-rwxr-xr-x 1 root root 50794 2011-02-03 01:25 /usr/lib/jvm/java-6-sun/jre/bin/java
root@foo:/tmp# getcap /usr/lib/jvm/java-6-sun/jre/bin/java
/usr/lib/jvm/java-6-sun/jre/bin/java = cap_net_raw+eip
root@foo:/tmp# strace java -version
:
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/x86_64/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/x86_64", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/tls", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
open("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/x86_64/ \
libpthread.so.0", O_RDONLY) = -1 ENOENT (No such file or directory)
stat("/usr/lib/jvm/java-6-sun-1.6.0.24/jre/bin/../lib/amd64/jli/x86_64", \
0x7fff0cc3b8f0) = -1 ENOENT (No such file or directory)
:
(Line breaks to make it look less ugly in the blog)
But as a user running the same command;
atp@foo:/tmp$ strace java -version
open("$ORIGIN/../lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/tls/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/x86_64/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../lib/amd64/jli/libpthread.so.0", O_RDONLY) = -1 ENOENT \
(No such file or directory)
open("$ORIGIN/../jre/lib/amd64/jli/tls/x86_64/libpthread.so.0", O_RDONLY) = -1 \
ENOENT (No such file or directory)
Or without the helpful strace command you get this cryptic error message;
atp@foo:~$ java -version
java: error while loading shared libraries: libjli.so: cannot open shared object \
file: No such file or directory
The reason is here
http://seclists.org/fulldisclosure/2010/Oct/257
which quotes a web page at caldera thus;
For security, the dynamic linker does not allow use of $ORIGIN substitution
sequences for set-user and set-group ID programs. For such sequences that
appear within strings specified by DT_RUNPATH dynamic array entries, the
specific search path containing the $ORIGIN sequence is ignored (though other
search paths in the same string are processed). $ORIGIN sequences within a
DT_NEEDED entry or path passed as a parameter to dlopen() are treated as
errors.
In other words, this is a feature, not a bug. The feature was implemented in the commit referenced in [1].
So we have the situation where java uses $ORIGIN to allow the run time linker to link its shared libraries no matter where in the filesystem it is located. The run time linker thinks that its a setuid root binary as soon as we add a capability to it, and refuses to expand $ORIGIN, causing the link to fail.
So why does linux think that having a capability set means the executable is suid root?
glibc checks the following (in elf/dl-sysdep.c) and sets the value of __libc_enable_secure appropriately.
- The ELF AT_SECURE auxiliary vector
- The values of AT_UID/GID/EUID/EGID auxiliary vectors
__libc_enable_secure = uid != euid || gid != egid;
Using a program that prints out the values of the auxiliary vectors (We can't use LD_SHOW_AUXV, because the secure mode disables that environment variable too (see [2])) we can see that;
atp@foo:~$ /sbin/getcap test_cap
test_cap = cap_net_raw+eip
atp@foo:~$ ls -l test_cap
-rwxr-xr-x 1 root atp 8608 2011-05-26 18:10 test_cap
atp@foo:~$ ./test_cap
current uid 500, effective uid 500
AT_SYSINFO_EHDR: 140735961518080
AT_PHDR: 4194368
AT_PHNUM: 9
AT_ENTRY: 4195552
AT_UID: 500
AT_EUID: 500
AT_GID: 500
AT_EGID: 500
AT_SECURE: 1
The kernel is setting it for us. A quick bit of chasing later through binfmt_elf.c and we end up at cap_bprm_secureexec()
/**
570 * cap_bprm_secureexec - Determine whether a secure execution is required
571 * @bprm: The execution parameters
572 *
573 * Determine whether a secure execution is required, return 1 if it is, and 0
574 * if it is not.
575 *
576 * The credentials have been committed by this point, and so are no longer
577 * available through @bprm->cred.
578 */
579int cap_bprm_secureexec(struct linux_binprm *bprm)
580{
581 const struct cred *cred = current_cred();
582
583 if (cred->uid != 0) {
584 if (bprm->cap_effective)
585 return 1;
586 if (!cap_isclear(cred->cap_permitted))
587 return 1;
588 }
589
590 return (cred->euid != cred->uid ||
591 cred->egid != cred->gid);
592}
593
Which as expected checks to see if we're not root, and if we have capabilities. As a result of this, AT_SECURE gets set in binfmt_elf.c and when the dynamic linker goes into "secure" mode, which disables $ORIGIN (plus other environment variables). This then prevents java from loading its shared objects, and it all falls over.
At the end of this journey we're left with two options.
- The kernel should be more discerning about the type of capability before pressing the AT_SECURE panic button.
- Java shouldn't rely on $ORIGIN expansion when loading shared libraries (although that complicates things enormously for them).
Neither of those look like a quick fix. (And fooling around with /etc/suid-debug didn't help either.).
[1] glibc patch http://www.cygwin.com/ml/libc-hacker/2010-10/msg00007.html
[2] glibc environment variables http://www.scratchbox.org/documentation/general/tutorials/glibcenv.html