Showing posts with label threads. Show all posts
Showing posts with label threads. Show all posts

Wednesday, June 30, 2004

Java and MT


Java's memory model is very aggressive, and you have to be very careful when accessing memory from multiple threads. You of course have to synchronize access to memory locations, but you have to synchronize them even when it looks like you don't have to. There are several cases where you must use synchronize:
  • To provide a mutual exclusion barrier to prevent one thread from modifying a data structure while the other is reading it.
  • To provide a memory barrier to prevent memory operation reordering from doing something you didn't want to have happen.
  • To make the memory you're accessing volatile so that the runtime optimizer doesn't throw away your request to read a memory location.
Here's a good web page that discusses this.
A good rule to use is that when in doubt, synchronize.
Reordering can only hit you with a multiple-cpu machine, but the problems that I've been running into recently happen on my single CPU machine, with something like this:
(Note that everything after this is speculation based on behavior I've seen):
int m_y = 0;
Thread1() {
    synchronized(m_x) {
        m_y = 1;
    }
}
void Thread2() {
    while(true)
        System.out.println(m_y);
}
Even after the code in Thread1 has executed in its thread, the code in Thread2 will print 0; I believe this is because the runtime optimizer doesn't bother to look at the value of m_y after the first access. This is similar to a compile-time optimizer, which you'd fix with volatile. But a compile-time optimizer couldn't do anything in this situation.
But in Java the runtime optimizer will make it so that the first access gets the value, but it won't bother reading the value from memory anymore after that.
This strange behavior goes away by putting the synchronize(m_x) around the access to m_y. I believe this tells the runtime optimizer that something is likely to have been changed by another thread.

Thursday, May 6, 2004

Linux and Java and Threads and setuid


Today I learned something very interesting. I learned that you can't setuid on a process in Linux; not when you have multiple threads. Please see #8 in this list.
What they refer to as interesting times probably includes the following:
  • When calling setuid, only the caller thread will actually get its uid changed. All other existing threads in the "process" retain their original uid.
  • I believe any sane person should recognize this as meaning Linux is broken when using threads and setuid.
  • This is a security hole, because root threads still exist in the process. If the non-root threads are hijacked by an attacker, they can stack stomp on the root threads and execute arbitrary code as root.
  • Because synchronization depends on the ability to deliver signals, and delivering signals depends on priviledges, it's easy to see how synchronization between a thread running as root and another running as non-root can wedge the process.
  • Even if I did call setuid in the first bytecode instruction in a Java process, it's too late; Java has already forked threads to do things like garbage collection, and those threads present the security hole described above, and the synchronization problem described above.
  • I'm sure there's a long list of other reasons why this is bad, but I can't think of them now, and the above is sufficient.
In our project we have a Java process that uses forked processes written in C; the purpose of these forked processes is to run as root, or at least elevated privileges, while the Java process runs as some sort of nobody user. Unfortunately this doesn't work very well at all on Linux because we cannot downgrade the uid of the Java process after it starts.
This also means that if we want to listen on a port under 1024, we'll have to do that some other way; there's no way we could get the Java process to bind to that port as root and then downgrade to a nobody uid.
Also the processes I refer to have to be forked before the JVM starts. This means that we have to rendezvous with them in some manner that either means some sort of JNI code to hook up the file descriptors in the pipes, or use some other form of IPC.