make-config.in: complete path (leftover of [807f64e2], 2015-12-26!)
[s-mailx.git] / dotlock.c
blob3a8f29f8cc157da17ff0d0eb3e11d6e6aaec84f5
1 /*@ S-nail - a mail user agent derived from Berkeley Mail.
2 *@ n_dotlock(): creation of an exclusive "dotlock" file.
4 * Copyright (c) 2016 - 2018 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
5 * SPDX-License-Identifier: ISC
7 * Permission to use, copy, modify, and/or distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
11 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
17 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19 #undef n_FILE
20 #define n_FILE dotlock
22 #ifndef HAVE_AMALGAMATION
23 # include "nail.h"
24 #endif
26 #ifdef HAVE_DOTLOCK
27 # include "dotlock.h"
28 #endif
30 /* XXX Our Popen() main() takes void, temporary global data store */
31 #ifdef HAVE_DOTLOCK
32 static enum n_file_lock_type a_dotlock_flt;
33 static int a_dotlock_fd;
34 struct n_dotlock_info *a_dotlock_dip;
35 #endif
37 /* main() of fork(2)ed dot file locker */
38 #ifdef HAVE_DOTLOCK
39 static int a_dotlock_main(void);
40 #endif
42 #ifdef HAVE_DOTLOCK
43 static int
44 a_dotlock_main(void){
45 /* Use PATH_MAX not NAME_MAX to catch those "we proclaim the minimum value"
46 * problems (SunOS), since the pathconf(3) value comes too late! */
47 char name[PATH_MAX +1];
48 struct n_dotlock_info di;
49 struct stat stb, fdstb;
50 enum n_dotlock_state dls;
51 char const *cp;
52 int fd;
53 enum n_file_lock_type flt;
54 NYD_ENTER;
56 /* Ignore SIGPIPE, we will see n_ERR_PIPE and "fall through" */
57 safe_signal(SIGPIPE, SIG_IGN);
59 /* Get the arguments "passed to us" */
60 flt = a_dotlock_flt;
61 fd = a_dotlock_fd;
62 n_UNUSED(fd);
63 di = *a_dotlock_dip;
65 /* chdir(2)? */
66 jislink:
67 dls = n_DLS_CANT_CHDIR | n_DLS_ABANDON;
69 if((cp = strrchr(di.di_file_name, '/')) != NULL){
70 char const *fname = cp + 1;
72 while(PTRCMP(cp - 1, >, di.di_file_name) && cp[-1] == '/')
73 --cp;
74 cp = savestrbuf(di.di_file_name, PTR2SIZE(cp - di.di_file_name));
75 if(chdir(cp))
76 goto jmsg;
78 di.di_file_name = fname;
81 /* So we are here, but then again the file can be a symbolic link!
82 * This is however only true if we do not have realpath(3) available since
83 * that will have resolved the path already otherwise; nonetheless, let
84 * readlink(2) be a precondition for dotlocking and keep this code */
85 if(lstat(cp = di.di_file_name, &stb) == -1)
86 goto jmsg;
87 if(S_ISLNK(stb.st_mode)){
88 /* Use n_autorec_alloc() and hope we stay in built-in buffer.. */
89 char *x;
90 size_t i;
91 ssize_t sr;
93 for(x = NULL, i = PATH_MAX;; i += PATH_MAX){
94 x = n_autorec_alloc(i +1);
95 sr = readlink(cp, x, i);
96 if(sr <= 0){
97 dls = n_DLS_FISHY | n_DLS_ABANDON;
98 goto jmsg;
100 if(UICMP(z, sr, <, i)){
101 x[sr] = '\0';
102 break;
105 di.di_file_name = x;
106 goto jislink;
109 dls = n_DLS_FISHY | n_DLS_ABANDON;
111 /* Bail out if the file has changed its identity in the meanwhile */
112 if(fstat(fd, &fdstb) == -1 ||
113 fdstb.st_dev != stb.st_dev || fdstb.st_ino != stb.st_ino ||
114 fdstb.st_uid != stb.st_uid || fdstb.st_gid != stb.st_gid ||
115 fdstb.st_mode != stb.st_mode)
116 goto jmsg;
118 /* Be aware, even if the error is false! Note the shared code in dotlock.h
119 * *requires* that it is possible to create a filename at least one byte
120 * longer than di_lock_name! */
121 do/* while(0) breaker */{
122 # ifdef HAVE_PATHCONF
123 long pc;
124 # endif
125 int i;
127 i = snprintf(name, sizeof name, "%s.lock", di.di_file_name);
128 if(i < 0 || UICMP(32, i, >=, sizeof name)){
129 jenametool:
130 dls = n_DLS_NAMETOOLONG | n_DLS_ABANDON;
131 goto jmsg;
134 /* fd is a file, not portable to use for _PC_NAME_MAX */
135 # ifdef HAVE_PATHCONF
136 n_err_no = n_ERR_NONE;
137 if((pc = pathconf(".", _PC_NAME_MAX)) == -1){
138 /* n_err_no unchanged: no limit */
139 if(n_err_no == 0)
140 break;
141 # endif
142 if(UICMP(z, NAME_MAX - 1, <, i))
143 goto jenametool;
144 # ifdef HAVE_PATHCONF
145 }else if(pc - 1 >= i)
146 break;
147 else
148 goto jenametool;
149 # endif
150 }while(0);
152 di.di_lock_name = name;
154 /* We are in the directory of the mailbox for which we have to create
155 * a dotlock file for. Any symbolic links have been resolved.
156 * We do not know whether we have realpath(3) available,and manually
157 * resolving the path is due especially given that S-nail supports the
158 * special "%:" syntax to warp any file into a "system mailbox"; there may
159 * also be multiple system mailbox directories...
160 * So what we do is that we fstat(2) the mailbox and check its UID and
161 * GID against that of our own process: if any of those mismatch we must
162 * either assume a directory we are not allowed to write in, or that we run
163 * via -u/$USER/%USER as someone else, in which case we favour our
164 * privilege-separated dotlock process */
165 assert(cp != NULL); /* Ugly: avoid a useless var and reuse that one */
166 if(access(".", W_OK)){
167 /* This may however also indicate a read-only filesystem, which is not
168 * really an error from our point of view since the mailbox will degrade
169 * to a readonly one for which no dotlock is needed, then, and errors
170 * may arise only due to actions which require box modifications */
171 if(n_err_no == n_ERR_ROFS){
172 dls = n_DLS_ROFS | n_DLS_ABANDON;
173 goto jmsg;
175 cp = NULL;
177 if(cp == NULL || stb.st_uid != n_user_id || stb.st_gid != n_group_id){
178 char itoabuf[64];
179 char const *args[13];
181 snprintf(itoabuf, sizeof itoabuf, "%" PRIuZ, di.di_pollmsecs);
182 args[ 0] = VAL_PRIVSEP;
183 args[ 1] = (flt == FLT_READ ? "rdotlock" : "wdotlock");
184 args[ 2] = "mailbox"; args[ 3] = di.di_file_name;
185 args[ 4] = "name"; args[ 5] = di.di_lock_name;
186 args[ 6] = "hostname"; args[ 7] = di.di_hostname;
187 args[ 8] = "randstr"; args[ 9] = di.di_randstr;
188 args[10] = "pollmsecs"; args[11] = itoabuf;
189 args[12] = NULL;
190 execv(VAL_LIBEXECDIR "/" VAL_UAGENT "-privsep", n_UNCONST(args));
192 dls = n_DLS_NOEXEC;
193 write(STDOUT_FILENO, &dls, sizeof dls);
194 /* But fall through and try it with normal privileges! */
197 /* So let's try and call it ourselfs! Note we do not block signals just
198 * like our privsep child does, the user will anyway be able to remove his
199 * file again, and if we are in -u/$USER mode then we are allowed to access
200 * the user's box: shall we leave behind a stale dotlock then at least we
201 * start a friendly human conversation. Since we cannot handle SIGKILL and
202 * SIGSTOP malicious things could happen whatever we do */
203 safe_signal(SIGHUP, SIG_IGN);
204 safe_signal(SIGINT, SIG_IGN);
205 safe_signal(SIGQUIT, SIG_IGN);
206 safe_signal(SIGTERM, SIG_IGN);
208 NYD;
209 dls = a_dotlock_create(&di);
210 NYD;
212 /* Finally: notify our parent about the actual lock state.. */
213 jmsg:
214 write(STDOUT_FILENO, &dls, sizeof dls);
215 close(STDOUT_FILENO);
217 /* ..then eventually wait until we shall remove the lock again, which will
218 * be notified via the read returning */
219 if(dls == n_DLS_NONE){
220 read(STDIN_FILENO, &dls, sizeof dls);
222 unlink(name);
224 NYD_LEAVE;
225 return n_EXIT_OK;
227 #endif /* HAVE_DOTLOCK */
229 FL FILE *
230 n_dotlock(char const *fname, int fd, enum n_file_lock_type flt,
231 off_t off, off_t len, size_t pollmsecs){
232 #undef _DOMSG
233 #define _DOMSG() \
234 n_err(_("Creating file lock for %s "), n_shexp_quote_cp(fname, FAL0))
236 #ifdef HAVE_DOTLOCK
237 int cpipe[2];
238 struct n_dotlock_info di;
239 enum n_dotlock_state dls;
240 char const *emsg;
241 #endif
242 int serr;
243 union {size_t tries; int (*ptf)(void); char const *sh; ssize_t r;} u;
244 bool_t flocked, didmsg;
245 FILE *rv;
246 NYD_ENTER;
248 if(pollmsecs == UIZ_MAX)
249 pollmsecs = FILE_LOCK_MILLIS;
251 rv = NULL;
252 didmsg = FAL0;
253 n_UNINIT(serr, 0);
254 #ifdef HAVE_DOTLOCK
255 emsg = NULL;
256 #endif
258 if(n_poption & n_PO_D_VV){
259 _DOMSG();
260 didmsg = TRUM1;
263 flocked = FAL0;
264 for(u.tries = 0; !n_file_lock(fd, flt, off, len, 0);)
265 switch((serr = n_err_no)){
266 case n_ERR_ACCES:
267 case n_ERR_AGAIN:
268 case n_ERR_NOLCK:
269 if(pollmsecs > 0 && ++u.tries < FILE_LOCK_TRIES){
270 if(!didmsg)
271 _DOMSG();
272 n_err(".");
273 didmsg = TRUM1;
274 n_msleep(pollmsecs, FAL0);
275 continue;
277 /* FALLTHRU */
278 default:
279 goto jleave;
281 flocked = TRU1;
283 #ifndef HAVE_DOTLOCK
284 jleave:
285 if(didmsg == TRUM1)
286 n_err("\n");
287 if(flocked)
288 rv = (FILE*)-1;
289 else
290 n_err_no = serr;
291 NYD_LEAVE;
292 return rv;
294 #else
295 if(ok_blook(dotlock_disable)){
296 rv = (FILE*)-1;
297 goto jleave;
300 /* Create control-pipe for our dot file locker process, which will remove
301 * the lock and terminate once the pipe is closed, for whatever reason */
302 if(pipe_cloexec(cpipe) == -1){
303 serr = n_err_no;
304 emsg = N_(" Cannot create dotlock file control pipe\n");
305 goto jemsg;
308 /* And the locker process itself; it will be a (rather cheap) thread only
309 * unless the lock has to be placed in the system spool and we have our
310 * privilege-separated dotlock program available, in which case that will be
311 * executed and do "it" */
312 di.di_file_name = fname;
313 di.di_pollmsecs = pollmsecs;
314 /* Initialize some more stuff; query the two strings in the parent in order
315 * to cache the result of the former and anyway minimalize child page-ins.
316 * Especially uname(3) may hang for multiple seconds when it is called the
317 * first time! */
318 di.di_hostname = n_nodename(FAL0);
319 di.di_randstr = n_random_create_cp(16, NULL);
320 a_dotlock_flt = flt;
321 a_dotlock_fd = fd;
322 a_dotlock_dip = &di;
324 u.ptf = &a_dotlock_main;
325 rv = Popen((char*)-1, "W", u.sh, NULL, cpipe[1]);
326 serr = n_err_no;
328 close(cpipe[1]);
329 if(rv == NULL){
330 close(cpipe[0]);
331 emsg = N_(" Cannot create file lock process\n");
332 goto jemsg;
335 /* Let's check whether we were able to create the dotlock file */
336 for(;;){
337 u.r = read(cpipe[0], &dls, sizeof dls);
338 if(UICMP(z, u.r, !=, sizeof dls)){
339 serr = (u.r != -1) ? n_ERR_AGAIN : n_err_no;
340 dls = n_DLS_DUNNO | n_DLS_ABANDON;
341 }else
342 serr = n_ERR_NONE;
344 if(dls == n_DLS_NONE || (dls & n_DLS_ABANDON))
345 close(cpipe[0]);
347 switch(dls & ~n_DLS_ABANDON){
348 case n_DLS_NONE:
349 goto jleave;
350 case n_DLS_CANT_CHDIR:
351 if(n_poption & n_PO_D_V)
352 emsg = N_(" Cannot change directory, please check permissions\n");
353 serr = n_ERR_ACCES;
354 break;
355 case n_DLS_NAMETOOLONG:
356 emsg = N_("Resulting dotlock filename would be too long\n");
357 serr = n_ERR_ACCES;
358 break;
359 case n_DLS_ROFS:
360 assert(dls & n_DLS_ABANDON);
361 if(n_poption & n_PO_D_V)
362 emsg = N_(" Read-only filesystem, not creating lock file\n");
363 serr = n_ERR_ROFS;
364 break;
365 case n_DLS_NOPERM:
366 if((n_psonce & n_PSO_INTERACTIVE) || (n_poption & n_PO_D_V))
367 emsg = N_(" Cannot create a dotlock file, "
368 "please check permissions\n"
369 " (Or set *dotlock-disable*, then try again)\n");
370 serr = n_ERR_ACCES;
371 break;
372 case n_DLS_NOEXEC:
373 if((n_psonce & (n_PSO_INTERACTIVE | n_PSO_DOTLOCK_PRIVSEP_NOTED)
374 ) == n_PSO_INTERACTIVE || (n_poption & n_PO_D_V)){
375 n_psonce |= n_PSO_DOTLOCK_PRIVSEP_NOTED;
376 emsg = N_(" Cannot find privilege-separated dotlock program\n");
378 serr = n_ERR_NOENT;
379 break;
380 case n_DLS_PRIVFAILED:
381 emsg = N_(" Privilege-separated dotlock program cannot change "
382 "privileges\n");
383 serr = n_ERR_PERM;
384 break;
385 case n_DLS_EXIST:
386 emsg = N_(" It seems there is a stale dotlock file?\n"
387 " Please remove the lock file manually, then retry\n");
388 serr = n_ERR_EXIST;
389 break;
390 case n_DLS_FISHY:
391 emsg = N_(" Fishy! Is someone trying to \"steal\" foreign files?\n"
392 " Please check the mailbox file etc. manually, then retry\n");
393 serr = n_ERR_AGAIN; /* ? Hack to ignore *dotlock-ignore-error* xxx */
394 break;
395 default:
396 case n_DLS_DUNNO:
397 emsg = N_(" Unspecified dotlock file control process error.\n"
398 " Like broken I/O pipe; this one is unlikely to happen\n");
399 if(serr != n_ERR_AGAIN)
400 serr = n_ERR_INVAL;
401 break;
402 case n_DLS_PING:
403 if(!didmsg)
404 _DOMSG();
405 n_err(".");
406 didmsg = TRUM1;
407 continue;
410 if(emsg != NULL){
411 if(!didmsg){
412 _DOMSG();
413 didmsg = TRUM1;
415 if(didmsg == TRUM1)
416 n_err(_(". failed\n"));
417 didmsg = TRU1;
418 n_err(V_(emsg));
419 emsg = NULL;
422 if(dls & n_DLS_ABANDON){
423 Pclose(rv, FAL0);
424 rv = NULL;
425 break;
429 jleave:
430 if(didmsg == TRUM1)
431 n_err(". %s\n", (rv != NULL ? _("ok") : _("failed")));
432 if(rv == NULL) {
433 if(flocked){
434 if(serr == n_ERR_ROFS)
435 rv = (FILE*)-1;
436 else if(serr != n_ERR_AGAIN && serr != n_ERR_EXIST &&
437 ok_blook(dotlock_ignore_error)){
438 n_OBSOLETE(_("*dotlock-ignore-error*: please use "
439 "*dotlock-disable* instead"));
440 if(n_poption & n_PO_D_V)
441 n_err(_(" *dotlock-ignore-error* set: continuing\n"));
442 rv = (FILE*)-1;
443 }else
444 goto jserr;
445 }else
446 jserr:
447 n_err_no = serr;
449 NYD_LEAVE;
450 return rv;
451 jemsg:
452 if(!didmsg)
453 _DOMSG();
454 n_err("\n");
455 didmsg = TRU1;
456 n_err(V_(emsg));
457 goto jleave;
458 #endif /* HAVE_DOTLOCK */
459 #undef _DOMSG
462 /* s-it-mode */