Monday, October 30, 2006

UPDATE: The char *apple[] Argument Vector

Way back in February I posted about the the char *apple[] argument vector, i.e. the "secret" 4th parameter to all binaries executed on Mac OS X. I just wanted to update that post with one small piece of information.

The value of apple[0] isn't always the path to the executed binary image on disk. Specifically, if a symlink points to the file, apple[0] will refer to the symlink. For example:

$ cat -n apple.c
1 #include <stdio.h>
2 int main(int argc, char *argv[], char *envp[], char *apple[]) {
3 printf("apple[0] = %s\n", apple[0]);
4 return 0;
5 }
$ gcc -o apple apple.c
apple[0] = ./apple
$ ln -s apple foo
$ ./foo
apple[0] = ./foo


Ideally, what we'd like is the path to the executing binary image, even if it was executed via a symlink. We can do this using apple[0] and the function realpath(3) to resolve all the symlinks. For example:

$ cat -n apple.c 
1 #include <sys/param.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4 int main(int argc, char *argv[], char *envp[], char *apple[]) {
5 char resolved[PATH_MAX];
6 realpath(apple[0], resolved);
7 printf("resolved apple[0] = %s\n", resolved);
8 return 0;
9 }
$ gcc -o apple apple.c
$ ln -s apple foo
$ ./apple
resolved apple[0] = /Users/jgm/apple
$ ./foo
resolved apple[0] = /Users/jgm/apple


That's about it.

Saturday, October 14, 2006

Google Calculator from the Command Line

I'm not sure if you're aware of Google's built-in calculator, but it's totally awesome. It does all sorts of basic calculations, almost any unit conversion you can dream up, and for you programmers, it's great for doing base conversions. But opening a web browser to www.google.com just to make sure you converted 0x34 to decimal in your head correctly, isn't always convenient (this is of course assuming that no other calculators exist, such as bc ;-) ).

So, I threw together a very simple command line interface to Google Calculator -- aptly named gcalc. It's also a good example of how Cocoa's powerful classes can let you whip up useful tools quickly, much like a scripting language. The help screen shows some example usages:

$ gcalc
gcalc version 0.1 by Greg Miller

Usage: gcalc [-d] <calculator query>

example: gcalc "5+2*2"
example: gcalc 5!
example: gcalc "sqrt(-4)"
example: gcalc "160 pounds * 4000 feet in calories"
example: gcalc avogadros number
example: gcalc 0b110111010 + 0x33 in decimal
example: gcalc 22 lira in yen
example: gcalc 2 to the power of 5


You can download the source and a prebuilt universal binary from the Google code project page at http://code.google.com/p/uj-gcalc/

Or if you just want to glance at the source, you can check it out here:

Saturday, October 07, 2006

LaunchServices From a root Daemon?

At the end of this post I mention that I'd like to find a way to start a process in the console user's session, from a root process in the startup item context. I do not know of a documented way to do this, but to be clear, there is currently (at least as of 10.4.8) a way to do it simply using LaunchServices.

LaunchServices is the recommended way to launch an application on OS X. When you double-click an item in the dock, it's launched using LaunchServices. When you double-click on a PDF file, it is LaunchServices that figures out that Preview.app is the best candidate to handle that file type (because you certainly don't have Adobe Acrobat installed), it opens Preview.app and tells it to open said file.

LaunchServices is a sub-framework the ApplicationServices umbrella framework, which is not daemon safe according to TN2083. "Daemon safe" means that daemon processes running in the root bootstrap context (aka startup item context) are allowed to link with and use the framework. One reason a framework would not be daemon safe, is if it uses the WindowServer process.

The WindowServer is not only in charge of managing windows on screen, it's also intimately involved with process management (with some help from the loginwindow process). As the matter a fact, when you launch an application it will ultimately be the WindowServer that does a fork() and exec() to start your process. You can use ps to see that the WindowServer is indeed the parent for most of your processes.

$ ps jaxww | grep WindowServe[r]
windowse 59 1 59 .../CoreGraphics.framework/Resources/WindowServer -daemon
$ ps jx | awk '{if ($3 == 59) print}'
jgm 132 59 ... /System/Library/CoreServices/Dock.app/Contents/MacOS/Dock -psn_0_393217
jgm 134 59 ... /System/Library/CoreServices/SystemUIServer.app/Contents/MacOS/SystemUIServer -psn_0_524289
jgm 136 59 ... /System/Library/CoreServices/Finder.app/Contents/MacOS/Finder -psn_0_655361
jgm 139 59 ... /Applications/Google Notifier.app/Contents/MacOS/Google Notifier -psn_0_786433
jgm 197 59 ... /Applications/Utilities/Terminal.app/Contents/MacOS/Terminal -psn_0_1572865
jgm 257 59 ... /Applications/Safari.app/Contents/MacOS/Safari -psn_0_2490369

(Note that if you look at the Parent Process field in Activity Monitor, it will incorrectly report the Dock as the parent of processes that were launched by clicking their dock icon. This is incorrect, and can be verified by look at the output with ps.)

OK, back on topic. So we know the WindowServer is important to launching processes. Actually when we launch a process in typical Cocoa fashion using the NSWorkspace class, LaunchServices is invoked behind the scenes to send a message to the WindowServer via mach messages, requesting that the WindowServer fork() up a new process in the console user's session. So in order to use LaunchServices (or NSWorkspace), we need to be able to communicate with the WindowServer, which is the reason why LaunchServices (and therefore ApplicationServices and therefore Cocoa) is not "daemon safe".

