1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2  *@ Privilege-separated dot file lock program (OPT_DOTLOCK=yes)
3  *@ that is capable of calling setuid(2), and change its user identity
4  *@ to the VAL_PS_DOTLOCK_USER (usually "root") in order to create a
5  *@ dotlock file with the same UID/GID as the mailbox to be locked.
6  *@ It should be started when chdir(2)d to the lock file's directory,
7  *@ with a symlink-resolved target and with SIGPIPE being ignored.
8  *
9  * Copyright (c) 2015 - 2020 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
10  * SPDX-License-Identifier: ISC
11  *
12  * Permission to use, copy, modify, and/or distribute this software for any
13  * purpose with or without fee is hereby granted, provided that the above
14  * copyright notice and this permission notice appear in all copies.
15  *
16  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
17  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
18  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
19  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
20  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
21  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
22  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
23  */
24 #undef su_FILE
25 #define su_FILE ps_dotlock_main
26 #define mx_SOURCE
27 #define mx_SOURCE_PS_DOTLOCK_MAIN
28 
29 #define su_ASSERT_EXPAND_NOTHING
30 
31 #include "mx/nail.h"
32 
33 #include <errno.h>
34 #include <string.h>
35 
36 #if defined mx_HAVE_PRCTL_DUMPABLE
37 # include <sys/prctl.h>
38 #elif defined mx_HAVE_PROCCTL_TRACE_CTL
39 # include <sys/procctl.h>
40 
41 # include <unistd.h>
42 #elif defined mx_HAVE_PTRACE_DENY
43 # include <sys/ptrace.h>
44 #elif defined mx_HAVE_SETPFLAGS_PROTECT
45 # include <priv.h>
46 #endif
47 
48 #include "mx/file-locks.h"
49 
50 /* TODO fake */
51 #include "su/code-in.h"
52 
53 /* TODO Avoid linkage errors, instantiate what is needed;
54  * TODO SU needs to be available as a library to overcome this,
55  * TODO or a compiler capable of inlining can only be used */
56 uz su__state;
57 #ifdef su_MEM_ALLOC_DEBUG
su__mem_check(su_DBG_LOC_ARGS_DECL_SOLE)58 boole su__mem_check(su_DBG_LOC_ARGS_DECL_SOLE) {return FAL0;}
su__mem_trace(su_DBG_LOC_ARGS_DECL_SOLE)59 boole su__mem_trace(su_DBG_LOC_ARGS_DECL_SOLE) {return FAL0;}
60 #endif
61 #define su_err_no() errno
62 #define su_err_set_no(X) (errno = X)
63 
64 static void _ign_signal(int signum);
65 static uz n_msleep(uz millis, boole ignint);
66 
67 #include "mx/file-dotlock.h" /* $(PS_DOTLOCK_SRCDIR) */
68 
69 static void
_ign_signal(int signum)70 _ign_signal(int signum){
71    struct sigaction nact, oact;
72 
73    nact.sa_handler = SIG_IGN;
74    sigemptyset(&nact.sa_mask);
75    nact.sa_flags = 0;
76    sigaction(signum, &nact, &oact);
77 }
78 
79 static uz
n_msleep(uz millis,boole ignint)80 n_msleep(uz millis, boole ignint){
81    uz rv;
82 
83 #ifdef mx_HAVE_NANOSLEEP
84    /* C99 */{
85       struct timespec ts, trem;
86       int i;
87 
88       ts.tv_sec = millis / 1000;
89       ts.tv_nsec = (millis %= 1000) * 1000 * 1000;
90 
91       while((i = nanosleep(&ts, &trem)) != 0 && ignint)
92          ts = trem;
93       rv = (i == 0) ? 0
94             : (trem.tv_sec * 1000) + (trem.tv_nsec / (1000 * 1000));
95    }
96 
97 #elif defined mx_HAVE_SLEEP
98    if((millis /= 1000) == 0)
99       millis = 1;
100    while((rv = sleep(S(ui,millis))) != 0 && ignint)
101       millis = rv;
102 #else
103 # error Configuration should have detected a function for sleeping.
104 #endif
105    return rv;
106 }
107 
108 int
main(int argc,char ** argv)109 main(int argc, char **argv){
110    char hostbuf[64];
111    struct mx_file_dotlock_info fdi;
112    struct stat stb;
113    sigset_t nset, oset;
114    enum mx_file_dotlock_state fdls;
115 
116    /* We're a dumb helper, ensure as much as we can no one else uses us */
117    if(argc != 12 ||
118          strcmp(argv[ 0], VAL_PS_DOTLOCK) ||
119          (argv[1][0] != 'r' && argv[1][0] != 'w') ||
120          strcmp(argv[ 1] + 1, "dotlock") ||
121          strcmp(argv[ 2], "mailbox") ||
122          strchr(argv[ 3], '/') != NULL /* Seal path injection.. */ ||
123          strcmp(argv[ 4], "name") ||
124          strcmp(argv[ 6], "hostname") ||
125          strcmp(argv[ 8], "randstr") ||
126          strchr(argv[ 9], '/') != NULL /* ..attack vector */ ||
127          strcmp(argv[10], "pollmsecs") ||
128          fstat(STDIN_FILENO, &stb) == -1 || !S_ISFIFO(stb.st_mode) ||
129          fstat(STDOUT_FILENO, &stb) == -1 || !S_ISFIFO(stb.st_mode)){
130 jeuse:
131       fprintf(stderr,
132          "This is a helper program of " VAL_BINDIR "/" VAL_UAGENT ".\n"
133          "  It is capable of gaining more privileges than " VAL_UAGENT "\n"
134          "  and will be used to create lock files.\n"
135          "  The sole purpose is outsourcing of high privileges into\n"
136          "  fewest lines of code in order to reduce attack surface.\n"
137          "  This program cannot be run by itself.\n");
138       exit(n_EXIT_USE);
139    }else{
140       /* Prevent one more path injection attack vector, but be friendly */
141       char const *ccp;
142       uz i;
143       char *cp, c;
144 
145       for(ccp = argv[7], cp = hostbuf, i = 0; (c = *ccp) != '\0'; ++cp, ++ccp){
146          *cp = (c == '/' ? '_' : c);
147          if(++i == sizeof(hostbuf) -1)
148             break;
149       }
150       *cp = '\0';
151       if(cp == hostbuf)
152          goto jeuse;
153       argv[7] = hostbuf;
154    }
155 
156    fdi.fdi_file_name = argv[3];
157    fdi.fdi_lock_name = argv[5];
158    fdi.fdi_hostname = argv[7];
159    fdi.fdi_randstr = argv[9];
160    fdi.fdi_pollmsecs = S(uz,strtoul(argv[11], NULL, 10)); /* XXX SU */
161 
162    /* Ensure the lock name and the file name are identical */
163    /* C99 */{
164       uz i;
165 
166       i = strlen(fdi.fdi_file_name);
167       if(i == 0 || strncmp(fdi.fdi_file_name, fdi.fdi_lock_name, i) ||
168             fdi.fdi_lock_name[i] == '\0' ||
169             strcmp(fdi.fdi_lock_name + i, ".lock"))
170          goto jeuse;
171    }
172 
173    /* Ensure that we got some random string, and some hostname.
174     * a_dotlock_create() will later ensure that it will produce some string
175     * not-equal to .fdi_lock_name if it is called by us */
176    if(fdi.fdi_hostname[0] == '\0' || fdi.fdi_randstr[0] == '\0')
177       goto jeuse;
178 
179    close(STDERR_FILENO);
180 
181    /* In order to prevent stale lock files at all cost block any signals until
182     * we have unlinked the lock file.
183     * It is still not safe because we may be SIGKILLed and may linger around
184     * because we have been SIGSTOPped, but unfortunately the standard doesn't
185     * give any option, e.g. atcrash() or open(O_TEMPORARY_KEEP_NAME) or so, ---
186     * and then again we should not unlink(2) the lock file unless our parent
187     * has finalized the synchronization!  While at it, let me rant about the
188     * default action of realtime signals, program termination */
189    _ign_signal(SIGPIPE); /* (Inherited, though) */
190    sigfillset(&nset);
191    sigdelset(&nset, SIGCONT); /* (Rather redundant, though) */
192    sigprocmask(SIG_BLOCK, &nset, &oset);
193 
194    fdls = mx_FILE_DOTLOCK_STATE_NOPERM | mx_FILE_DOTLOCK_STATE_ABANDON;
195 
196    /* First of all: we only dotlock when the executing user has the necessary
197     * rights to access the mailbox */
198    if(access(fdi.fdi_file_name, (argv[1][0] == 'r' ? R_OK : R_OK | W_OK)))
199       goto jmsg;
200 
201    /* We need UID and GID information about the mailbox to lock */
202    if(stat(fdi.fdi_file_name, fdi.fdi_stb = &stb) == -1)
203       goto jmsg;
204 
205    fdls = mx_FILE_DOTLOCK_STATE_PRIVFAILED | mx_FILE_DOTLOCK_STATE_ABANDON;
206 
207    /* We are SETUID and do not want to become traced or being attached to */
208 #if defined mx_HAVE_PRCTL_DUMPABLE
209    if(prctl(PR_SET_DUMPABLE, 0))
210       goto jmsg;
211 #elif defined mx_HAVE_PROCCTL_TRACE_CTL
212    /* C99 */{
213       int disable_trace = PROC_TRACE_CTL_DISABLE;
214 
215       if(procctl(P_PID, getpid(), PROC_TRACE_CTL, &disable_trace) == -1)
216          goto jmsg;
217    }
218 #elif defined mx_HAVE_PTRACE_DENY
219    if(ptrace(PT_DENY_ATTACH, 0, 0, 0) == -1)
220       goto jmsg;
221 #elif defined mx_HAVE_SETPFLAGS_PROTECT
222    if(setpflags(__PROC_PROTECT, 1))
223       goto jmsg;
224 #endif
225 
226    /* This helper is only executed when really needed, it thus doesn't make
227     * sense to try to continue with initial privileges */
228    if(setuid(geteuid()))
229       goto jmsg;
230 
231    fdls = a_file_lock_dotlock_create(&fdi);
232 
233    /* Finally: notify our parent about the actual lock state.. */
234 jmsg:
235    write(STDOUT_FILENO, &fdls, sizeof fdls);
236    close(STDOUT_FILENO);
237 
238    /* ..then eventually wait until we shall remove the lock again, which will
239     * be notified via the read returning */
240    if(fdls == mx_FILE_DOTLOCK_STATE_NONE){
241       read(STDIN_FILENO, &fdls, sizeof fdls);
242 
243       unlink(fdi.fdi_lock_name);
244    }
245 
246    sigprocmask(SIG_SETMASK, &oset, NULL);
247    return (fdls == mx_FILE_DOTLOCK_STATE_NONE ? n_EXIT_OK : n_EXIT_ERR);
248 }
249 
250 #include "su/code-ou.h"
251 /* s-it-mode */
252