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