However, as TN2083 points out, there is a "global window server" reference available in the startup item context. This window server reference appears to be an artifact from the past as Quinn explains in the Technote:

The reasons for this non-obvious behavior are lost in the depths of history. However, the fact that this works at all is pretty much irrelevant because there are important caveats that prevent it from being truly useful.

But the fact remains that there is a reference, and we can see it with the Bootstrap dump command.
$ sudo /usr/libexec/StartupItemContext ~/bin/BootstrapDump  | grep Window
"com.apple.windowserver" by "/System/Library/.../Resources/WindowServer"


However, the only users allowed to connect to the WindowServer process are root and the console user. (The console user is the "current" user in a fast-user-switched environment. The console user is the owner of the /dev/console device.) But if we're running as root we should be able to connect to the window server using standard LaunchServices calls. Let's try.

$ cat -n launch.m
1 #import <Cocoa/Cocoa.h>
2 int main(void) {
3 [[NSWorkspace sharedWorkspace]
4 launchApplication:@"TextEdit"];
5 return 0;
6 }
$ gcc -o launch launch.m -framework Cocoa
$ sudo /usr/libexec/StartupItemContext ./launch

This will start TextEdit in my current user session. We can use Bootstrap dump again to verify that the TextEdit process is indeed running in my session, but it is. The command /usr/bin/open links against Cocoa, and works the same way, so I can simply type sudo /usr/libexec/StartupItemContext /usr/bin/open -a TextEdit to do the same thing.

As I mentioned above, only the console user and root are allowed to connect to the WindowServer process, so if your daemon is running as nobody (for example), it won't be able to do this.

$ sudo /usr/libexec/StartupItemContext /usr/bin/sudo -u nobody -s
$ open -a TextEdit
kCGErrorRangeCheck : Window Server communications from outside of session allowed for root and console user only
INIT_Processeses(), could not establish the default connection to the WindowServer.Abort trap

(The first command above starts a shell running as user nobody in the startup item context.)

So, we can see that from the startup item context, as root, we are able to launch a process in the console user's session, simply using normal LaunchServices calls. But note that this is undocumented behavior, and it is not guaranteed to work at any point in the future. It merely worked in this example.

Friday, October 06, 2006

Finder's Locum

Jumping right in, let's consider this little example:

$ mkdir -p foo/bar
$ sudo chown -R root:wheel foo
Password:
$ rm -rf foo
rm: foo/bar: Permission denied
rm: foo: Directory not empty


We create two directories, foo/ and a subdir bar/. We change both of these directories to be owned by root and in group wheel. Then, as a non-root user, we try to recursively delete foo/, and not too surprisingly it fails.

Notice that an error is displayed for foo/bar before the error for foo/. This is because the system call to remove a directory -- rmdir(2) -- requires the directory to be empty before it can be removed. This means that directory hierarchies are removed in a depth-first order. In order for foo/ to be removed, it must be empty, so to make it empty we must remove foo/bar/, etc.

As a quick aside, removing a file (or directory) in Unix does not require write permission to the file! Let me repeat that. You can remove a file if you have write access to the directory in which the file resides -- even if root owns the file. Quick example:
$ mkdir test
$ cd test
$ touch hi.txt
$ sudo chown root:wheel hi.txt
Password:
$ ls -al
total 0
drwxr-xr-x 3 jgm jgm 102 Oct 6 23:13 ./
drwxr-xr-x 31 jgm jgm 1054 Oct 6 23:13 ../
-rw-r--r-- 1 root wheel 0 Oct 6 23:13 hi.txt
$ rm hi.txt
$ ls -al
total 0
drwxr-xr-x 2 jgm jgm 68 Oct 6 23:15 ./
drwxr-xr-x 31 jgm jgm 1054 Oct 6 23:13 ../


This shows that I can delete a root owned file as long as I have write access to the directory. (See chmod(2) for details about how the sticky bit on a directory affects this behavior [this is why shared tmp directories generally have the sticky bit set].)

Thinking back to the original issue, this also explains why we were unable to rm -rf foo. Because foo/bar/ needed to be deleted first, but in order to delete that we need write access to its parent dir. But foo/ was owned by root and we didn't have write access to it. So, all that makes sense now.

Interestingly, if we use the Finder and drag the folder foo/ to the trash, we are able to empty the trash. We're not prompted for a password, it just works. How does the Finder do this?

Well, the Finder application links with a private Apple framework named DesktopServicesPriv.framework, which helps with taking out the trash. Bundled as a resource in the framework is a setuid root binary named Locum, which the Finder uses to delete files that it normally wouldn't have access to delete.

$ cd /System/Library/PrivateFrameworks/DesktopServicesPriv.framework/
$ ls -l Resources/Locum
-rwsr-xr-x 1 root wheel 108940 Mar 26 2006 Resources/Locum*


And we can watch what happens when we drag foo/ to the trash then empty it, using fs_usage.
$ sudo fs_usage | grep Locum
...
23:33:48 rmdir /.vol/234881026/2148678/bar 0.000303 Locum
23:33:48 rmdir /.vol/234881026/2147406/foo 0.000264 Locum
...


Now the question is, is this secure and safe? Well, it's probably fine. I imagine that Apple has rigorously tested and reviewed Locum. Conceivably, it has smarts to guarantee that it only deletes things out of a .Trashes folder. It possibly even does a little handshake to guarantee that its parent is a Finder process. This is all speculation, but the point is, I don't immediately see any big holes here (though this is very different than the behavior one would find on a typical Unix system).

I think the only remaining question is, what the hell does Locum mean? Thankfully we have Wikipedia to help us out: http://en.wikipedia.org/wiki/Locum.