2 * Copyright (c) 2019 Apple Inc. All rights reserved.
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
18 #include <dirent.h> // opendir
19 #include <sys/stat.h> // stat
21 #include <archive_entry.h>
22 #include <AssertMacros.h> // require, require_action
24 #include "mDNSMacOSX.h"
26 #include "xpc_services.h"
27 #include "xpc_service_log_utility.h"
28 #include "xpc_clients.h"
29 #include "system_utilities.h" // IsAppleInternalBuild
31 #define STATE_DUMP_PLAIN_SUFFIX "txt"
32 #define STATE_DUMP_COMPRESSED_SUFFIX "tar.bz2"
35 extern mDNS mDNSStorage
;
36 static dispatch_queue_t log_utility_server_queue
= NULL
;
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
,
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
);
55 // function definition
56 mDNSexport
void init_log_utility_service(void)
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!");
64 log_utility_server_queue
= dispatch_queue_create("com.apple.mDNSResponder.log_utility_server_queue", NULL
);
66 xpc_connection_set_event_handler(log_utility_listener
, ^(xpc_object_t eventmsg
) {
67 xpc_type_t type
= xpc_get_type(eventmsg
);
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
));
76 LogRedact(MDNS_LOG_CATEGORY_XPC
, MDNS_LOG_ERROR
, "C%p {error='receives unknown xpc request'}", eventmsg
);
80 xpc_connection_resume(log_utility_listener
);
83 mDNSlocal
void accept_client(xpc_connection_t 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
);
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
);
98 xpc_connection_resume(conn
);
101 mDNSlocal mDNSs8
handle_requests(xpc_object_t req
)
104 xpc_connection_t remote_conn
= xpc_dictionary_get_remote_connection(req
);
106 LogRedact(MDNS_LOG_CATEGORY_XPC
, MDNS_LOG_INFO
, "C%p {action='handling log utility request'}", remote_conn
);
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
);
116 ret
= check_permission(remote_conn
);
119 reply_value
= kDNSMsg_Error
;
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");
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
];
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
);
135 reply_value
= kDNSMsg_NoError
;
136 xpc_dictionary_set_int64(response
, kDNSStateDumpTimeUsed
, time_used
);
138 if (dump_option
!= full_state_to_stdout
) {
139 xpc_dictionary_set_string(response
, kDNSDumpFilePath
, full_file_name
);
142 reply_value
= kDNSMsg_Error
;
143 xpc_dictionary_set_string(response
, kDNSErrorDescription
, "State dump fails");
147 reply_value
= kDNSMsg_Error
;
148 xpc_dictionary_set_string(response
, kDNSErrorDescription
, "State dump is only enabled in internal builds");
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");
157 xpc_dictionary_set_uint64(response
, kDNSDaemonReply
, reply_value
);
158 xpc_connection_send_message(remote_conn
, response
);
159 xpc_release(response
);
164 mDNSlocal mDNSs8
check_permission(xpc_connection_t connection
)
166 uid_t client_euid
= xpc_connection_get_euid(connection
);
167 int client_pid
= xpc_connection_get_pid(connection
);
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
);
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
);
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.
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
)
195 // record the start time, and lock the kqueue
196 struct timeval time_start
;
197 gettimeofday(&time_start
, mDNSNULL
);
200 if (dump_option
== full_state_to_stdout
) {
201 dump_state_to_fd(client_fd
);
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
);
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
);
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
)
223 char oldest_file_name
[PATH_MAX
];
224 mDNSs32 dump_file_count
= 0;
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"));
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"));
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"));
239 dump_state_to_fd(fd
);
240 close(fd
); // create_new_state_dump_file open the file, we have to close it here
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
)));
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.
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
)
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
);
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
));
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
};
280 while ((dir_entry_p
= readdir(dir_p
)) != mDNSNULL
) {
281 if (dir_entry_p
->d_namlen
<= file_name_len
)
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
);
288 // use stat to get creation time
289 ret
= stat(full_file_name
, &file_state
);
291 LogRedact(MDNS_LOG_CATEGORY_XPC
, MDNS_LOG_DEFAULT
,
292 "State Dump: error when reading file properties, reason: " PUB_S
, strerror(errno
));
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));
308 return dump_file_count
;
311 mDNSlocal mDNSs8
remove_state_dump_if_too_many(const char *dump_dir
, const char *oldest_file_name
, mDNSs32 dump_file_count
,
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
));
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.
334 mDNSlocal
int create_new_state_dump_file(const char *dump_dir
, const char *file_name
, char *full_file_name
, mDNSu32 buffer_len
)
337 struct tm local_time
;
338 char date_time_str
[32];
339 char time_zone_str
[32];
341 gettimeofday(&now
, NULL
);
342 localtime_r(&now
.tv_sec
, &local_time
);
344 // 2008-08-08_20-00-00
345 strftime(date_time_str
, sizeof(date_time_str
), "%F_%H-%M-%S", &local_time
);
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
);
352 int fd
= open(full_file_name
, O_WRONLY
| O_CREAT
, 0644); // 0644 means * (owning) User: read & write * Group: read * Other: read
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
));
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.
366 mDNSlocal mDNSs8
compress_state_dump_and_delete(char *input_file
, mDNSu32 buffer_len
)
368 struct archive
*a
= mDNSNULL
;
369 struct archive_entry
*entry
= mDNSNULL
;
372 char output_file
[PATH_MAX
];
373 void *mapped_pointer
= mDNSNULL
;
376 output_file
[PATH_MAX
- 1] = '\0';
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
);
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
));
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
)));
395 // get the state of file to be compressed
396 stat(input_file
, &st
);
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
)));
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
;
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
);
413 // write entry into archive
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
)));
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
);
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
)));
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
));
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
;
438 fd
= -1; // set the file descriptor to -1 to avoid double free
440 archive_entry_free(entry
);
443 archive_write_close(a
);
444 archive_write_free(a
);
447 // remove the original one, return the newly created compressed file name
449 strncpy(input_file
, output_file
, buffer_len
);
450 input_file
[buffer_len
- 1] = '\0';
456 archive_write_close(a
);
457 archive_write_free(a
);
459 if (entry
!= mDNSNULL
) {
460 archive_entry_free(entry
);
465 if (mapped_pointer
!= mDNSNULL
) {
466 munmap(mapped_pointer
, st
.st_size
);
473 * Return the time difference(ms) between two struct timeval.
475 #define US_PER_S 1000000
476 #define MS_PER_S 1000
477 mDNSlocal mDNSs32
timediff_ms(struct timeval
* t1
, struct timeval
* t2
)
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
);
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
;
488 usec
= t1
->tv_usec
+ US_PER_S
- t2
->tv_usec
;
492 ms
+= usec
/ MS_PER_S
;