]> git.saurik.com Git - apple/mdnsresponder.git/blob - mDNSMacOSX/xpc_services/xpc_service_log_utility.c
mDNSResponder-1096.0.2.tar.gz
[apple/mdnsresponder.git] / mDNSMacOSX / xpc_services / xpc_service_log_utility.c
1 /*
2 * Copyright (c) 2019 Apple Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include <xpc/xpc.h>
18 #include <dirent.h> // opendir
19 #include <sys/stat.h> // stat
20 #include <archive.h>
21 #include <archive_entry.h>
22 #include <AssertMacros.h> // require, require_action
23
24 #include "mDNSMacOSX.h"
25 #include "helper.h"
26 #include "xpc_services.h"
27 #include "xpc_service_log_utility.h"
28 #include "xpc_clients.h"
29 #include "system_utilities.h" // IsAppleInternalBuild
30
31 #define STATE_DUMP_PLAIN_SUFFIX "txt"
32 #define STATE_DUMP_COMPRESSED_SUFFIX "tar.bz2"
33
34 // global variables
35 extern mDNS mDNSStorage;
36 static dispatch_queue_t log_utility_server_queue = NULL;
37
38 // function declaration
39 extern void dump_state_to_fd(int fd);
40 mDNSlocal void accept_client(xpc_connection_t conn);
41 mDNSlocal mDNSs8 handle_requests(xpc_object_t req);
42 mDNSlocal mDNSs8 check_permission(xpc_connection_t connection);
43 mDNSlocal mDNSs8 handle_state_dump(mDNSu32 dump_option, char *full_file_name, mDNSu32 name_buffer_len,
44 int client_fd, mDNSs32 *time_ms_used);
45 mDNSlocal mDNSs32 find_oldest_state_dump(const char *dump_dir, const char *file_name, char *full_file_name,
46 mDNSu32 buffer_len, char *oldest_file_name);
47 mDNSlocal mDNSs8 remove_state_dump_if_too_many(const char *dump_dir, const char *oldest_file_name, mDNSs32 dump_file_count,
48 mDNSs32 max_allowed);
49 mDNSlocal int create_new_state_dump_file(const char *dump_dir, const char *file_name, char *full_file_name, mDNSu32 buffer_len);
50 mDNSlocal mDNSs8 handle_state_dump_to_fd(const char *dump_dir, const char *file_name, char *full_file_name, mDNSu32 buffer_len,
51 mDNSBool if_compress);
52 mDNSlocal mDNSs8 compress_state_dump_and_delete(char *input_file, mDNSu32 buffer_len);
53 mDNSlocal mDNSs32 timediff_ms(struct timeval* t1, struct timeval* t2);
54
55 // function definition
56 mDNSexport void init_log_utility_service(void)
57 {
58 xpc_connection_t log_utility_listener = xpc_connection_create_mach_service(kDNSLogUtilityService, NULL, XPC_CONNECTION_MACH_SERVICE_LISTENER);
59 if (!log_utility_listener || xpc_get_type(log_utility_listener) != XPC_TYPE_CONNECTION) {
60 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_ERROR, "Error Creating XPC Listener for Log Utility Server!");
61 return;
62 }
63
64 log_utility_server_queue = dispatch_queue_create("com.apple.mDNSResponder.log_utility_server_queue", NULL);
65
66 xpc_connection_set_event_handler(log_utility_listener, ^(xpc_object_t eventmsg) {
67 xpc_type_t type = xpc_get_type(eventmsg);
68
69 if (type == XPC_TYPE_CONNECTION) {
70 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_INFO, "C%p {action='receives connection'}", eventmsg);
71 accept_client(eventmsg);
72 } else if (type == XPC_TYPE_ERROR) {
73 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_ERROR, "C%p {xpc_error=\n" PUB_S "\n}", eventmsg,
74 xpc_dictionary_get_string(eventmsg, XPC_ERROR_KEY_DESCRIPTION));
75 } else {
76 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_ERROR, "C%p {error='receives unknown xpc request'}", eventmsg);
77 }
78 });
79
80 xpc_connection_resume(log_utility_listener);
81 }
82
83 mDNSlocal void accept_client(xpc_connection_t conn)
84 {
85 xpc_retain(conn);
86 xpc_connection_set_target_queue(conn, log_utility_server_queue);
87 xpc_connection_set_event_handler(conn, ^(xpc_object_t req_msg) {
88 xpc_type_t type = xpc_get_type(req_msg);
89
90 if (type == XPC_TYPE_DICTIONARY) {
91 handle_requests(req_msg);
92 } else { // We hit this case ONLY if Client Terminated Connection OR Crashed
93 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT, "C%p {status='client closed the connection'}", conn);
94 xpc_release(conn);
95 }
96 });
97
98 xpc_connection_resume(conn);
99 }
100
101 mDNSlocal mDNSs8 handle_requests(xpc_object_t req)
102 {
103 mDNSs8 ret = 0;
104 xpc_connection_t remote_conn = xpc_dictionary_get_remote_connection(req);
105
106 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_INFO, "C%p {action='handling log utility request'}", remote_conn);
107
108 // create the dictionary for response purpose
109 xpc_object_t response = xpc_dictionary_create_reply(req);
110 if (response == mDNSNULL) {
111 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_ERROR, "C%p {error='cannot create reply response dictionary'}", remote_conn);
112 return -1;
113 }
114
115 mDNSu32 reply_value;
116 ret = check_permission(remote_conn);
117 if (ret < 0) {
118 // permission error
119 reply_value = kDNSMsg_Error;
120 if (ret == -1) {
121 xpc_dictionary_set_string(response, kDNSErrorDescription, "Client must be running as root");
122 } else if (ret == -2) {
123 xpc_dictionary_set_string(response, kDNSErrorDescription, "Client is missing the entitlement");
124 }
125 } else if (xpc_dictionary_get_uint64(req, kDNSStateDump)) {
126 mDNSu32 dump_option = (mDNSs32)xpc_dictionary_get_uint64(req, kDNSStateDump);
127 char full_file_name[PATH_MAX];
128 mDNSs32 time_used;
129
130 // We do not dump state in the customer build due to privacy consideration.
131 if (IsAppleInternalBuild()) {
132 int client_fd = xpc_dictionary_dup_fd(req, kDNSStateDumpFD);
133 ret = handle_state_dump(dump_option, full_file_name, sizeof(full_file_name), client_fd, &time_used);
134 if (ret == 0) {
135 reply_value = kDNSMsg_NoError;
136 xpc_dictionary_set_int64(response, kDNSStateDumpTimeUsed, time_used);
137
138 if (dump_option != full_state_to_stdout) {
139 xpc_dictionary_set_string(response, kDNSDumpFilePath, full_file_name);
140 }
141 } else {
142 reply_value = kDNSMsg_Error;
143 xpc_dictionary_set_string(response, kDNSErrorDescription, "State dump fails");
144 }
145 close(client_fd);
146 } else {
147 reply_value = kDNSMsg_Error;
148 xpc_dictionary_set_string(response, kDNSErrorDescription, "State dump is only enabled in internal builds");
149 }
150 } else {
151 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_ERROR,
152 "C%p {error='unknown log utility request from client'}", remote_conn);
153 reply_value = kDNSMsg_UnknownRequest;
154 xpc_dictionary_set_string(response, kDNSErrorDescription, "unknown log utility request from client");
155 }
156
157 xpc_dictionary_set_uint64(response, kDNSDaemonReply, reply_value);
158 xpc_connection_send_message(remote_conn, response);
159 xpc_release(response);
160
161 return 0;
162 }
163
164 mDNSlocal mDNSs8 check_permission(xpc_connection_t connection)
165 {
166 uid_t client_euid = xpc_connection_get_euid(connection);
167 int client_pid = xpc_connection_get_pid(connection);
168 mDNSs8 ret = 0;
169
170 if (client_euid != 0) {
171 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT,
172 "C%p {client_pid=%d,error='not running as root'}", connection, client_pid);
173 ret = -1;
174 }
175
176 if (!IsEntitled(connection, kDNSLogUtilityService)){
177 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT,
178 "C%p {client_pid=%d,error='Client is missing entitlement'}", connection, client_pid);
179 ret = -2;
180 }
181
182 return ret;
183 }
184
185 /*
186 * pointers of full_file_name and time_used are passed into, when function returns, full_file_name will be filled
187 * with the full path to the dumped file, and time_used is filled with time duration(ms) when mDNSResponder is
188 * blocked. if_get_lock indicates if we should lock the kqueue before dumping the state.
189 */
190 mDNSexport mDNSs8 handle_state_dump(mDNSu32 dump_option, char *full_file_name, mDNSu32 name_buffer_len,
191 int client_fd, mDNSs32 *time_ms_used)
192 {
193 mDNSs8 ret;
194
195 // record the start time, and lock the kqueue
196 struct timeval time_start;
197 gettimeofday(&time_start, mDNSNULL);
198 KQueueLock();
199
200 if (dump_option == full_state_to_stdout) {
201 dump_state_to_fd(client_fd);
202 ret = 0;
203 } else {
204 // dump_option == full_state || dump_option == full_state_with_compression
205 ret = handle_state_dump_to_fd(MDSNRESPONDER_STATE_DUMP_DIR, MDSNRESPONDER_STATE_DUMP_FILE_NAME,
206 full_file_name, name_buffer_len,
207 dump_option == full_state_with_compression ? mDNStrue : mDNSfalse);
208 }
209
210 // unlock the kqueue, record the end time and calculate the duration.
211 KQueueUnlock("State Dump");
212 struct timeval time_end;
213 gettimeofday(&time_end, mDNSNULL);
214 *time_ms_used = timediff_ms(&time_end, &time_start);
215
216 return ret;
217 }
218
219 #define MAX_NUM_DUMP_FILES 5 // controls how many files we are allowed to created for the state dump
220 mDNSlocal mDNSs8 handle_state_dump_to_fd(const char *dump_dir, const char *file_name, char *full_file_name, mDNSu32 name_buffer_len,
221 mDNSBool if_compress)
222 {
223 char oldest_file_name[PATH_MAX];
224 mDNSs32 dump_file_count = 0;
225 int ret;
226
227 dump_file_count = find_oldest_state_dump(dump_dir, file_name, full_file_name, name_buffer_len, oldest_file_name);
228 require_action(dump_file_count >= 0, error,
229 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "find_oldest_state_dump fails"));
230
231 ret = remove_state_dump_if_too_many(dump_dir, oldest_file_name, dump_file_count, MAX_NUM_DUMP_FILES);
232 require_action(ret == 0, error,
233 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "remove_state_dump_if_too_many fails"));
234
235 int fd = create_new_state_dump_file(dump_dir, file_name, full_file_name, name_buffer_len);
236 require_action(fd >= 0, error,
237 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "create_new_state_dump_file fails"));
238
239 dump_state_to_fd(fd);
240 close(fd); // create_new_state_dump_file open the file, we have to close it here
241
242 if (if_compress) {
243 ret = compress_state_dump_and_delete(full_file_name, name_buffer_len);
244 require_action(ret == 0, error,
245 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT, "State Dump: Error happens when trying to compress the state dump, reason: %s", strerror(errno)));
246 }
247
248 return 0;
249
250 error:
251 return -1;
252 }
253
254 /*
255 * Scan the directory, find all the files that start with <mDNSResponder state dump file name>. Return the number of
256 * state dump files and the name of the oldest file created.
257 */
258 mDNSlocal mDNSs32 find_oldest_state_dump(const char *dump_dir, const char *file_name, char *full_file_name, mDNSu32 buffer_len,
259 char *oldest_file_name)
260 {
261 int ret;
262
263 full_file_name[0] = '\0';
264 full_file_name[buffer_len - 1]= '\0';
265 snprintf(full_file_name, buffer_len - 1, "%s/%s", dump_dir, file_name);
266
267 DIR *dir_p = opendir(dump_dir);
268 if (dir_p == mDNSNULL) {
269 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT,
270 "State Dump: directory " PUB_S " cannot be opened, reason: " PUB_S, dump_dir, strerror(errno));
271 return -1;
272 }
273
274 // scan every entry under directory, if starts with <mDNSResponder state dump file name>, check its create time.
275 struct dirent *dir_entry_p = mDNSNULL;
276 mDNSu32 file_name_len = strnlen(file_name, MAXPATHLEN);
277 mDNSu8 dump_file_count = 0;
278 struct timespec oldest_time = {LONG_MAX, LONG_MAX};
279
280 while ((dir_entry_p = readdir(dir_p)) != mDNSNULL) {
281 if (dir_entry_p->d_namlen <= file_name_len)
282 continue;
283
284 if (strncmp(dir_entry_p->d_name, file_name, file_name_len) == 0) {
285 struct stat file_state;
286 snprintf(full_file_name, buffer_len - 1, "%s/%s", dump_dir, dir_entry_p->d_name);
287
288 // use stat to get creation time
289 ret = stat(full_file_name, &file_state);
290 if (ret != 0) {
291 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT,
292 "State Dump: error when reading file properties, reason: " PUB_S, strerror(errno));
293 return -1;
294 }
295 // if the file is older than the current record
296 if (oldest_time.tv_sec > file_state.st_birthtimespec.tv_sec
297 || (oldest_time.tv_sec == file_state.st_birthtimespec.tv_sec
298 && oldest_time.tv_sec > file_state.st_birthtimespec.tv_sec)) {
299 oldest_time = file_state.st_birthtimespec;
300 strncpy(oldest_file_name, dir_entry_p->d_name, MIN(PATH_MAX - 1, dir_entry_p->d_namlen + 1));
301 }
302
303 dump_file_count++;
304 }
305 }
306 closedir(dir_p);
307
308 return dump_file_count;
309 }
310
311 mDNSlocal mDNSs8 remove_state_dump_if_too_many(const char *dump_dir, const char *oldest_file_name, mDNSs32 dump_file_count,
312 mDNSs32 max_allowed)
313 {
314 char path_file_to_remove[PATH_MAX];
315 path_file_to_remove[PATH_MAX - 1] = '\0';
316 // If the number of state dump files has reached the maximum value, we delete the oldest one.
317 if (dump_file_count == max_allowed) {
318 // construct the full name
319 snprintf(path_file_to_remove, PATH_MAX - 1, "%s/%s", dump_dir, oldest_file_name);
320 if (remove(path_file_to_remove) != 0) {
321 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEFAULT,
322 "State Dump: file " PUB_S " cannot be deleted, reason: " PUB_S, path_file_to_remove, strerror(errno));
323 return -1;
324 }
325 }
326
327 return 0;
328 }
329
330 /*
331 * Generate the file name of state dump with current time stamp, and return the FILE pointer, anyone who calls this
332 * function must call fclose() to release the FILE pointer.
333 */
334 mDNSlocal int create_new_state_dump_file(const char *dump_dir, const char *file_name, char *full_file_name, mDNSu32 buffer_len)
335 {
336 struct timeval now;
337 struct tm local_time;
338 char date_time_str[32];
339 char time_zone_str[32];
340
341 gettimeofday(&now, NULL);
342 localtime_r(&now.tv_sec, &local_time);
343
344 // 2008-08-08_20-00-00
345 strftime(date_time_str, sizeof(date_time_str), "%F_%H-%M-%S", &local_time);
346 // +0800
347 strftime(time_zone_str, sizeof(time_zone_str), "%z", &local_time);
348 // /private/var/log/mDNSResponder/mDNSResponder_state_dump_2008-08-08_20-00-00-000000+0800.txt
349 snprintf(full_file_name, buffer_len, "%s/%s_%s-%06lu%s." STATE_DUMP_PLAIN_SUFFIX,
350 dump_dir, file_name, date_time_str, (unsigned long)now.tv_usec, time_zone_str);
351
352 int fd = open(full_file_name, O_WRONLY | O_CREAT, 0644); // 0644 means * (owning) User: read & write * Group: read * Other: read
353 if (fd < 0) {
354 LogRedact(MDNS_LOG_CATEGORY_DEFAULT, MDNS_LOG_DEFAULT,
355 "State Dump: file " PUB_S " cannot be opened, reason: " PUB_S, full_file_name, strerror(errno));
356 return -1;
357 }
358
359 return fd;
360 }
361
362 /*
363 * Compress the state dump from pliantext to tar.bz2, remove the original one, and change the content of input_file to
364 * newly created compressed file if compression succeeds.
365 */
366 mDNSlocal mDNSs8 compress_state_dump_and_delete(char *input_file, mDNSu32 buffer_len)
367 {
368 struct archive *a = mDNSNULL;
369 struct archive_entry *entry = mDNSNULL;
370 struct stat st;
371 int fd = -1;
372 char output_file[PATH_MAX];
373 void *mapped_pointer = mDNSNULL;
374 int ret;
375
376 output_file[PATH_MAX - 1] = '\0';
377
378 a = archive_write_new();
379 require_action(a != mDNSNULL, error,
380 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "archive_write_new fails: " PUB_S, archive_error_string(a)));
381 archive_write_add_filter_bzip2(a);
382 archive_write_set_format_ustar(a);
383
384 // remove the .txt suffix, and append .tar.bz2 suffix
385 mDNSu32 plain_file_name_len = strlen(input_file); // input_file is guaranteed to be '\0'-terminated
386 strncpy(output_file, input_file, plain_file_name_len - sizeof(STATE_DUMP_PLAIN_SUFFIX));
387 output_file[plain_file_name_len - sizeof(STATE_DUMP_PLAIN_SUFFIX)] = '\0';
388 strncat(output_file, "." STATE_DUMP_COMPRESSED_SUFFIX, 1 + sizeof(STATE_DUMP_COMPRESSED_SUFFIX));
389
390 // open/create the archive for the given path name
391 ret = archive_write_open_filename(a, output_file);
392 require_action(ret == ARCHIVE_OK, error,
393 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "archive_write_open_filename fails: " PUB_S, archive_error_string(a)));
394
395 // get the state of file to be compressed
396 stat(input_file, &st);
397
398 // entry is required to create an archive
399 entry = archive_entry_new();
400 require_action(entry != mDNSNULL, error,
401 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "archive_entry_new fails: " PUB_S, strerror(errno)));
402
403 // set the name of file in the compressed file
404 const char *file_name_with_timestamp = strstr(input_file, MDSNRESPONDER_STATE_DUMP_FILE_NAME);
405 if (file_name_with_timestamp == mDNSNULL) {
406 file_name_with_timestamp = MDSNRESPONDER_STATE_DUMP_FILE_NAME "." STATE_DUMP_PLAIN_SUFFIX;
407 }
408
409 // copy the original file state to entry
410 archive_entry_copy_stat(entry, &st);
411 archive_entry_set_pathname(entry, file_name_with_timestamp);
412
413 // write entry into archive
414 do {
415 ret = archive_write_header(a, entry);
416 } while (ret == ARCHIVE_RETRY);
417 require_action(ret == ARCHIVE_OK, error,
418 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "archive_write_header fails: " PUB_S, archive_error_string(a)));
419
420 // if the original file has something to compress, use mmap to read its content
421 if (st.st_size > 0) {
422 fd = open(input_file, O_RDONLY);
423
424 mapped_pointer = mmap(NULL, st.st_size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0);
425 require_action(mapped_pointer != MAP_FAILED, error,
426 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "mmap fails: " PUB_S, strerror(errno)));
427
428 mDNSu32 amount_written = (mDNSu32)archive_write_data(a, mapped_pointer, st.st_size);
429 require_action(amount_written == (mDNSu32)st.st_size, error,
430 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "archive_write_data fails: amount_written(%u) != (%u)", amount_written, (mDNSu32)st.st_size));
431
432 int munmap_result = munmap(mapped_pointer, st.st_size);
433 require_action(munmap_result == 0, error,
434 LogRedact(MDNS_LOG_CATEGORY_XPC, MDNS_LOG_DEBUG, "munmap fails: " PUB_S, strerror(errno)));
435 mapped_pointer = mDNSNULL;
436
437 close(fd);
438 fd = -1; // set the file descriptor to -1 to avoid double free
439 }
440 archive_entry_free(entry);
441 entry = mDNSNULL;
442
443 archive_write_close(a);
444 archive_write_free(a);
445 a = mDNSNULL;
446
447 // remove the original one, return the newly created compressed file name
448 remove(input_file);
449 strncpy(input_file, output_file, buffer_len);
450 input_file[buffer_len - 1] = '\0';
451
452 return 0;
453
454 error:
455 if (a != mDNSNULL) {
456 archive_write_close(a);
457 archive_write_free(a);
458 }
459 if (entry != mDNSNULL) {
460 archive_entry_free(entry);
461 }
462 if (fd != -1) {
463 close(fd);
464 }
465 if (mapped_pointer != mDNSNULL) {
466 munmap(mapped_pointer, st.st_size);
467 }
468 remove(input_file);
469 return -1;
470 }
471
472 /*
473 * Return the time difference(ms) between two struct timeval.
474 */
475 #define US_PER_S 1000000
476 #define MS_PER_S 1000
477 mDNSlocal mDNSs32 timediff_ms(struct timeval* t1, struct timeval* t2)
478 {
479 int usec, ms, sec;
480
481 if (t1->tv_sec < t2->tv_sec || (t1->tv_sec == t2->tv_sec && t1->tv_usec < t2->tv_usec))
482 return -timediff_ms(t2, t1);
483
484 sec = (int)(t1->tv_sec - t2->tv_sec);
485 if (t1->tv_usec >= t2->tv_usec)
486 usec = t1->tv_usec - t2->tv_usec;
487 else {
488 usec = t1->tv_usec + US_PER_S - t2->tv_usec;
489 sec -= 1;
490 }
491 ms = sec * MS_PER_S;
492 ms += usec / MS_PER_S;
493 return ms;
494 }