Coding can be a lot of fun. And one of the things you can have fun with is fixing bugs1.
Getting a list of symptoms, formulating hypothesis and narrowing those down with tests, arriving at a diagnosis, and ultimately, fixing the bug. The entire process is very similar to what Doctors do and can be very interesting.
Now… some bugs are more interesting than others hence the reason for this blog post. I thought it would be fun to share some of the more interesting bugs I’ve found.
So today, I’m sharing a bug that could be described like this:
Secrets’s browser extensions may not work for users with long usernames.
Sounds interesting, right? Why on earth is the username linked to the browser extensions?
The bug
One of Secrets’s users reported that none of the browser extensions were working with him. Not in Safari, nor Chrome, nor Firefox… nada, nickles.
These browser extensions communicate with a Secrets Helper process via unix socket2. That process in turn communicates with Secrets via another unix socket.
To narrow down the issue, I ask the user to check if he could lock Secrets via the menu bar. Which he could. Since the menubar is managed by the Secrets Helper I knew the communication between the Helper and the main app was working. So the problem is only in the communication with the browser extensions.
Next, I’m thinking that this may be a permissions issue. Somehow the socket does not have the right permission and the browser is unable to connect. So I ask the user the open the terminal and do a ls -l
on the folder. I send him detailed steps on how to do this3. He sends me back this screenshot:
The permissions look fine, but… browser.soc
?? There’s a missing “k” at the end there! WTH?!
I know that I’m creating the path for the socket from a constant in code, so there’s no chance I’ve made a typo somewhere.
I start looking at the code related to creating the socket, retracing all the steps, and finally arrive at this method from GCDAsyncSocket
.
- (NSData *)getInterfaceAddressFromUrl:(NSURL *)url;
{
NSString *path = url.path;
if (path.length == 0) {
return nil;
}
struct sockaddr_un nativeAddr;
nativeAddr.sun_family = AF_UNIX;
strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path));
nativeAddr.sun_len = SUN_LEN(&nativeAddr);
NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)];
return interface;
}
Hmm… it’s copying the socket path to the sockaddr_un
struct and limiting it to the size of sun_path
. Could it be?
struct sockaddr_un {
unsigned char sun_len; /* sockaddr len including null */
sa_family_t sun_family; /* [XSI] AF_UNIX */
char sun_path[104]; /* [XSI] path name (gag) */
};
Argh! The socket path can have a maximum of 104 chars?! Since I’m passing the full path (including the user’s home directory) the “k” is getting clipped. The other socket called helper.sock
has one less character in the name so it evaded the issue.
The fix
Now that I finally understood the issue I could start thinking on the solution. Basically, whatever the path I use to create the socket it must be under 104 characters and not depend on external factors such as the username. It has to be the same path for all instances of Secrets.
The first thing that comes to mind is using the ~
to represent the home directory in the path, but alas we’re in sandbox land and the home directory for sandboxed apps is their sandboxed folder inside ~/Library/Containers
. And since I need other processes to use these sockets I need them to be on a shared group folder.
I could also change the working directory for the Secrets Helper process to be the folder where I create the sockets. This way I would have to pass the socket name. But I was afraid that changing the home directory for the entire process would have unforeseen side effects. I could spawn another process or XPC service just for that but… geez, that would be a lot of work just to create a socket.
In the end, I wrote a method that computes the relative path to the socket from the current home directory. Because both the app’s home directory and the shared group folder are inside ~/Library
I know the final path will not contain the username, so the size of the path will be constant. I could actually hardcode the path and it would work but… yuck.
Problem solved.