yes i see now why you said the spinptr code didn't work, but the code i posted was a little different. the spinptr code really did work originally: the code was a little different from what i posted earlier--it had switch(fork()){ which became switch(rfork(RFFDG|RFPROC)) { in your version BUT in Plan 9 when the code was originally written, an rfork with RFMEM caused RFMEM to be implied(!) in all subsequent rforks (and thus forks), so the original spinptr worked because although it said `fork', it was still sharing the address space. (i first noticed the feature when i had a little trouble with the 8-1/2 event library.) when Plan 9's implementation (fortunately) later changed to eliminate that unpleasant effect, the fork made the memory unshared, spinptr stopped working and a rendezvous would indeed be needed. on the other hand, the vstack probably isn't needed then because the two processes are not sharing the stack. my rfork call in the example was rfork(RFMEM|RFPROC|RFFDG|RFENVG|RFREND) which kept the RFMEM (explicitly), and that does keep the spinptr working. not that it's an efficient method, but there's a reason it's hard to use rendezvous straightforwardly in that particular context. it was really some of the peculiar effects of RFMEM and consequent shared (malloc'd) stack in this particular case that interested me, as an example of how plausible code and helpful comments can be completely misleading. the idea behind spinptr/vstack is that the two processes are sharing the malloc'd stack after the rfork so the child calls assembly-language vstack to shift its stack to its private, unshared memory (rfork even with RFMEM doesn't share the stack segment). until the child does that, the parent had better not budge because that would change the stack on which the child is still executing, corrupting its data. once the child has switched to its unshared stack, by setting SP to the value of ustack, which points into the unshared stack segment, it can let the parent continue. it works, but for odd reasons, and it's critical how the code is arranged. one key point is that the comment next to vstack, is wrong (on an x86). the set of possible execution traces is rather bizarre, and depends who runs first after the rfork. the child needs to call something in assembly language to change the stack pointer. they both return from rfork. if the parent returns first, it blocks on the while(*spinptr) ; the child process runs, calls vstack, changes its stack pointer, calls exectramp on that new stack, writes zero to the spin value, and allows the parent to proceed. if the child returns from rfork first, it calls vstack, changes its stack pointer, etc. the parent returns from rfork, finds *spinptr zero and simply continues on its way. that's the way the code is written but it overlooks the stack sharing: the child must call vstack (it can't change the stack pointer otherwise), so its return address will be at the default: following the call to vstack. but its stack is the parent's too, so its vstack return address replaces the parent's rfork return address, and when it runs it doesn't execute the switch but ends up on the other side of the vstack call, and falls through into the default: case. of course on a multiprocessor the two paths can run concurrently, or if a time slice hits on a uniprocessor, they can be interleaved. switch(rfork(RFPROC|RFMEM|RFREND|RFNOTEG|RFFDG|RFNAMEG|RFENVG)) { case -1: goto Error; case 0: /* if child returns first from rfork, its call to vstack replaces ... */ vstack(t); /* ... parent's return address from rfork and parent returns here */ default: /* if parent returns first from rfork, it comes here */ /* can't call anything: on shared stack until child releases spin in exectramp */ while(*spinptr) ; break; }