1/*
2 * Copyright (c) 2006-2007, 2010 Apple Inc. All rights reserved.
3 *
4 * @APPLE_LICENSE_HEADER_START@
5 *
6 * This file contains Original Code and/or Modifications of Original Code
7 * as defined in and that are subject to the Apple Public Source License
8 * Version 2.0 (the 'License'). You may not use this file except in
9 * compliance with the License. Please obtain a copy of the License at
10 * http://www.opensource.apple.com/apsl/ and read it before using this
11 * file.
12 *
13 * The Original Code and all software distributed under the License are
14 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
15 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
16 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
17 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
18 * Please see the License for the specific language governing rights and
19 * limitations under the License.
20 *
21 * @APPLE_LICENSE_HEADER_END@
22 */
23#include <stdbool.h>
24#include <stdlib.h>
25#include <stdio.h>
26#include <dirent.h>
27#include <errno.h>
28#include <fcntl.h>
29#include <fts.h>
30#include <pthread.h>
31#include <pwd.h>
32#include <unistd.h>
33#include <sys/kauth.h>
34#include <sys/stat.h>
35#include <sys/sysctl.h>
36#include <sys/time.h>
37
38#include <mach/mach.h>
39#include <mach/mach_error.h>
40#include <servers/bootstrap.h>
41
42#include <bsm/libbsm.h>
43
44#include <asl.h>
45#include <membership.h>
46#include <launch.h>
47#include <dirhelper_priv.h>
48
49#include "dirhelper.h"
50#include "dirhelperServer.h"
51/*
52 * Uncomment the next line to define BOOTDEBUG, which will write timing
53 * info for clean_directories() to a file, /Debug.
54 */
55//#define BOOTDEBUG
56#ifdef BOOTDEBUG
57#include <mach/mach_time.h>
58#endif //BOOTDEBUG
59
60// globals for idle exit
61struct idle_globals {
62	mach_port_t	mp;
63	long		timeout;
64	struct timeval	lastmsg;
65};
66
67struct idle_globals idle_globals;
68
69// argument structure for clean_thread
70struct clean_args {
71	const char** dirs;
72	int machineBoot;
73};
74
75void* idle_thread(void* param __attribute__((unused)));
76void* clean_thread(void *);
77
78int file_check(const char* path, int mode, int uid, int gid, uid_t* owner, gid_t* group);
79#define is_file(x) file_check((x), S_IFREG, -1, -1, NULL, NULL)
80#define is_directory(x) file_check((x), S_IFDIR, -1, -1, NULL, NULL)
81#define is_directory_get_owner_group(x,o,g) file_check((x), S_IFDIR, -1, -1, (o), (g))
82#define is_root_wheel_directory(x) file_check((x), S_IFDIR, 0, 0, NULL, NULL)
83
84int is_safeboot(void);
85
86void clean_files_older_than(const char* path, time_t when);
87void clean_directories(const char* names[], int);
88
89kern_return_t
90do___dirhelper_create_user_local(
91	mach_port_t server_port __attribute__((unused)),
92	audit_token_t au_tok)
93{
94	int res = 0;
95	uid_t euid;
96	gid_t gid = 0;
97	struct passwd* pwd = NULL;
98
99	gettimeofday(&idle_globals.lastmsg, NULL);
100
101	audit_token_to_au32(au_tok,
102			NULL, // audit uid
103			&euid, // euid
104			NULL, // egid
105			NULL, // ruid
106			NULL, // rgid
107			NULL, // remote_pid
108			NULL, // asid
109			NULL); // aud_tid_t
110
111	// Look-up the primary gid of the user.  We'll use this for chown(2)
112	// so that the created directory is owned by a group that the user
113	// belongs to, avoiding warnings if files are moved outside this dir.
114	pwd = getpwuid(euid);
115	if (pwd) gid = pwd->pw_gid;
116
117	do { // begin block
118		char path[PATH_MAX];
119		char *next;
120
121		if (__user_local_dirname(euid, DIRHELPER_USER_LOCAL, path, sizeof(path)) == NULL) {
122			asl_log(NULL, NULL, ASL_LEVEL_ERR,
123				"__user_local_dirname: %s", strerror(errno));
124			break;
125		}
126
127		// All dirhelper directories are now at the same level, so
128		// we need to remove the DIRHELPER_TOP_STR suffix to get the
129		// parent directory.
130		path[strlen(path) - (sizeof(DIRHELPER_TOP_STR) - 1)] = 0;
131
132		//
133		// 1. Starting with VAR_FOLDERS_PATH, make each subdirectory
134		//    in path, ignoring failure if it already exists.
135		// 2. Change ownership of directory to the user.
136		//
137		next = path + strlen(VAR_FOLDERS_PATH);
138		while ((next = strchr(next, '/')) != NULL) {
139			*next = 0; // temporarily truncate
140			res = mkdir(path, 0755);
141			if (res != 0 && errno != EEXIST) {
142				asl_log(NULL, NULL, ASL_LEVEL_ERR,
143					"mkdir(%s): %s", path, strerror(errno));
144				break;
145			}
146			*next++ = '/'; // restore the slash and increment
147		}
148		if(next || res) // an error occurred
149			break;
150		res = chown(path, euid, gid);
151		if (res != 0) {
152			asl_log(NULL, NULL, ASL_LEVEL_ERR,
153			"chown(%s): %s", path, strerror(errno));
154		}
155	} while(0); // end block
156	return KERN_SUCCESS;
157}
158
159kern_return_t
160do___dirhelper_idle_exit(
161	mach_port_t server_port __attribute__((unused)),
162	audit_token_t au_tok __attribute__((unused))) {
163
164	struct timeval now;
165	gettimeofday(&now, NULL);
166	long delta = now.tv_sec - idle_globals.lastmsg.tv_sec;
167	if (delta >= idle_globals.timeout) {
168		asl_log(NULL, NULL, ASL_LEVEL_DEBUG,
169			"idle exit after %ld seconds", delta);
170		exit(EXIT_SUCCESS);
171	}
172
173	return KERN_SUCCESS;
174}
175
176void*
177idle_thread(void* param __attribute__((unused))) {
178	for(;;) {
179		struct timeval now;
180		gettimeofday(&now, NULL);
181		long delta = (now.tv_sec - idle_globals.lastmsg.tv_sec);
182		if (delta < idle_globals.timeout) {
183			// sleep for remainder of timeout
184			sleep((int)(idle_globals.timeout - delta));
185		} else {
186			// timeout has elapsed, attempt to idle exit
187			__dirhelper_idle_exit(idle_globals.mp);
188		}
189	}
190	return NULL;
191}
192
193// If when == 0, all files are removed.  Otherwise, only regular files that were both created _and_ last modified before `when`.
194void
195clean_files_older_than(const char* path, time_t when) {
196	FTS* fts;
197
198	char* path_argv[] = { (char*)path, NULL };
199	fts = fts_open(path_argv, FTS_PHYSICAL | FTS_XDEV, NULL);
200	if (fts) {
201		FTSENT* ent;
202		asl_log(NULL, NULL, ASL_LEVEL_INFO, "Cleaning " VAR_FOLDERS_PATH "%s", path);
203		while ((ent = fts_read(fts))) {
204			switch(ent->fts_info) {
205				case FTS_F:
206				case FTS_DEFAULT:
207					if (when == 0) {
208#if DEBUG
209						asl_log(NULL, NULL, ASL_LEVEL_ALERT, "unlink(" VAR_FOLDERS_PATH "%s)", ent->fts_path);
210#endif
211						(void)unlink(ent->fts_path);
212					} else if (S_ISREG(ent->fts_statp->st_mode) && (ent->fts_statp->st_birthtime < when) && (ent->fts_statp->st_atime < when)) {
213						int fd = open(ent->fts_path, O_RDONLY | O_NONBLOCK);
214						if (fd != -1) {
215							// Obtain an exclusive lock so
216        					// that we can avoid a race with other processes
217        					// attempting to open or modify the file.
218							int res = flock(fd, LOCK_EX | LOCK_NB);
219							if (res == 0) {
220								struct stat sb;
221								res = fstat(fd, &sb);
222								if ((res == 0) && (sb.st_birthtime < when) && (sb.st_atime < when)) {
223#if DEBUG
224									asl_log(NULL, NULL, ASL_LEVEL_ALERT, "unlink(" VAR_FOLDERS_PATH "%s)", ent->fts_path);
225#endif
226									(void)unlink(ent->fts_path);
227								}
228								(void)flock(fd, LOCK_UN);
229							}
230							close(fd);
231						}
232					}
233					break;
234
235				case FTS_SL:
236				case FTS_SLNONE:
237					if (when == 0) {
238#if DEBUG
239						asl_log(NULL, NULL, ASL_LEVEL_ALERT, "unlink(" VAR_FOLDERS_PATH "%s)", ent->fts_path);
240#endif
241						(void)unlink(ent->fts_path);
242					}
243					break;
244
245				case FTS_DP:
246					if (when == 0) {
247#if DEBUG
248						asl_log(NULL, NULL, ASL_LEVEL_ALERT, "rmdir(" VAR_FOLDERS_PATH "%s)", ent->fts_path);
249#endif
250						(void)rmdir(ent->fts_path);
251					}
252					break;
253
254				case FTS_ERR:
255				case FTS_NS:
256					asl_log(NULL, NULL, ASL_LEVEL_ERR, VAR_FOLDERS_PATH "%s: %s", ent->fts_path, strerror(ent->fts_errno));
257					break;
258
259				default:
260					break;
261			}
262		}
263		fts_close(fts);
264	} else {
265		asl_log(NULL, NULL, ASL_LEVEL_ERR, VAR_FOLDERS_PATH "%s: %s", path, strerror(errno));
266	}
267}
268
269int
270file_check(const char* path, int mode, int uid, int gid, uid_t* owner, gid_t* group) {
271	int check = 1;
272	struct stat sb;
273	if (lstat(path, &sb) == 0) {
274		check = check && ((sb.st_mode & S_IFMT) == mode);
275		check = check && ((sb.st_uid == (uid_t)uid) || uid == -1);
276		check = check && ((sb.st_gid == (gid_t)gid) || gid == -1);
277		if (check) {
278			if (owner) *owner = sb.st_uid;
279			if (group) *group = sb.st_gid;
280		}
281	} else {
282		if (errno != ENOENT) {
283			/* This will print a shorter path after chroot() */
284			asl_log(NULL, NULL, ASL_LEVEL_ERR, "%s: %s", path, strerror(errno));
285		}
286		check = 0;
287	}
288	return check;
289}
290
291int
292is_safeboot(void) {
293	uint32_t sb = 0;
294	size_t sbsz = sizeof(sb);
295
296	if (sysctlbyname("kern.safeboot", &sb, &sbsz, NULL, 0) != 0) {
297		return 0;
298	} else {
299		return (int)sb;
300	}
301}
302
303void *
304clean_thread(void *a) {
305	struct clean_args* args = (struct clean_args*)a;
306	DIR* d;
307	time_t when = 0;
308	int i;
309
310	if (!args->machineBoot) {
311		struct timeval now;
312		long days = 3;
313		const char* str = getenv("CLEAN_FILES_OLDER_THAN_DAYS");
314		if (str) {
315			days = strtol(str, NULL, 0);
316		}
317		(void)gettimeofday(&now, NULL);
318		for (i = 0; args->dirs[i]; i++)
319			asl_log(NULL, NULL, ASL_LEVEL_INFO, "Cleaning %s older than %ld days", args->dirs[i], days);
320
321		when = now.tv_sec - (days * 60 * 60 * 24);
322	}
323
324	// Look up the boot time
325	struct timespec boottime;
326	size_t len = sizeof(boottime);
327	if (sysctlbyname("kern.boottime", &boottime, &len, NULL, 0) == -1) {
328		asl_log(NULL, NULL, ASL_LEVEL_ERR, "%s: %s", "sysctl kern.boottime", strerror(errno));
329		return NULL;
330	}
331
332	if (!is_root_wheel_directory(VAR_FOLDERS_PATH)) {
333		asl_log(NULL, NULL, ASL_LEVEL_ERR, "%s: %s", VAR_FOLDERS_PATH, "invalid ownership");
334		return NULL;
335	}
336
337	if (chroot(VAR_FOLDERS_PATH)) {
338		asl_log(NULL, NULL, ASL_LEVEL_ERR, "chroot(%s) failed: %s",
339			VAR_FOLDERS_PATH, strerror(errno));
340	}
341	chdir("/");
342	if ((d = opendir("/"))) {
343		struct dirent* e;
344		char path[PATH_MAX];
345
346		// /var/folders/*
347		while ((e = readdir(d))) {
348			if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0) continue;
349
350			snprintf(path, sizeof(path), "%s%s", "/", e->d_name);
351			if (is_root_wheel_directory(path)) {
352				DIR* d2 = opendir(path);
353				if (d2) {
354					struct dirent* e2;
355
356					// /var/folders/*/*
357					while ((e2 = readdir(d2))) {
358						char dirbuf[PATH_MAX];
359						uid_t owner;
360						gid_t group;
361						if (strcmp(e2->d_name, ".") == 0 || strcmp(e2->d_name, "..") == 0) continue;
362						snprintf(dirbuf, sizeof(dirbuf),
363							"%s/%s", path, e2->d_name);
364						if (!is_directory_get_owner_group(dirbuf, &owner, &group)) continue;
365						if (pthread_setugid_np(owner, group) != 0) {
366							asl_log(NULL, NULL, ASL_LEVEL_ERR,
367								"skipping %s: pthread_setugid_np(%u, %u): %s",
368								dirbuf, owner, group, strerror(errno));
369							continue;
370						}
371						for (i = 0; args->dirs[i]; i++) {
372							const char *name = args->dirs[i];
373							snprintf(dirbuf, sizeof(dirbuf),
374								 "%s/%s/%s", path, e2->d_name, name);
375							if (is_directory(dirbuf)) {
376								// at boot time we clean all files,
377								// otherwise only clean regular files.
378								clean_files_older_than(dirbuf, when);
379							}
380						}
381						if (pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE) != 0) {
382							asl_log(NULL, NULL, ASL_LEVEL_ERR,
383								"%s: pthread_setugid_np(KAUTH_UID_NONE, KAUTH_GID_NONE): %s",
384								dirbuf, strerror(errno));
385						}
386					}
387					closedir(d2);
388				} else {
389					asl_log(NULL, NULL, ASL_LEVEL_ERR, "%s: %s", path, strerror(errno));
390				}
391			}
392		}
393
394		closedir(d);
395	} else {
396		asl_log(NULL, NULL, ASL_LEVEL_ERR, "%s: %s", VAR_FOLDERS_PATH, strerror(errno));
397	}
398	return NULL;
399}
400
401void
402clean_directories(const char* dirs[], int machineBoot) {
403	struct clean_args args;
404	pthread_t t;
405	int ret;
406#ifdef BOOTDEBUG
407	double ratio;
408	struct mach_timebase_info info;
409	uint64_t begin, end;
410	FILE *debug;
411
412	mach_timebase_info(&info);
413	ratio = (double)info.numer / ((double)info.denom * NSEC_PER_SEC);
414	begin = mach_absolute_time();
415	if((debug = fopen("/Debug", "a")) != NULL) {
416	    fprintf(debug, "clean_directories: machineBoot=%d\n", machineBoot);
417	}
418#endif //BOOTDEBUG
419
420	args.dirs = dirs;
421	args.machineBoot = machineBoot;
422	ret = pthread_create(&t, NULL, clean_thread, &args);
423	if (ret == 0) {
424		ret = pthread_join(t, NULL);
425		if (ret) {
426			asl_log(NULL, NULL, ASL_LEVEL_ERR, "clean_directories: pthread_join: %s",
427				strerror(ret));
428		}
429	} else {
430		asl_log(NULL, NULL, ASL_LEVEL_ERR, "clean_directories: pthread_create: %s",
431			strerror(ret));
432	}
433#ifdef BOOTDEBUG
434	end = mach_absolute_time();
435	if(debug) {
436	    fprintf(debug, "clean_directories: %f secs\n", ratio * (end - begin));
437	    fclose(debug);
438	}
439#endif //BOOTDEBUG
440}
441
442int
443main(int argc, char* argv[]) {
444	mach_msg_size_t mxmsgsz = MAX_TRAILER_SIZE;
445	kern_return_t kr;
446	long idle_timeout = 30; // default 30 second timeout
447
448#ifdef BOOTDEBUG
449	{
450		FILE *debug;
451		int i;
452		if((debug = fopen("/Debug", "a")) != NULL) {
453		    for(i = 0; i < argc; i++) {
454			    fprintf(debug, " %s", argv[i]);
455		    }
456		    fputc('\n', debug);
457		    fclose(debug);
458		}
459	}
460#endif //BOOTDEBUG
461	// Clean up TemporaryItems directory when launched at boot.
462	// It is safe to clean all file types at this time.
463	if (argc > 1 && strcmp(argv[1], "-machineBoot") == 0) {
464		const char *dirs[5];
465		int i = 0;
466		dirs[i++] = DIRHELPER_TEMP_STR;
467		dirs[i++] = "TemporaryItems";
468		dirs[i++] = "Cleanup At Startup";
469		if (is_safeboot()) {
470			dirs[i++] = DIRHELPER_CACHE_STR;
471		}
472		dirs[i] = NULL;
473		clean_directories(dirs, 1);
474		exit(EXIT_SUCCESS);
475	} else if (argc > 1 && strcmp(argv[1], "-cleanTemporaryItems") == 0) {
476		const char *dirs[] = {
477			DIRHELPER_TEMP_STR,
478			"TemporaryItems",
479			NULL
480		};
481		clean_directories(dirs, 0);
482		exit(EXIT_SUCCESS);
483	} else if (argc > 1) {
484		exit(EXIT_FAILURE);
485	}
486
487	launch_data_t config = NULL, checkin = NULL;
488	checkin = launch_data_new_string(LAUNCH_KEY_CHECKIN);
489	config = launch_msg(checkin);
490	if (!config || launch_data_get_type(config) == LAUNCH_DATA_ERRNO) {
491		asl_log(NULL, NULL, ASL_LEVEL_ERR, "launchd checkin failed");
492		exit(EXIT_FAILURE);
493	}
494
495	launch_data_t tmv;
496	tmv = launch_data_dict_lookup(config, LAUNCH_JOBKEY_TIMEOUT);
497	if (tmv) {
498		idle_timeout = (long)launch_data_get_integer(tmv);
499		asl_log(NULL, NULL, ASL_LEVEL_DEBUG,
500			"idle timeout set: %ld seconds", idle_timeout);
501	}
502
503	launch_data_t svc;
504	svc = launch_data_dict_lookup(config, LAUNCH_JOBKEY_MACHSERVICES);
505	if (!svc) {
506		asl_log(NULL, NULL, ASL_LEVEL_ERR, "no mach services");
507		exit(EXIT_FAILURE);
508	}
509
510	svc = launch_data_dict_lookup(svc, DIRHELPER_BOOTSTRAP_NAME);
511	if (!svc) {
512		asl_log(NULL, NULL, ASL_LEVEL_ERR, "no mach service: %s",
513			DIRHELPER_BOOTSTRAP_NAME);
514		exit(EXIT_FAILURE);
515	}
516
517	mach_port_t mp = launch_data_get_machport(svc);
518	if (mp == MACH_PORT_NULL) {
519		asl_log(NULL, NULL, ASL_LEVEL_ERR, "NULL mach service: %s",
520			DIRHELPER_BOOTSTRAP_NAME);
521		exit(EXIT_FAILURE);
522	}
523
524	// insert a send right so we can send our idle exit message
525	kr = mach_port_insert_right(mach_task_self(), mp, mp,
526		MACH_MSG_TYPE_MAKE_SEND);
527	if (kr != KERN_SUCCESS) {
528		asl_log(NULL, NULL, ASL_LEVEL_ERR, "send right failed: %s",
529			mach_error_string(kr));
530		exit(EXIT_FAILURE);
531	}
532
533	// spawn a thread for our idle timeout
534	pthread_t thread;
535	idle_globals.mp = mp;
536	idle_globals.timeout = idle_timeout;
537	gettimeofday(&idle_globals.lastmsg, NULL);
538	pthread_create(&thread, NULL, &idle_thread, NULL);
539
540	// look to see if we have any messages queued.  if not, assume
541	// we were launched because of the calendar interval, and attempt
542	// to clean the temporary items.
543	mach_msg_type_number_t status_count = MACH_PORT_RECEIVE_STATUS_COUNT;
544	mach_port_status_t status;
545	kr = mach_port_get_attributes(mach_task_self(), mp,
546		MACH_PORT_RECEIVE_STATUS, (mach_port_info_t)&status, &status_count);
547	if (kr == KERN_SUCCESS && status.mps_msgcount == 0) {
548		const char *dirs[] = {
549			DIRHELPER_TEMP_STR,
550			"TemporaryItems",
551			NULL
552		};
553		clean_directories(dirs, 0);
554		exit(EXIT_SUCCESS);
555	}
556
557	// main event loop
558
559	kr = mach_msg_server(dirhelper_server, mxmsgsz, mp,
560			MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AUDIT) |
561			MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0));
562	if (kr != KERN_SUCCESS) {
563		asl_log(NULL, NULL, ASL_LEVEL_ERR,
564			"mach_msg_server(mp): %s", mach_error_string(kr));
565		exit(EXIT_FAILURE);
566	}
567
568	exit(EXIT_SUCCESS);
569}
570