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.
20 #define n_FILE dotlock
22 #ifndef HAVE_AMALGAMATION
30 /* XXX Our Popen() main() takes void, temporary global data store */
32 static enum n_file_lock_type a_dotlock_flt
;
33 static int a_dotlock_fd
;
34 struct n_dotlock_info
*a_dotlock_dip
;
37 /* main() of fork(2)ed dot file locker */
39 static int 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
;
53 enum n_file_lock_type flt
;
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" */
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] == '/')
74 cp
= savestrbuf(di
.di_file_name
, PTR2SIZE(cp
- di
.di_file_name
));
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)
87 if(S_ISLNK(stb
.st_mode
)){
88 /* Use n_autorec_alloc() and hope we stay in built-in buffer.. */
93 for(x
= NULL
, i
= PATH_MAX
;; i
+= PATH_MAX
){
94 x
= n_autorec_alloc(i
+1);
95 sr
= readlink(cp
, x
, i
);
97 dls
= n_DLS_FISHY
| n_DLS_ABANDON
;
100 if(UICMP(z
, sr
, <, i
)){
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
)
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
127 i
= snprintf(name
, sizeof name
, "%s.lock", di
.di_file_name
);
128 if(i
< 0 || UICMP(32, i
, >=, sizeof name
)){
130 dls
= n_DLS_NAMETOOLONG
| n_DLS_ABANDON
;
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 */
142 if(UICMP(z
, NAME_MAX
- 1, <, i
))
144 # ifdef HAVE_PATHCONF
145 }else if(pc
- 1 >= i
)
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
;
177 if(cp
== NULL
|| stb
.st_uid
!= n_user_id
|| stb
.st_gid
!= n_group_id
){
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
;
190 execv(VAL_LIBEXECDIR
"/" VAL_UAGENT
"-privsep", n_UNCONST(args
));
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
);
209 dls
= a_dotlock_create(&di
);
212 /* Finally: notify our parent about the actual lock state.. */
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
);
227 #endif /* HAVE_DOTLOCK */
230 n_dotlock(char const *fname
, int fd
, enum n_file_lock_type flt
,
231 off_t off
, off_t len
, size_t pollmsecs
){
234 n_err(_("Creating file lock for %s "), n_shexp_quote_cp(fname, FAL0))
238 struct n_dotlock_info di
;
239 enum n_dotlock_state dls
;
243 union {size_t tries
; int (*ptf
)(void); char const *sh
; ssize_t r
;} u
;
244 bool_t flocked
, didmsg
;
248 if(pollmsecs
== UIZ_MAX
)
249 pollmsecs
= FILE_LOCK_MILLIS
;
258 if(n_poption
& n_PO_D_VV
){
264 for(u
.tries
= 0; !n_file_lock(fd
, flt
, off
, len
, 0);)
265 switch((serr
= n_err_no
)){
269 if(pollmsecs
> 0 && ++u
.tries
< FILE_LOCK_TRIES
){
274 n_msleep(pollmsecs
, FAL0
);
295 if(ok_blook(dotlock_disable
)){
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){
304 emsg
= N_(" Cannot create dotlock file control pipe\n");
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
318 di
.di_hostname
= n_nodename(FAL0
);
319 di
.di_randstr
= n_random_create_cp(16, NULL
);
324 u
.ptf
= &a_dotlock_main
;
325 rv
= Popen((char*)-1, "W", u
.sh
, NULL
, cpipe
[1]);
331 emsg
= N_(" Cannot create file lock process\n");
335 /* Let's check whether we were able to create the dotlock file */
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
;
344 if(dls
== n_DLS_NONE
|| (dls
& n_DLS_ABANDON
))
347 switch(dls
& ~n_DLS_ABANDON
){
350 case n_DLS_CANT_CHDIR
:
351 if(n_poption
& n_PO_D_V
)
352 emsg
= N_(" Cannot change directory, please check permissions\n");
355 case n_DLS_NAMETOOLONG
:
356 emsg
= N_("Resulting dotlock filename would be too long\n");
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");
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");
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");
380 case n_DLS_PRIVFAILED
:
381 emsg
= N_(" Privilege-separated dotlock program cannot change "
386 emsg
= N_(" It seems there is a stale dotlock file?\n"
387 " Please remove the lock file manually, then retry\n");
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 */
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
)
416 n_err(_(". failed\n"));
422 if(dls
& n_DLS_ABANDON
){
431 n_err(". %s\n", (rv
!= NULL
? _("ok") : _("failed")));
434 if(serr
== n_ERR_ROFS
)
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"));
458 #endif /* HAVE_DOTLOCK */