1/*-
2 * SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3 *
4 * Copyright (c) 2014 The FreeBSD Foundation
5 * All rights reserved.
6 *
7 * This software was developed by Edward Tomasz Napierala under sponsorship
8 * from the FreeBSD Foundation.
9 *
10 * Redistribution and use in source and binary forms, with or without
11 * modification, are permitted provided that the following conditions
12 * are met:
13 * 1. Redistributions of source code must retain the above copyright
14 *    notice, this list of conditions and the following disclaimer.
15 * 2. Redistributions in binary form must reproduce the above copyright
16 *    notice, this list of conditions and the following disclaimer in the
17 *    documentation and/or other materials provided with the distribution.
18 *
19 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29 * SUCH DAMAGE.
30 *
31 */
32
33#include <sys/cdefs.h>
34__FBSDID("$FreeBSD$");
35
36#include <sys/types.h>
37#include <sys/event.h>
38#include <sys/mount.h>
39#include <sys/time.h>
40#include <assert.h>
41#include <errno.h>
42#include <libutil.h>
43#include <stdbool.h>
44#include <stdint.h>
45#include <stdio.h>
46#include <stdlib.h>
47#include <string.h>
48#include <unistd.h>
49
50#include "common.h"
51
52#define AUTOUNMOUNTD_PIDFILE	"/var/run/autounmountd.pid"
53
54struct automounted_fs {
55	TAILQ_ENTRY(automounted_fs)	af_next;
56	time_t				af_mount_time;
57	bool				af_mark;
58	fsid_t				af_fsid;
59	char				af_mountpoint[MNAMELEN];
60};
61
62static TAILQ_HEAD(, automounted_fs)	automounted;
63
64static struct automounted_fs *
65automounted_find(fsid_t fsid)
66{
67	struct automounted_fs *af;
68
69	TAILQ_FOREACH(af, &automounted, af_next) {
70		if (fsidcmp(&af->af_fsid, &fsid) == 0)
71			return (af);
72	}
73
74	return (NULL);
75}
76
77static struct automounted_fs *
78automounted_add(fsid_t fsid, const char *mountpoint)
79{
80	struct automounted_fs *af;
81
82	af = calloc(1, sizeof(*af));
83	if (af == NULL)
84		log_err(1, "calloc");
85	af->af_mount_time = time(NULL);
86	af->af_fsid = fsid;
87	strlcpy(af->af_mountpoint, mountpoint, sizeof(af->af_mountpoint));
88
89	TAILQ_INSERT_TAIL(&automounted, af, af_next);
90
91	return (af);
92}
93
94static void
95automounted_remove(struct automounted_fs *af)
96{
97
98	TAILQ_REMOVE(&automounted, af, af_next);
99	free(af);
100}
101
102static void
103refresh_automounted(void)
104{
105	struct automounted_fs *af, *tmpaf;
106	struct statfs *mntbuf;
107	int i, nitems;
108
109	nitems = getmntinfo(&mntbuf, MNT_WAIT);
110	if (nitems <= 0)
111		log_err(1, "getmntinfo");
112
113	log_debugx("refreshing list of automounted filesystems");
114
115	TAILQ_FOREACH(af, &automounted, af_next)
116		af->af_mark = false;
117
118	for (i = 0; i < nitems; i++) {
119		if (strcmp(mntbuf[i].f_fstypename, "autofs") == 0) {
120			log_debugx("skipping %s, filesystem type is autofs",
121			    mntbuf[i].f_mntonname);
122			continue;
123		}
124
125		if ((mntbuf[i].f_flags & MNT_AUTOMOUNTED) == 0) {
126			log_debugx("skipping %s, not automounted",
127			    mntbuf[i].f_mntonname);
128			continue;
129		}
130
131		af = automounted_find(mntbuf[i].f_fsid);
132		if (af == NULL) {
133			log_debugx("new automounted filesystem found on %s "
134			    "(FSID:%d:%d)", mntbuf[i].f_mntonname,
135			    mntbuf[i].f_fsid.val[0], mntbuf[i].f_fsid.val[1]);
136			af = automounted_add(mntbuf[i].f_fsid,
137			    mntbuf[i].f_mntonname);
138		} else {
139			log_debugx("already known automounted filesystem "
140			    "found on %s (FSID:%d:%d)", mntbuf[i].f_mntonname,
141			    mntbuf[i].f_fsid.val[0], mntbuf[i].f_fsid.val[1]);
142		}
143		af->af_mark = true;
144	}
145
146	TAILQ_FOREACH_SAFE(af, &automounted, af_next, tmpaf) {
147		if (af->af_mark)
148			continue;
149		log_debugx("lost filesystem mounted on %s (FSID:%d:%d)",
150		    af->af_mountpoint, af->af_fsid.val[0], af->af_fsid.val[1]);
151		automounted_remove(af);
152	}
153}
154
155static int
156unmount_by_fsid(const fsid_t fsid, const char *mountpoint)
157{
158	char *fsid_str;
159	int error, ret;
160
161	ret = asprintf(&fsid_str, "FSID:%d:%d", fsid.val[0], fsid.val[1]);
162	if (ret < 0)
163		log_err(1, "asprintf");
164
165	error = unmount(fsid_str, MNT_NONBUSY | MNT_BYFSID);
166	if (error != 0) {
167		if (errno == EBUSY) {
168			log_debugx("cannot unmount %s (%s): %s",
169			    mountpoint, fsid_str, strerror(errno));
170		} else {
171			log_warn("cannot unmount %s (%s)",
172			    mountpoint, fsid_str);
173		}
174	}
175
176	free(fsid_str);
177
178	return (error);
179}
180
181static double
182expire_automounted(double expiration_time)
183{
184	struct automounted_fs *af, *tmpaf;
185	time_t now;
186	double mounted_for, mounted_max = -1.0;
187	int error;
188
189	now = time(NULL);
190
191	log_debugx("expiring automounted filesystems");
192
193	TAILQ_FOREACH_SAFE(af, &automounted, af_next, tmpaf) {
194		mounted_for = difftime(now, af->af_mount_time);
195
196		if (mounted_for < expiration_time) {
197			log_debugx("skipping %s (FSID:%d:%d), mounted "
198			    "for %.0f seconds", af->af_mountpoint,
199			    af->af_fsid.val[0], af->af_fsid.val[1],
200			    mounted_for);
201
202			if (mounted_for > mounted_max)
203				mounted_max = mounted_for;
204
205			continue;
206		}
207
208		log_debugx("filesystem mounted on %s (FSID:%d:%d), "
209		    "was mounted for %.0f seconds; unmounting",
210		    af->af_mountpoint, af->af_fsid.val[0], af->af_fsid.val[1],
211		    mounted_for);
212		error = unmount_by_fsid(af->af_fsid, af->af_mountpoint);
213		if (error != 0) {
214			if (mounted_for > mounted_max)
215				mounted_max = mounted_for;
216		}
217	}
218
219	return (mounted_max);
220}
221
222static void
223usage_autounmountd(void)
224{
225
226	fprintf(stderr, "usage: autounmountd [-r time][-t time][-dv]\n");
227	exit(1);
228}
229
230static void
231do_wait(int kq, double sleep_time)
232{
233	struct timespec timeout;
234	struct kevent unused;
235	int nevents;
236
237	if (sleep_time != -1.0) {
238		assert(sleep_time > 0.0);
239		timeout.tv_sec = sleep_time;
240		timeout.tv_nsec = 0;
241
242		log_debugx("waiting for filesystem event for %.0f seconds", sleep_time);
243		nevents = kevent(kq, NULL, 0, &unused, 1, &timeout);
244	} else {
245		log_debugx("waiting for filesystem event");
246		nevents = kevent(kq, NULL, 0, &unused, 1, NULL);
247	}
248	if (nevents < 0) {
249		if (errno == EINTR)
250			return;
251		log_err(1, "kevent");
252	}
253
254	if (nevents == 0) {
255		log_debugx("timeout reached");
256		assert(sleep_time > 0.0);
257	} else {
258		log_debugx("got filesystem event");
259	}
260}
261
262int
263main_autounmountd(int argc, char **argv)
264{
265	struct kevent event;
266	struct pidfh *pidfh;
267	pid_t otherpid;
268	const char *pidfile_path = AUTOUNMOUNTD_PIDFILE;
269	int ch, debug = 0, error, kq;
270	double expiration_time = 600, retry_time = 600, mounted_max, sleep_time;
271	bool dont_daemonize = false;
272
273	while ((ch = getopt(argc, argv, "dr:t:v")) != -1) {
274		switch (ch) {
275		case 'd':
276			dont_daemonize = true;
277			debug++;
278			break;
279		case 'r':
280			retry_time = atoi(optarg);
281			break;
282		case 't':
283			expiration_time = atoi(optarg);
284			break;
285		case 'v':
286			debug++;
287			break;
288		case '?':
289		default:
290			usage_autounmountd();
291		}
292	}
293	argc -= optind;
294	if (argc != 0)
295		usage_autounmountd();
296
297	if (retry_time <= 0)
298		log_errx(1, "retry time must be greater than zero");
299	if (expiration_time <= 0)
300		log_errx(1, "expiration time must be greater than zero");
301
302	log_init(debug);
303
304	pidfh = pidfile_open(pidfile_path, 0600, &otherpid);
305	if (pidfh == NULL) {
306		if (errno == EEXIST) {
307			log_errx(1, "daemon already running, pid: %jd.",
308			    (intmax_t)otherpid);
309		}
310		log_err(1, "cannot open or create pidfile \"%s\"",
311		    pidfile_path);
312	}
313
314	if (dont_daemonize == false) {
315		if (daemon(0, 0) == -1) {
316			log_warn("cannot daemonize");
317			pidfile_remove(pidfh);
318			exit(1);
319		}
320	}
321
322	pidfile_write(pidfh);
323
324	TAILQ_INIT(&automounted);
325
326	kq = kqueue();
327	if (kq < 0)
328		log_err(1, "kqueue");
329
330	EV_SET(&event, 0, EVFILT_FS, EV_ADD | EV_CLEAR, 0, 0, NULL);
331	error = kevent(kq, &event, 1, NULL, 0, NULL);
332	if (error < 0)
333		log_err(1, "kevent");
334
335	for (;;) {
336		refresh_automounted();
337		mounted_max = expire_automounted(expiration_time);
338		if (mounted_max == -1.0) {
339			sleep_time = mounted_max;
340			log_debugx("no filesystems to expire");
341		} else if (mounted_max < expiration_time) {
342			sleep_time = difftime(expiration_time, mounted_max);
343			log_debugx("some filesystems expire in %.0f seconds",
344			    sleep_time);
345		} else {
346			sleep_time = retry_time;
347			log_debugx("some expired filesystems remain mounted, "
348			    "will retry in %.0f seconds", sleep_time);
349		}
350
351		do_wait(kq, sleep_time);
352	}
353
354	return (0);
355}
356