1 /*
2  * dotlockfile.c	Command line version of liblockfile.
3  *			Runs setgid mail so is able to lock mailboxes
4  *			as well. Liblockfile can call this command.
5  *
6  *		Copyright (C) Miquel van Smoorenburg and contributors 1999-2021
7  *
8  *		This program is free software; you can redistribute it and/or
9  *		modify it under the terms of the GNU General Public License
10  *		as published by the Free Software Foundation; either version 2
11  *		of the License, or (at your option) any later version.
12  */
13 
14 #include "autoconf.h"
15 
16 #include <sys/types.h>
17 #if HAVE_SYS_PARAM_H
18 #include <sys/param.h>
19 #endif
20 #include <sys/wait.h>
21 #include <stdio.h>
22 #include <string.h>
23 #include <pwd.h>
24 #include <fcntl.h>
25 #include <stdlib.h>
26 #include <unistd.h>
27 #include <signal.h>
28 #include <time.h>
29 #include <errno.h>
30 #include <maillock.h>
31 #include <lockfile.h>
32 
33 #ifdef HAVE_GETOPT_H
34 #include <getopt.h>
35 #endif
36 
37 #ifndef HAVE_GETOPT_H
38 extern int getopt();
39 extern char *optarg;
40 extern int optind;
41 #endif
42 
43 extern int is_maillock(const char *lockfile);
44 extern int lockfile_create_set_tmplock(const char *lockfile,
45 			volatile char **tmplock, int retries, int flags, struct __lockargs *);
46 
47 static volatile char *tmplock;
48 static int quiet;
49 
50 /*
51  *	If we got SIGINT, SIGQUIT, SIGHUP, remove the
52  *	tempfile and re-raise the signal.
53  */
got_signal(int sig)54 void got_signal(int sig)
55 {
56 	if (tmplock && tmplock[0])
57 		unlink((char *)tmplock);
58 	signal(sig, SIG_DFL);
59 	raise(sig);
60 }
61 
ignore_signal(int sig)62 void ignore_signal(int sig)
63 {
64 }
65 
66 /*
67  *	Install signal handler only if the signal was
68  *	not ignored already.
69  */
set_signal(int sig,void (* handler)(int))70 int set_signal(int sig, void (*handler)(int))
71 {
72 	struct sigaction sa;
73 
74 	if (sigaction(sig, NULL, &sa) < 0)
75 		return -1;
76 	if (sa.sa_handler == SIG_IGN)
77 		return 0;
78 	memset(&sa, 0, sizeof(sa));
79 	sa.sa_handler = handler;
80 	return sigaction(sig, &sa, NULL);
81 }
82 
83 /*
84  *	Sleep for an amount of time while regulary checking if
85  *	our parent is still alive.
86  */
check_sleep(int sleeptime,int flags)87 int check_sleep(int sleeptime, int flags)
88 {
89 	int		i;
90 	int		interval = 5;
91 	static int	ppid = 0;
92 
93 	if (ppid == 0) ppid = getppid();
94 
95 	if (flags & __L_INTERVAL)
96 		interval = 1;
97 
98 	for (i = 0; i < sleeptime; i += interval) {
99 		sleep(interval);
100 		if (kill(ppid, 0) < 0 && errno == ESRCH)
101 			return L_ERROR;
102 	}
103 	return 0;
104 }
105 
106 /*
107  *	Split a filename up in  file and directory.
108  */
fn_split(char * fn,char ** fn_p,char ** dir_p)109 int fn_split(char *fn, char **fn_p, char **dir_p)
110 {
111 	static char	*buf = NULL;
112 	char		*p;
113 
114 	if (buf)
115 		free (buf);
116 	buf = (char *) malloc (strlen (fn) + 1);
117 	if (! buf)
118 		return L_ERROR;
119 	strcpy(buf, fn);
120 	if ((p = strrchr(buf, '/')) != NULL) {
121 		*p++   = 0;
122 		*fn_p  = p;
123 		*dir_p = buf;
124 	} else {
125 		*fn_p  = fn;
126 		*dir_p = ".";
127 	}
128 	return L_SUCCESS;
129 }
130 
131 
132 /*
133  *	Return name of lockfile for mail.
134  */
mlockname(char * user)135 char *mlockname(char *user)
136 {
137 	static char	*buf = NULL;
138 	char		*e;
139 
140 	if (buf)
141 		free(buf);
142 
143 	e = getenv("MAIL");
144 	if (e) {
145 		buf = (char *)malloc(strlen(e)+6);
146 		if (!buf)
147 			return NULL;
148 		sprintf(buf, "%s.lock", e);
149 	} else {
150 		buf = (char *)malloc(strlen(MAILDIR)+strlen(user)+6);
151 		if (!buf)
152 			return NULL;
153 		sprintf(buf, "%s%s.lock", MAILDIR, user);
154 	}
155 	return buf;
156 }
157 
perror_exit(const char * why)158 void perror_exit(const char *why) {
159 	if (!quiet) {
160 		fprintf(stderr, "dotlockfile: ");
161 		perror(why);
162 	}
163 	exit(L_ERROR);
164 }
165 
166 /*
167  *	Print usage mesage and exit.
168  */
usage(void)169 void usage(void)
170 {
171 	fprintf(stderr, "Usage:  dotlockfile -l [-r retries] [-i interval] [-p] [-q] <-m|lockfile>\n");
172 	fprintf(stderr, "        dotlockfile -l [-r retries] [-i interval] [-p] [-q] <-m|lockfile> [-P] command args...\n");
173 	fprintf(stderr, "        dotlockfile -u|-t\n");
174 	exit(1);
175 }
176 
main(int argc,char ** argv)177 int main(int argc, char **argv)
178 {
179 	struct passwd	*pwd;
180 	struct __lockargs args = { 0 };
181 	gid_t		gid, egid;
182 	char		*lockfile = NULL;
183 	char		**cmd = NULL;
184 	int 		c, r;
185 	int		retries = 5;
186 	int		interval = 0;
187 	int		flags = 0;
188 	int		lock = 0;
189 	int		unlock = 0;
190 	int		check = 0;
191 	int		touch = 0;
192 	int		writepid = 0;
193 	int		passthrough = 0;
194 
195 	/*
196 	 *	Remember real and effective gid, and
197 	 *	drop privs for now.
198 	 */
199 	if ((gid = getgid()) < 0)
200 		perror_exit("getgid");
201 	if ((egid = getegid()) < 0)
202 		perror_exit("getegid");
203 	if (gid != egid) {
204 		if (setregid(-1, gid) < 0)
205 			perror_exit("setregid(-1, gid)");
206 	}
207 
208 	set_signal(SIGINT, got_signal);
209 	set_signal(SIGQUIT, got_signal);
210 	set_signal(SIGHUP, got_signal);
211 	set_signal(SIGTERM, got_signal);
212 	set_signal(SIGPIPE, got_signal);
213 
214 	/*
215 	 *	Process the options.
216 	 */
217 	while ((c = getopt(argc, argv, "+qpNr:mluci:tP")) != EOF) switch(c) {
218 		case 'q':
219 			quiet = 1;
220 			break;
221 		case 'p':
222 			writepid = 1;
223 			break;
224 		case 'N':
225 			/* NOP */
226 			break;
227 		case 'r':
228 			retries = atoi(optarg);
229 			if (retries <= 0 &&
230 			    retries != -1 && strcmp(optarg, "0") != 0) {
231 				if (!quiet)
232 					fprintf(stderr, "dotlockfile: "
233 						"-r %s: invalid argument\n",
234 						optarg);
235 				return L_ERROR;
236 			}
237 			if (retries == -1) {
238 				/* 4000 years */
239 				retries = 0x7ffffff0;
240 			}
241 			break;
242 		case 'm':
243 			if ((pwd = getpwuid(geteuid())) == NULL) {
244 				if (!quiet)
245 					fprintf(stderr, "dotlockfile: You don't exist. Go away.\n");
246 				return L_ERROR;
247 			}
248 			lockfile = mlockname(pwd->pw_name);
249 			if (!lockfile) {
250 				if (!quiet)
251 					perror("dotlockfile");
252 				return L_ERROR;
253 			}
254 			break;
255 		case 'l':
256 			lock = 1;
257 			break;
258 		case 'u':
259 			unlock = 1;
260 			break;
261 		case 'c':
262 			check = 1;
263 			break;
264 		case 'i':
265 			interval = atoi(optarg);
266 			if (interval <= 0 && strcmp(optarg, "0") != 0) {
267 				fprintf(stderr, "dotlockfile: -i needs argument >= 0\n");
268 				return L_ERROR;
269 			}
270 			flags |= __L_INTERVAL;
271 			args.interval = interval;
272 			break;
273 		case 't':
274 			touch = 1;
275 			break;
276 		case 'P':
277 			passthrough = 1;
278 			break;
279 		default:
280 			usage();
281 			break;
282 	}
283 
284 	/*
285 	 * next argument may be lockfile name
286 	 */
287 	if (!lockfile) {
288 		if (optind == argc)
289 			usage();
290 		lockfile = argv[optind++];
291 	}
292 
293 	/*
294 	 * next arguments may be command [args...]
295 	 */
296 	if (optind < argc)
297 		cmd = argv + optind;
298 
299 	/*
300 	 *	Options sanity check
301 	 */
302 	if ((cmd || lock) && (touch || check || unlock))
303 		usage();
304 
305 	if (writepid)
306 		flags |= (cmd ? L_PID : L_PPID);
307 
308 #ifdef MAXPATHLEN
309 	if (strlen(lockfile) >= MAXPATHLEN) {
310 		if (!quiet)
311 			fprintf(stderr, "dotlockfile: %s: name too long\n", lockfile);
312 		return L_NAMELEN;
313 	}
314 #endif
315 
316 	/*
317 	 *	Check if we run setgid.
318 	 */
319 	int cwd_fd = -1;
320 	int need_privs = 0;
321 #ifdef MAILGROUP
322 	if (gid != egid) {
323 		/*
324 		 *	See if the requested lock is for a mailbox.
325 		 *	First, remember currect working directory.
326 		 */
327 #ifdef O_PATH
328 		cwd_fd = open(".", O_PATH|O_CLOEXEC);
329 #else
330 		cwd_fd = open(".", O_RDONLY|O_CLOEXEC);
331 #endif
332 		if (cwd_fd < 0) {
333 			if (!quiet)
334 				fprintf(stderr, "dotlockfile: opening \".\": %s\n",
335 					strerror(errno));
336 			return L_ERROR;
337 		}
338 		/*
339 		 *	Now change directory to the directory the lockfile is in.
340 		 */
341 		char *file, *dir;
342 		r = fn_split(lockfile, &file, &dir);
343 		if (r != L_SUCCESS) {
344 			if (!quiet)
345 				perror("dotlockfile");
346 			return L_ERROR;
347 		}
348 		if (chdir(dir) != 0) {
349 			if (!quiet)
350 				fprintf(stderr, "dotlockfile: %s: %s\n", dir, strerror(errno));
351 			return L_ERROR;
352 		}
353 
354 		lockfile = file;
355 		need_privs = is_maillock(lockfile);
356 	}
357 #endif
358 
359 	/*
360 	 *	See if we actually need to run setgid.
361 	 */
362 	if (need_privs) {
363 		if (setregid(gid, egid) != 0)
364 			perror_exit("setregid");
365 	} else {
366 		if (gid != egid && setgid(gid) != 0)
367 			perror_exit("setgid");
368 	}
369 
370 	/*
371 	 *	Simple check for a valid lockfile ?
372 	 */
373 	if (check)
374 		return (lockfile_check(lockfile, flags) < 0) ? 1 : 0;
375 
376 
377 	/*
378 	 *	Touch lock ?
379 	 */
380 	if (touch)
381 		return (lockfile_touch(lockfile) < 0) ? 1 : 0;
382 
383 	/*
384 	 *	Remove lockfile?
385 	 */
386 	if (unlock)
387 		return (lockfile_remove(lockfile) == 0) ? 0 : 1;
388 
389 
390 	/*
391 	 *	No, lock.
392 	 */
393 	r = lockfile_create_set_tmplock(lockfile, &tmplock, retries, flags, &args);
394 	if (r != 0 || !cmd)
395 		return r;
396 
397 
398 	/*
399 	 *	Spawn command.
400 	 *
401 	 *	Using an empty signal handler means that we ignore the
402 	 *	signal, but that it's restored to SIG_DFL at execve().
403 	 */
404 	set_signal(SIGINT, ignore_signal);
405 	set_signal(SIGQUIT, ignore_signal);
406 	set_signal(SIGHUP, ignore_signal);
407 	set_signal(SIGALRM, ignore_signal);
408 
409 	pid_t pid = fork();
410 	if (pid < 0) {
411 		if (!quiet)
412 			perror("fork");
413 		lockfile_remove(lockfile);
414 		exit(L_ERROR);
415 	}
416 	if (pid == 0) {
417 		/* drop setgid */
418 		if (gid != egid && setgid(gid) < 0) {
419 			perror("setgid");
420 			exit(127);
421 		}
422 		/* restore current working directory */
423 		if (cwd_fd >= 0) {
424 			if (fchdir(cwd_fd) < 0) {
425 				perror("dotlockfile: restoring cwd:");
426 				exit(127);
427 			}
428 			close(cwd_fd);
429 		}
430 		/* exec */
431 		execvp(cmd[0], cmd);
432 		perror(cmd[0]);
433 		exit(127);
434 	}
435 
436 	/* wait for child */
437 	int e, wstatus;
438 	while (1) {
439 		if (!writepid)
440 			alarm(30);
441 		e = waitpid(pid, &wstatus, 0);
442 		if (e >= 0 || errno != EINTR)
443 			break;
444 		if (!writepid)
445 			lockfile_touch(lockfile);
446 	}
447 
448 	alarm(0);
449 	lockfile_remove(lockfile);
450 
451 	if (passthrough) {
452 		if (WIFEXITED(wstatus))
453 			return WEXITSTATUS(wstatus);
454 		if (WIFSIGNALED(wstatus))
455 			return 128+WTERMSIG(wstatus);
456 	}
457 	return 0;
458 }
459 
460