Let me start by saying how much I love the Alpine mail client. It’s fast and easy. I can hit the “d” key ten times and delete as many messages in the amount of time I can click on one email in a GUI mail client.
So in my foray into OpenBSD I ran into the annoying, but ever-present, issue with mbox locking that you get into with Alpine. The dreaded:
[Folder vulnerable - directory /var/mail must have 1777 protection]
There have been countless arguments about this over the years. You can be sure it is a VERY good idea to use some form of locking when accessing mbox files. Without this you are likely to eventually corrupt your entire mailbox.
Mode 1777 sets the sticky bit on the mail spool directory (/var/mail on OpenBSD) and makes that directory world writable (so users can write lock files here). With the sticky bit this is isn’t as bad as it seems, but it does allow nefarious users to potentially fill your mail directory. Some admins work around this with quotas, blah, blah, blah 😉
The alternative is the default permissions of 0755 with root:wheel ownership. With this setup you must be root to write a lock file in the /var/mail directory. This means that Alpine for normal users cannot properly lock the mbox. This is bad. Don’t leave it like this and assume, “She’ll be right!”
There are a couple of alternatives including Alpine’s suggestion (only if you can’t set /var/mail to 1777) of using an external setgid mail locking program like mlock, which comes with the uw-imap setup (in the Alpine source).
From the uw-imap distribution (with Alpine) FAQ:
/var/spool/mail must have 1777 protection mean? How can I fix this?
In order to update a mailbox in the default UNIX format, it is
necessary to create a lock file to prevent the mailer from
delivering mail while an update is in progress. Some systems use
a directory protection of 775, requiring that all mail handling
programs be setgid mail; or of 755, requiring that all mail
handling programs be setuid root.
The IMAP toolkit does not run with any special privileges, and I
plan to keep it that way. It is antithetical to the concept of a
toolkit if users can't write their own programs to use it. Also,
I've had enough bad experiences with security bugs while running
privileged; the IMAP and POP servers have to be root when not
logged in, in order to be able to log themselves in. I don't
want to go any deeper down that slippery slope.
Directory protection 1777 is secure enough on most well-managed
systems. If you can't trust your users with a 1777 mail spool
(petty harassment is about the limit of the abuse exposure),
then you have much worse problems then that.
If you absolutely insist upon requiring privileges to create a
lock file, external file locking can be done via a setgid mail
program named /etc/mlock (this is defined by LOCKPGM in the
c-client Makefile). If the toolkit is unable to create a
<...mailbox...>.lock file in the directory by itself, it will
try to call mlock to do it. I do not recommend doing this for
performance reasons.
A sample mlock program is included as part of imap-2010. We have
tried to make this sample program secure, but it has not been
thoroughly audited.
Ok, you say… “Why can’t I just set /var/mail to 1777 and be done with it?”
Well, you should be able to, kind of. Except this doesn’t work with OpenBSD’s built-in MTA, OpenSmtpd (known as just smtpd on OpenBSD). Although the stable release of 6.7 says it should work, alas, it does not.
For (good) security purposes some pledge() code was added to /usr/libexec/lockspool. This is a good thing since locksppol is setuid root! The mail delivery program used for smtpd is mail.local. The mail.local process makes calls to seteuid() if the mode on /var/mail is 1777. (Actually lockspool makes this call, but the code is part of the mail.local src). However, lockspool has pledge()d that it will not call seteuid()! So this simply fails and you end up with this in your mail logs (/var/log/maillog)
Aug 23 16:53:01 <hostname> mail.local: lockspool: unable to get lock
If you are seeing these in your logs you are likely also seeing these in /var/log/messages:
Aug 23 16:53:01 <hostname> /bsd: lockspool[85982]: pledge "id", syscall 183
This is the kernel saying “Whoa there!” to lockspool– it did not include the “id” permission in its pledge() call, so calling syscall 183 (SYS_seteuid) is not allowed. The kernel unpleasantly terminates the lockspool process and the mail never gets delivered.
If you found this article via search, and found just this bit above above the log messages the solution is to remove the sticky bit from /var/mail and to set it back to mode 755. This is not the solution to make Alpine work right, though.
Well this goes against the (stable branch) manpage of mail.local:
Significant efforts have been made to ensure that mail.local acts as
securely as possible if the spool directory is mode 1777 or 755. The
default of mode 755 is more secure, but it prevents mail clients from
using username.lock style locking. The use of 1777 is more flexible in
an NFS shared-spool environment, so many sites use it. However, it does
carry some risks, such as attackers filling the spool disk. Some of
these problems may be alleviated by making the spool a separate
filesystem, and placing quotas on it. The use of any mode other than
1777 and 755 for the spool directory is recommended against but may work
properly.
So either lockspool has a bug and needs to allow the seteuid() from its pledge(), or the documentations is wrong. It seems that now in the current branch of OpenBSD the docs have been changed and mail.local now states that only mode root:wheel 0755 is supported for /var/mail, and that processes need to be root (or setuid) to write to /var/mail. Good or bad, this does resolve the bug into a feature.
But Where does this leave us with Alpine?
Good question. My first thought was to just use mlock from the Alpine folks. This is a setgid program that runs as the mail user. I did get this working fine. I had to set the group of /var/mail and the mailboxes to _smmsp as hard coded in the (OpenBSD port Patched version) source for mlock. Then I had to install mlock in /usr/local/libexec/mlock and set it to the same gid and chmod it g+s. But this is not the ideal solution. At least to me it isn’t. (FYI: If you want to build the ports Alpine with mlock I think you need to set the environment variable SUBPACKAGE to “-imap” before you make. Though you don’t need that if you use my proposed better solution below.)
Even the Alpine docs quoted above claim mlock has not be audited for security.
We have tried to make this sample program secure,
but it has not been thoroughly audited.
I had modified a few things in my copy (made the strcpy()s use strncpy() and such – as well as added some unveil() and pledge() calls to the start of it, which generally made it safer.) However, I still wasn’t happy. I now had Alpine using this setgid locking program that was not 100% guaranteed to work with the locking of smtpd (pretty sure it did, though).
Hmmm… So how does OpenBSD’s smtpd handle locking by default? It uses /usr/libexec/lockspool. This is a setuid root locking program. Naturally a program that is setuid root should be scarier to the admin than a program that is setgid to the mail user. However, this program is simpler and seems to have better code. It already has the unveil() and pledge() calls for added protection, and it is supported as part of the OpenBSD install. Hopefully it is audited as well. Nevertheless, locksppol is already being used all the time by smtpd. So you are likely already vulnerable if there is an issue.
So in an ideal world we would patch Alpine itself to use lockspool rather than mlock. This let’s OpenBSD keep its /var/mail root:wheel 0755, and “just works.” Sure, running the external locking program is a hint slower than Alpine’s internal locking designed for a mail spool with mode 1777, but this patched Alpine method will work correctly with OpenBSD as is. It is pretty clear from the current branch manpage for mail.local that they want to keep /var/mail as is, and not use 1777 mode for it. From that new current branch mail.local manpage:
Significant effort has been made to ensure that mail.local
acts as securely as possible. It will only deliver to a mail
spool directory that is not world-writable. The default mode
of /var/mail on OpenBSD is 755, which prevents non-root
processes from creating mail spool files. The MTA is
expected to either create the mail spool file itself, or
call mail.local as root.
OK. Reasonable approach for best security.
Patches
So here are the patches. I plan on submitting these to the ports maintainer.
This first patch is to modify the patch that came with the ports package of Alpine found in /usr/ports/mail/alpine/patches. All the modification does is adjust the LOCKPGM variable to point to lockspool.
Edit the file:
/usr/ports/mail/alpine/patches/patch-imap_src_osdep_unix_Makefile
Change this line:
LOCKPGM=$(PREFIX)/libexec/mlock \
to this:
LOCKPGM=/usr/libexec/lockspool \
You possibly modify that in your build directory manually if you needed to. May need to” make clean” as well if you do.
This second patch modifies c-client, which is used by alpine to access the mboxes. First, here is a sample of just the changes I made. I am going to provide a patch to reply the ports patch below, but it will be confusing with the ports patches in there as well.
These are my changes to make Alpine use lockspool on OpenBSD. This is a diff against the already patched source file. You want the file below for your machine
--- ./imap/src/osdep/unix/env_unix.c.ports_patched Mon Aug 24 16:54:40 2020
+++ ./imap/src/osdep/unix/env_unix.c Mon Aug 24 17:06:26 2020
@@ -1198,14 +1198,12 @@
case EACCES: /* protection failure? */
MM_CRITICAL (NIL); /* go critical */
if (closedBox || !lockpgm); /* can't do on closed box or disabled */
- else if ((*lockpgm && stat (lockpgm,&sb)) ||
- (!*lockpgm && stat (lockpgm = LOCKPGM1,&sb) &&
- stat (lockpgm = LOCKPGM2,&sb) && stat (lockpgm = LOCKPGM3,&sb) &&
- stat (lockpgm = LOCKPGM4,&sb)))
+ else if ((*lockpgm && stat (lockpgm,&sb)))
lockpgm = NIL; /* disable if can't find lockpgm */
+ /* /usr/libexec/lockspool for OpenBSD */
else if (pipe (pi) >= 0) { /* make command pipes */
long cf;
- char *argv[4],arg[20];
+ char *argv[2];
/* if input pipes usable create output pipes */
if ((pi[0] < FD_SETSIZE) && (pi[1] < FD_SETSIZE) && (pipe (po) >= 0)) {
/* make sure output pipes are usable */
@@ -1214,9 +1212,8 @@
else if (!(j = fork ())) {
if (!fork ()) { /* make grandchild so it's inherited by init */
/* prepare argument vector */
- sprintf (arg,"%d",fd);
- argv[0] = lockpgm; argv[1] = arg;
- argv[2] = file; argv[3] = NIL;
+ argv[0] = lockpgm; /* no args to lockspool for OpenBSD */
+ argv[1] = NIL;
/* set parent's I/O to my O/I */
dup2 (pi[1],1); dup2 (pi[1],2); dup2 (po[0],0);
/* close all unnecessary descriptors */
@@ -1238,7 +1235,8 @@
grim_pid_reap (j,NIL);/* reap child; grandchild now owned by init */
/* read response from locking program */
if (select (pi[0]+1,&rfd,0,0,&tmo) &&
- (read (pi[0],tmp,1) == 1) && (tmp[0] == '+')) {
+ (read (pi[0],tmp,1) == 1) && (tmp[0] == '1')) {
+ /* OpenBSD lockspool uses 1 for successful lock */
/* success, record pipes */
base->pipei = pi[0]; base->pipeo = po[1];
/* close child's side of the pipes */
First we remove the references to the other locations for the lock program. These are LOCKPGM1, LOCKPGM2, etc. These provided a way for Alpine to fall back on various locations where it might find mlock. Since we are tailoring this to OpenBSD and lockpool is part of OpenBSD and always at /usr/libexec/lockspool we don’t need this extra stuff. The variable lockpgm is already set to LOCKPGM (no number) up above in the code. This was the macro we set in that Makefile above.
Then we adjust our argv array to be just two elements because lockspool doesn’t need any arguments. It just needs the program name (lockspool) in argv[0] and NULL in argv[1]. Not sure why the rest of the code uses NIL rather than NULL, but they are the same (0). We no longer need the sprintf() because again lockspool doesn’t need arguments (it determines the current user’s name to figure out the mbox filename).
Finally we check tmp[0] == ‘1’ rather than equal to “+” like mlock used. OpenBSD locksppol outputs the 1 for a successful lock.
That’s it! Pretty simple patch. To clean this up references to the other LOCKPGM1, LOCKPGM2, etc. in the headers and Makefiles should be removed, but this has no effect on the compiled code.
Here is the link to the new patch file for ports. Replace the file:
/usr/ports/mail/alpine/patches/patch-imap_src_osdep_unix_env_unix_c
with this new one.
Now you should be able to make and make install to install the fixed Alpine. If you encounter any errors try a “make clean” and then try to build again. See the ports docs if you need help.
Testing (Important – Don’t skip)
There are a couple ways to determine that the new alpine is running, and running correctly on your OpenBSD system. First, with the new setup you should no longer see the warning about “[Folder vulnerable – directory /var/mail must have 1777 protection].” Alpine seems to quell this is you have a working locking program. Of course if you already adjusted the conf to hide this message then this check is useless.
The real way to make sure Alpine is calling lockspool is to use ktrace to trace the system calls. Don’t worry. This is much easier than it sounds.
Go to a directory where you have write permission ($HOME is good). Here we run alpine through ktrace. We give ktrace the -f argument to tell it what file to write its output to.
ktrace -f alpine_trace.out alpine
Alpine will start up normally, but ktrace will do its magic in the background logging all the system calls for us! Open your inbox. Perhaps send one email to your self and wait for delivery. Delete that test email, and expunge the mailbox. Now quit Alpine.
Now we are going to examine the trace output with kdump.
kdump -f alpine_trace.out | less
You will see all sorts of system calls in here and other info. What we are looking for is the call to lockspool. So use the slash key “/” in /usr/bin/less to search for lockspool. Just type “/lockspool” right into the less “:” prompt. This will take you to this section (hopefully).
10877 alpine NAMI "/usr/libexec/lockspool"
10877 alpine STRU struct stat { dev=5, ino=1166436, mode=-r-sr-xr-x ,
nlink=1, uid=0<"root">, gid=7<"bin">, rdev=4668898,
atime=1598303198<"Aug 24 16:06:38 2020">.610763358,
mtime=1588870339<"May 7 11:52:19 2020">,
ctime=1597953746<"Aug 20 15:02:26 2020">.413726489,
size=10800, blocks=24, blksize=16384, flags=0x0, gen=0x0 }
10877 alpine RET stat 0
10877 alpine CALL kbind(0x7f7ffffdea18,24,0x7683999d577f74c4)
10877 alpine RET kbind 0
10877 alpine CALL pipe(0x7f7ffffdf018)
10877 alpine STRU int [2] { 5, 6 }
10877 alpine RET pipe 0
10877 alpine CALL pipe(0x7f7ffffdf010)
10877 alpine STRU int [2] { 7, 8 }
10877 alpine RET pipe 0
10877 alpine CALL kbind(0x7f7ffffdea18,24,0x7683999d577f74c4)
10877 alpine RET kbind 0
10877 alpine CALL kbind(0x7f7ffffde9e8,24,0x7683999d577f74c4)
10877 alpine RET kbind 0
10877 alpine CALL fork()
10877 alpine RET fork 41244/0xa11c
10877 alpine CALL kbind(0x7f7ffffdea18,24,0x7683999d577f74c4)
10877 alpine RET kbind 0
10877 alpine CALL wait4(41244,0,0<>,0)
10877 alpine PSIG SIGCHLD caught handler=0xc63a1ac9e70 mask=0<>
10877 alpine RET wait4 RESTART
10877 alpine CALL sigreturn(0x7f7ffffde740)
10877 alpine RET sigreturn JUSTRETURN
10877 alpine CALL wait4(41244,0,0<>,0)
10877 alpine RET wait4 41244/0xa11c
10877 alpine CALL kbind(0x7f7ffffdea18,24,0x7683999d577f74c4)
10877 alpine RET kbind 0
10877 alpine CALL select(6,0x7f7ffffdeee0,0,0,0x7f7ffffdefe0)
10877 alpine RET select -1 errno 22 Invalid argument
10877 alpine CALL read(5,0x7f7ffffdeae0,0x1)
10877 alpine GIO fd 5 read 1 bytes
"1"
The key things to look for are:
<pid> alpine NAMI "/usr/libexec/lockspool"
You can then see the CALL to pipe and the CALL to fork as Alpine executes the child process of lockspool. Then we see the part where the Alpine parent process reads the “1” from lockspool:
<pid> alpine GIO fd 5 read 1 bytes
"1"
That is what we want. We want to see that /usr/libexec/lockspool is executed, and that we read the “1” for a successful lock. If you want to be lazy you could just:
kdump -f alpine_trace.out | grep lockspool
and check for the NAMI line. That is likely sufficient to at least make sure some patch got in there.
Summary
So you never thought there would be this much to write about the admonition “[Folder vulnerable – directory /var/mail must have 1777 protection]” did you? I sure didn’t!
Actually many people have had flame wars about the mail spool permissions for years. Here we have a real solution that works to both quell the warning, but also to truly keep the mbox safe from corruption (at least via Alpine or smtpd). Simply ignoring the warning is unwise as eventually two processes will collide accessing the mbox and chaos will ensue.
Hopefully we can get these changes incorporated into ports and then nobody will have to think about it again. If you manage to submit this into ports before I do please drop me a note here to give me a credit for the changes. Email joe at this domain. I can provide details privately.