Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

shm_open and ftruncate race condition possible?

From the shm_open man page:

A new shared memory object initially has zero length. The size of the object can be set using ftruncate(2). [...] The shm_open() function itself does not create a shared object of a specified size because doing so would duplicate an extant function that sets the size of an object referenced by a file descriptor.

Doesn't this expose the application to a race condition? Consider the following pseudo-code:

int fd = shm_open("/foo", CREATE);
if ( fd is valid ) {
  // created shm object, so set its size
  ftruncate(fd, 128);
} else {
  fd = shm_open("/foo", GET_EXISTING);
}
void* mem = mmap(fd, 128);

Since the shm_open and ftruncate calls (together) are not atomic, you could have a race condition whereby one process calls shm_open (CREATE case) but, before calling ftruncate, another process calls shm_open (GET_EXISTING case) and attempts to mmap the object of 0 size and possibly even write to it.

I can think of two ways to avoid this race condition:

  1. Use an IPC mutex/semaphore to make the whole thing synchronized, or...

  2. If it's safe (per POSIX), call ftruncate in both the CREATE and GET_EXISTING cases.

Which is the preferred method for avoiding this race condition?

like image 677
etherice Avatar asked May 11 '13 23:05

etherice


2 Answers

Your approach (calling ftruncate from both) should work, but you need a way to synchronize use of the contents of the shared memory segment anyway. As the memory is initially empty (zero-filled) and thus does not contain a valid synchronization object, unless you're going to roll your own with atomics, you need a secondary form of synchronization anyway for controlling access to the shared memory.

I would think normally, rather than having multiple process racing to create-or-open a shared memory segment with a fixed name, you'd want to have an owner process responsible for creating a segment with a random name, using O_EXCL to avoid random or malicious collisions, and then passing the name, once you've successfully opened it, sized it, and created synchronization objects in it, to the other processes that need to access it.

like image 58
R.. GitHub STOP HELPING ICE Avatar answered Oct 31 '22 14:10

R.. GitHub STOP HELPING ICE


As @R. alluded to, another issue here is that having created the file there is still a window before contents, such as a mutex, are initialised and ready for use.

A slightly different solution to the above is:

Try to open(). If open() succeeds, simply map() and use with the necessary guarantee (see below) that the contents are already initialised and good to go. If open() fails, create and initialise a temporary file, then try to hard link() the temporary file as the desired file and unlink() the temporary name.

If link() succeeds, we have now made the initialised file available for ourselves and other processes. If link() fails with EEXIST, another process got there first (unlike rename(), link() fails if the target name exists). Either way, our repeated open() should now succeed with an initialised ready to use file.

With this strategy a race condition clearly exists for initialisation of the temporary file, however provided that the initialisation process is idempotent, not overly expensive on resources, and processes each choose a unique temporary file, this is of no consequence. If multiple initialisation could be an issue, a solution is to split initialisation into a two stage process, with stage one being just a mutex in the file for use to guard against multiple initialisation of the rest of the file during the second stage.

like image 1
Nick Avatar answered Oct 31 '22 13:10

Nick