README: Add "Security record" section
[s-mailx.git] / dotlock.c
blob364f981533e98b0acffbd01cb08a615140d7b47e
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ n_dotlock(): creation of an exclusive "dotlock" file.
4 * Copyright (c) 2016 - 2017 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
5 */
6 #undef n_FILE
7 #define n_FILE dotlock
9 #ifndef HAVE_AMALGAMATION
10 # include "nail.h"
11 #endif
13 #include <sys/wait.h>
15 #ifdef HAVE_DOTLOCK
16 # include "dotlock.h"
17 #endif
19 /* XXX Our Popen() main() takes void, temporary global data store */
20 #ifdef HAVE_DOTLOCK
21 static enum n_file_lock_type a_dotlock_flt;
22 static int a_dotlock_fd;
23 struct n_dotlock_info *a_dotlock_dip;
24 #endif
26 /* main() of fork(2)ed dot file locker */
27 #ifdef HAVE_DOTLOCK
28 static int a_dotlock_main(void);
29 #endif
31 #ifdef HAVE_DOTLOCK
32 static int
33 a_dotlock_main(void){
34 /* Use PATH_MAX not NAME_MAX to catch those "we proclaim the minimum value"
35 * problems (SunOS), since the pathconf(3) value comes too late! */
36 char name[PATH_MAX +1];
37 struct n_dotlock_info di;
38 struct stat stb, fdstb;
39 enum n_dotlock_state dls;
40 char const *cp;
41 int fd;
42 enum n_file_lock_type flt;
43 NYD_ENTER;
45 /* Ignore SIGPIPE, we'll see EPIPE and "fall through" */
46 safe_signal(SIGPIPE, SIG_IGN);
48 /* Get the arguments "passed to us" */
49 flt = a_dotlock_flt;
50 fd = a_dotlock_fd;
51 n_UNUSED(fd);
52 di = *a_dotlock_dip;
54 /* chdir(2)? */
55 jislink:
56 dls = n_DLS_CANT_CHDIR | n_DLS_ABANDON;
58 if((cp = strrchr(di.di_file_name, '/')) != NULL){
59 char const *fname = cp + 1;
61 while(PTRCMP(cp - 1, >, di.di_file_name) && cp[-1] == '/')
62 --cp;
63 cp = savestrbuf(di.di_file_name, PTR2SIZE(cp - di.di_file_name));
64 if(chdir(cp))
65 goto jmsg;
67 di.di_file_name = fname;
70 /* So we're here, but then again the file can be a symbolic link!
71 * This is however only true if we do not have realpath(3) available since
72 * that'll have resolved the path already otherwise; nonetheless, let
73 * readlink(2) be a precondition for dotlocking and keep this code */
74 if(lstat(cp = di.di_file_name, &stb) == -1)
75 goto jmsg;
76 if(S_ISLNK(stb.st_mode)){
77 /* Use salloc() and hope we stay in builtin buffer.. */
78 char *x;
79 size_t i;
80 ssize_t sr;
82 for(x = NULL, i = PATH_MAX;; i += PATH_MAX){
83 x = salloc(i +1);
84 sr = readlink(cp, x, i);
85 if(sr <= 0){
86 dls = n_DLS_FISHY | n_DLS_ABANDON;
87 goto jmsg;
89 if(UICMP(z, sr, <, i)){
90 x[sr] = '\0';
91 i = (size_t)sr;
92 break;
95 di.di_file_name = x;
96 goto jislink;
99 dls = n_DLS_FISHY | n_DLS_ABANDON;
101 /* Bail out if the file has changed its identity in the meanwhile */
102 if(fstat(fd, &fdstb) == -1 ||
103 fdstb.st_dev != stb.st_dev || fdstb.st_ino != stb.st_ino ||
104 fdstb.st_uid != stb.st_uid || fdstb.st_gid != stb.st_gid ||
105 fdstb.st_mode != stb.st_mode)
106 goto jmsg;
108 /* Be aware, even if the error is false! Note the shared code in dotlock.h
109 * *requires* that it is possible to create a filename at least one byte
110 * longer than di_lock_name! */
111 do/* while(0) breaker */{
112 # ifdef HAVE_PATHCONF
113 long pc;
114 # endif
115 int i = snprintf(name, sizeof name, "%s.lock", di.di_file_name);
117 if(i < 0 || UICMP(32, i, >=, sizeof name)){
118 jenametool:
119 dls = n_DLS_NAMETOOLONG | n_DLS_ABANDON;
120 goto jmsg;
123 /* fd is a file, not portable to use for _PC_NAME_MAX */
124 # ifdef HAVE_PATHCONF
125 errno = 0;
126 if((pc = pathconf(".", _PC_NAME_MAX)) == -1){
127 /* errno unchanged: no limit */
128 if(errno == 0)
129 break;
130 # endif
131 if(UICMP(z, NAME_MAX - 1, <, i))
132 goto jenametool;
133 # ifdef HAVE_PATHCONF
134 }else if(pc - 1 >= (long)i)
135 break;
136 else
137 goto jenametool;
138 # endif
139 }while(0);
141 di.di_lock_name = name;
143 /* We are in the directory of the mailbox for which we have to create
144 * a dotlock file for. Any symbolic links have been resolved.
145 * We don't know whether we have realpath(3) available,and manually
146 * resolving the path is due especially given that S-nail supports the
147 * special "%:" syntax to warp any file into a "system mailbox"; there may
148 * also be multiple system mailbox directories...
149 * So what we do is that we fstat(2) the mailbox and check its UID and
150 * GID against that of our own process: if any of those mismatch we must
151 * either assume a directory we are not allowed to write in, or that we run
152 * via -u/$USER/%USER as someone else, in which case we favour our
153 * privilege-separated dotlock process */
154 assert(cp != NULL); /* Ugly: avoid a useless var and reuse that one */
155 if(access(".", W_OK)){
156 /* This may however also indicate a read-only filesystem, which is not
157 * really an error from our point of view since the mailbox will degrade
158 * to a readonly one for which no dotlock is needed, then, and errors
159 * may arise only due to actions which require box modifications */
160 if(errno == EROFS){
161 dls = n_DLS_ROFS | n_DLS_ABANDON;
162 goto jmsg;
164 cp = NULL;
166 if(cp == NULL || stb.st_uid != n_user_id || stb.st_gid != n_group_id){
167 char itoabuf[64];
168 char const *args[13];
170 snprintf(itoabuf, sizeof itoabuf, "%" PRIuZ, di.di_pollmsecs);
171 args[ 0] = VAL_PRIVSEP;
172 args[ 1] = (flt == FLT_READ ? "rdotlock" : "wdotlock");
173 args[ 2] = "mailbox"; args[ 3] = di.di_file_name;
174 args[ 4] = "name"; args[ 5] = di.di_lock_name;
175 args[ 6] = "hostname"; args[ 7] = di.di_hostname;
176 args[ 8] = "randstr"; args[ 9] = di.di_randstr;
177 args[10] = "pollmsecs"; args[11] = itoabuf;
178 args[12] = NULL;
179 execv(VAL_LIBEXECDIR "/" VAL_UAGENT "-privsep", n_UNCONST(args));
181 dls = n_DLS_NOEXEC;
182 write(STDOUT_FILENO, &dls, sizeof dls);
183 /* But fall through and try it with normal privileges! */
186 /* So let's try and call it ourselfs! Note that we don't block signals just
187 * like our privsep child does, the user will anyway be able to remove his
188 * file again, and if we're in -u/$USER mode then we are allowed to access
189 * the user's box: shall we leave behind a stale dotlock then at least we
190 * start a friendly human conversation. Since we cannot handle SIGKILL and
191 * SIGSTOP malicious things could happen whatever we do */
192 safe_signal(SIGHUP, SIG_IGN);
193 safe_signal(SIGINT, SIG_IGN);
194 safe_signal(SIGQUIT, SIG_IGN);
195 safe_signal(SIGTERM, SIG_IGN);
197 NYD;
198 dls = a_dotlock_create(&di);
199 NYD;
201 /* Finally: notify our parent about the actual lock state.. */
202 jmsg:
203 write(STDOUT_FILENO, &dls, sizeof dls);
204 close(STDOUT_FILENO);
206 /* ..then eventually wait until we shall remove the lock again, which will
207 * be notified via the read returning */
208 if(dls == n_DLS_NONE){
209 read(STDIN_FILENO, &dls, sizeof dls);
211 unlink(name);
213 NYD_LEAVE;
214 return n_EXIT_OK;
216 #endif /* HAVE_DOTLOCK */
218 FL FILE *
219 n_dotlock(char const *fname, int fd, enum n_file_lock_type flt,
220 off_t off, off_t len, size_t pollmsecs){
221 #undef _DOMSG
222 #ifdef HAVE_DOTLOCK
223 # define _DOMSG() \
224 n_err(_("Creating dotlock for %s "), n_shexp_quote_cp(fname, FAL0))
225 #else
226 # define _DOMSG() \
227 n_err(_("Trying to lock file %s "), n_shexp_quote_cp(fname, FAL0))
228 #endif
230 #ifdef HAVE_DOTLOCK
231 int cpipe[2];
232 struct n_dotlock_info di;
233 enum n_dotlock_state dls;
234 char const *emsg;
235 #endif
236 int serrno;
237 union {size_t tries; int (*ptf)(void); char const *sh; ssize_t r;} u;
238 bool_t flocked, didmsg;
239 FILE *rv;
240 NYD_ENTER;
242 if(pollmsecs == UIZ_MAX)
243 pollmsecs = FILE_LOCK_MILLIS;
245 rv = NULL;
246 didmsg = FAL0;
247 n_UNINIT(serrno, 0);
248 #ifdef HAVE_DOTLOCK
249 emsg = NULL;
250 #endif
252 if(n_poption & n_PO_D_VV){
253 _DOMSG();
254 didmsg = TRUM1;
257 flocked = FAL0;
258 for(u.tries = 0; !n_file_lock(fd, flt, off, len, 0);)
259 switch((serrno = errno)){
260 case EACCES:
261 case EAGAIN:
262 case ENOLCK:
263 if(pollmsecs > 0 && ++u.tries < FILE_LOCK_TRIES){
264 if(!didmsg)
265 _DOMSG();
266 n_err(".");
267 didmsg = TRUM1;
268 n_msleep(pollmsecs, FAL0);
269 continue;
271 /* FALLTHRU */
272 default:
273 goto jleave;
275 flocked = TRU1;
277 #ifndef HAVE_DOTLOCK
278 jleave:
279 if(didmsg == TRUM1)
280 n_err("\n");
281 if(flocked)
282 rv = (FILE*)-1;
283 else
284 errno = serrno;
285 NYD_LEAVE;
286 return rv;
288 #else
289 /* Create control-pipe for our dot file locker process, which will remove
290 * the lock and terminate once the pipe is closed, for whatever reason */
291 if(pipe_cloexec(cpipe) == -1){
292 serrno = errno;
293 emsg = N_(" Can't create dotlock file control pipe\n");
294 goto jemsg;
297 /* And the locker process itself; it'll be a (rather cheap) thread only
298 * unless the lock has to be placed in the system spool and we have our
299 * privilege-separated dotlock program available, in which case that will be
300 * executed and do "it" */
301 di.di_file_name = fname;
302 di.di_pollmsecs = pollmsecs;
303 /* Initialize some more stuff; query the two strings in the parent in order
304 * to cache the result of the former and anyway minimalize child page-ins.
305 * Especially uname(3) may hang for multiple seconds when it is called the
306 * first time! */
307 di.di_hostname = nodename(FAL0);
308 di.di_randstr = getrandstring(16);
309 a_dotlock_flt = flt;
310 a_dotlock_fd = fd;
311 a_dotlock_dip = &di;
313 u.ptf = &a_dotlock_main;
314 rv = Popen((char*)-1, "W", u.sh, NULL, cpipe[1]);
315 serrno = errno;
317 close(cpipe[1]);
318 if(rv == NULL){
319 close(cpipe[0]);
320 emsg = N_(" Can't create file lock process\n");
321 goto jemsg;
324 /* Let's check whether we were able to create the dotlock file */
325 for(;;){
326 u.r = read(cpipe[0], &dls, sizeof dls);
327 if(UICMP(z, u.r, !=, sizeof dls)){
328 serrno = (u.r != -1) ? EAGAIN : errno;
329 dls = n_DLS_DUNNO | n_DLS_ABANDON;
330 }else
331 serrno = 0;
333 if(dls == n_DLS_NONE || (dls & n_DLS_ABANDON))
334 close(cpipe[0]);
336 switch(dls & ~n_DLS_ABANDON){
337 case n_DLS_NONE:
338 goto jleave;
339 case n_DLS_CANT_CHDIR:
340 if(n_poption & n_PO_D_V)
341 emsg = N_(" Can't change directory! Please check permissions\n");
342 serrno = EACCES;
343 break;
344 case n_DLS_NAMETOOLONG:
345 emsg = N_("Resulting dotlock filename would be too long\n");
346 serrno = EACCES;
347 break;
348 case n_DLS_ROFS:
349 assert(dls & n_DLS_ABANDON);
350 if(n_poption & n_PO_D_V)
351 emsg = N_(" Read-only filesystem, not creating lock file\n");
352 serrno = EROFS;
353 break;
354 case n_DLS_NOPERM:
355 if(n_poption & n_PO_D_V)
356 emsg = N_(" Can't create a dotlock file, "
357 "please check permissions\n"
358 " (Or ignore by setting *dotlock-ignore-error* variable)\n");
359 serrno = EACCES;
360 break;
361 case n_DLS_NOEXEC:
362 if(n_poption & n_PO_D_V)
363 emsg = N_(" Can't find privilege-separated dotlock program\n");
364 serrno = ENOENT;
365 break;
366 case n_DLS_PRIVFAILED:
367 emsg = N_(" Privilege-separated dotlock program can't change "
368 "privileges\n");
369 serrno = EPERM;
370 break;
371 case n_DLS_EXIST:
372 emsg = N_(" It seems there is a stale dotlock file?\n"
373 " Please remove the lock file manually, then retry\n");
374 serrno = EEXIST;
375 break;
376 case n_DLS_FISHY:
377 emsg = N_(" Fishy! Is someone trying to \"steal\" foreign files?\n"
378 " Please check the mailbox file etc. manually, then retry\n");
379 serrno = EAGAIN; /* ? Hack to ignore *dotlock-ignore-error* xxx */
380 break;
381 default:
382 case n_DLS_DUNNO:
383 emsg = N_(" Unspecified dotlock file control process error.\n"
384 " Like broken I/O pipe; this one is unlikely to happen\n");
385 if(serrno != EAGAIN)
386 serrno = EINVAL;
387 break;
388 case n_DLS_PING:
389 if(!didmsg)
390 _DOMSG();
391 n_err(".");
392 didmsg = TRUM1;
393 continue;
396 if(emsg != NULL){
397 if(!didmsg){
398 _DOMSG();
399 didmsg = TRUM1;
401 if(didmsg == TRUM1)
402 n_err(_(". failed\n"));
403 didmsg = TRU1;
404 n_err(V_(emsg));
405 emsg = NULL;
408 if(dls & n_DLS_ABANDON){
409 Pclose(rv, FAL0);
410 rv = NULL;
411 break;
415 jleave:
416 if(didmsg == TRUM1)
417 n_err(". %s\n", (rv != NULL ? _("ok") : _("failed")));
418 if(rv == NULL) {
419 if(flocked){
420 if(serrno == EROFS)
421 rv = (FILE*)-1;
422 else if(serrno != EAGAIN && serrno != EEXIST &&
423 ok_blook(dotlock_ignore_error)){
424 if(n_poption & n_PO_D_V)
425 n_err(_(" *dotlock-ignore-error* set: continuing\n"));
426 rv = (FILE*)-1;
427 }else
428 goto jserrno;
429 }else
430 jserrno:
431 errno = serrno;
433 NYD_LEAVE;
434 return rv;
435 jemsg:
436 if(!didmsg)
437 _DOMSG();
438 n_err("\n");
439 didmsg = TRU1;
440 n_err(V_(emsg));
441 goto jleave;
442 #endif /* HAVE_DOTLOCK */
443 #undef _DOMSG
446 /* s-it-mode */