xref: /freebsd/lib/libc/gen/syslog.c (revision a0409676120c1e558d0ade943019934e0f15118d)
1 /*-
2  * SPDX-License-Identifier: BSD-3-Clause
3  *
4  * Copyright (c) 1983, 1988, 1993
5  *	The Regents of the University of California.  All rights reserved.
6  *
7  * Redistribution and use in source and binary forms, with or without
8  * modification, are permitted provided that the following conditions
9  * are met:
10  * 1. Redistributions of source code must retain the above copyright
11  *    notice, this list of conditions and the following disclaimer.
12  * 2. Redistributions in binary form must reproduce the above copyright
13  *    notice, this list of conditions and the following disclaimer in the
14  *    documentation and/or other materials provided with the distribution.
15  * 3. Neither the name of the University nor the names of its contributors
16  *    may be used to endorse or promote products derived from this software
17  *    without specific prior written permission.
18  *
19  * THIS SOFTWARE IS PROVIDED BY THE REGENTS 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 REGENTS 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 #include <sys/cdefs.h>
33 __SCCSID("@(#)syslog.c	8.5 (Berkeley) 4/29/95");
34 __FBSDID("$FreeBSD$");
35 
36 #include "namespace.h"
37 #include <sys/param.h>
38 #include <sys/socket.h>
39 #include <sys/syslog.h>
40 #include <sys/time.h>
41 #include <sys/uio.h>
42 #include <sys/un.h>
43 #include <netdb.h>
44 
45 #include <errno.h>
46 #include <fcntl.h>
47 #include <paths.h>
48 #include <pthread.h>
49 #include <stdio.h>
50 #include <stdlib.h>
51 #include <string.h>
52 #include <time.h>
53 #include <unistd.h>
54 
55 #include <stdarg.h>
56 #include "un-namespace.h"
57 
58 #include "libc_private.h"
59 
60 static int	LogFile = -1;		/* fd for log */
61 static int	status;			/* connection status */
62 static int	opened;			/* have done openlog() */
63 static int	LogStat = 0;		/* status bits, set by openlog() */
64 static const char *LogTag = NULL;	/* string to tag the entry with */
65 static int	LogFacility = LOG_USER;	/* default facility code */
66 static int	LogMask = 0xff;		/* mask of priorities to be logged */
67 static pthread_mutex_t	syslog_mutex = PTHREAD_MUTEX_INITIALIZER;
68 
69 #define	THREAD_LOCK()							\
70 	do { 								\
71 		if (__isthreaded) _pthread_mutex_lock(&syslog_mutex);	\
72 	} while(0)
73 #define	THREAD_UNLOCK()							\
74 	do {								\
75 		if (__isthreaded) _pthread_mutex_unlock(&syslog_mutex);	\
76 	} while(0)
77 
78 /* RFC5424 defined value. */
79 #define NILVALUE "-"
80 
81 static void	disconnectlog(void); /* disconnect from syslogd */
82 static void	connectlog(void);	/* (re)connect to syslogd */
83 static void	openlog_unlocked(const char *, int, int);
84 
85 enum {
86 	NOCONN = 0,
87 	CONNDEF,
88 	CONNPRIV,
89 };
90 
91 /*
92  * Format of the magic cookie passed through the stdio hook
93  */
94 struct bufcookie {
95 	char	*base;	/* start of buffer */
96 	int	left;
97 };
98 
99 /*
100  * stdio write hook for writing to a static string buffer
101  * XXX: Maybe one day, dynamically allocate it so that the line length
102  *      is `unlimited'.
103  */
104 static int
105 writehook(void *cookie, const char *buf, int len)
106 {
107 	struct bufcookie *h;	/* private `handle' */
108 
109 	h = (struct bufcookie *)cookie;
110 	if (len > h->left) {
111 		/* clip in case of wraparound */
112 		len = h->left;
113 	}
114 	if (len > 0) {
115 		(void)memcpy(h->base, buf, len); /* `write' it. */
116 		h->base += len;
117 		h->left -= len;
118 	}
119 	return len;
120 }
121 
122 /*
123  * syslog, vsyslog --
124  *	print message on log file; output is intended for syslogd(8).
125  */
126 void
127 syslog(int pri, const char *fmt, ...)
128 {
129 	va_list ap;
130 
131 	va_start(ap, fmt);
132 	vsyslog(pri, fmt, ap);
133 	va_end(ap);
134 }
135 
136 static void
137 vsyslog1(int pri, const char *fmt, va_list ap)
138 {
139 	struct timeval now;
140 	struct tm tm;
141 	char ch, *p;
142 	long tz_offset;
143 	int cnt, fd, saved_errno;
144 	char hostname[MAXHOSTNAMELEN], *stdp, tbuf[2048], fmt_cpy[1024],
145 	    errstr[64], tz_sign;
146 	FILE *fp, *fmt_fp;
147 	struct bufcookie tbuf_cookie;
148 	struct bufcookie fmt_cookie;
149 
150 #define	INTERNALLOG	LOG_ERR|LOG_CONS|LOG_PERROR|LOG_PID
151 	/* Check for invalid bits. */
152 	if (pri & ~(LOG_PRIMASK|LOG_FACMASK)) {
153 		syslog(INTERNALLOG,
154 		    "syslog: unknown facility/priority: %x", pri);
155 		pri &= LOG_PRIMASK|LOG_FACMASK;
156 	}
157 
158 	saved_errno = errno;
159 
160 	/* Check priority against setlogmask values. */
161 	if (!(LOG_MASK(LOG_PRI(pri)) & LogMask))
162 		return;
163 
164 	/* Set default facility if none specified. */
165 	if ((pri & LOG_FACMASK) == 0)
166 		pri |= LogFacility;
167 
168 	/* Create the primary stdio hook */
169 	tbuf_cookie.base = tbuf;
170 	tbuf_cookie.left = sizeof(tbuf);
171 	fp = fwopen(&tbuf_cookie, writehook);
172 	if (fp == NULL)
173 		return;
174 
175 	/* Build the message according to RFC 5424. Tag and version. */
176 	(void)fprintf(fp, "<%d>1 ", pri);
177 	/* Timestamp similar to RFC 3339. */
178 	if (gettimeofday(&now, NULL) == 0 &&
179 	    localtime_r(&now.tv_sec, &tm) != NULL) {
180 		if (tm.tm_gmtoff < 0) {
181 			tz_sign = '-';
182 			tz_offset = -tm.tm_gmtoff;
183 		} else {
184 			tz_sign = '+';
185 			tz_offset = tm.tm_gmtoff;
186 		}
187 
188 		(void)fprintf(fp,
189 		    "%04d-%02d-%02d"		/* Date. */
190 		    "T%02d:%02d:%02d.%06ld"	/* Time. */
191 		    "%c%02ld:%02ld ",		/* Time zone offset. */
192 		    tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
193 		    tm.tm_hour, tm.tm_min, tm.tm_sec, now.tv_usec,
194 		    tz_sign, tz_offset / 3600, (tz_offset % 3600) / 60);
195 	} else
196 		(void)fputs(NILVALUE " ", fp);
197 	/* Hostname. */
198 	(void)gethostname(hostname, sizeof(hostname));
199 	(void)fprintf(fp, "%s ",
200 	    hostname[0] == '\0' ? NILVALUE : hostname);
201 	if (LogStat & LOG_PERROR) {
202 		/* Transfer to string buffer */
203 		(void)fflush(fp);
204 		stdp = tbuf + (sizeof(tbuf) - tbuf_cookie.left);
205 	}
206 	/* Application name. */
207 	if (LogTag == NULL)
208 		LogTag = _getprogname();
209 	(void)fprintf(fp, "%s ", LogTag == NULL ? NILVALUE : LogTag);
210 	/*
211 	 * Provide the process ID regardless of whether LOG_PID has been
212 	 * specified, as it provides valuable information. Many
213 	 * applications tend not to use this, even though they should.
214 	 */
215 	(void)fprintf(fp, "%d ", getpid());
216 	/* Message ID. */
217 	(void)fputs(NILVALUE " ", fp);
218 	/* Structured data. */
219 	(void)fputs(NILVALUE " ", fp);
220 
221 	/* Check to see if we can skip expanding the %m */
222 	if (strstr(fmt, "%m")) {
223 
224 		/* Create the second stdio hook */
225 		fmt_cookie.base = fmt_cpy;
226 		fmt_cookie.left = sizeof(fmt_cpy) - 1;
227 		fmt_fp = fwopen(&fmt_cookie, writehook);
228 		if (fmt_fp == NULL) {
229 			fclose(fp);
230 			return;
231 		}
232 
233 		/*
234 		 * Substitute error message for %m.  Be careful not to
235 		 * molest an escaped percent "%%m".  We want to pass it
236 		 * on untouched as the format is later parsed by vfprintf.
237 		 */
238 		for ( ; (ch = *fmt); ++fmt) {
239 			if (ch == '%' && fmt[1] == 'm') {
240 				++fmt;
241 				strerror_r(saved_errno, errstr, sizeof(errstr));
242 				fputs(errstr, fmt_fp);
243 			} else if (ch == '%' && fmt[1] == '%') {
244 				++fmt;
245 				fputc(ch, fmt_fp);
246 				fputc(ch, fmt_fp);
247 			} else {
248 				fputc(ch, fmt_fp);
249 			}
250 		}
251 
252 		/* Null terminate if room */
253 		fputc(0, fmt_fp);
254 		fclose(fmt_fp);
255 
256 		/* Guarantee null termination */
257 		fmt_cpy[sizeof(fmt_cpy) - 1] = '\0';
258 
259 		fmt = fmt_cpy;
260 	}
261 
262 	/* Message. */
263 	(void)vfprintf(fp, fmt, ap);
264 	(void)fclose(fp);
265 
266 	cnt = sizeof(tbuf) - tbuf_cookie.left;
267 
268 	/* Remove a trailing newline */
269 	if (tbuf[cnt - 1] == '\n')
270 		cnt--;
271 
272 	/* Output to stderr if requested. */
273 	if (LogStat & LOG_PERROR) {
274 		struct iovec iov[2];
275 		struct iovec *v = iov;
276 
277 		v->iov_base = stdp;
278 		v->iov_len = cnt - (stdp - tbuf);
279 		++v;
280 		v->iov_base = "\n";
281 		v->iov_len = 1;
282 		(void)_writev(STDERR_FILENO, iov, 2);
283 	}
284 
285 	/* Get connected, output the message to the local logger. */
286 	if (!opened)
287 		openlog_unlocked(LogTag, LogStat | LOG_NDELAY, 0);
288 	connectlog();
289 
290 	/*
291 	 * If the send() fails, there are two likely scenarios:
292 	 *  1) syslogd was restarted
293 	 *  2) /var/run/log is out of socket buffer space, which
294 	 *     in most cases means local DoS.
295 	 * If the error does not indicate a full buffer, we address
296 	 * case #1 by attempting to reconnect to /var/run/log[priv]
297 	 * and resending the message once.
298 	 *
299 	 * If we are working with a privileged socket, the retry
300 	 * attempts end there, because we don't want to freeze a
301 	 * critical application like su(1) or sshd(8).
302 	 *
303 	 * Otherwise, we address case #2 by repeatedly retrying the
304 	 * send() to give syslogd a chance to empty its socket buffer.
305 	 */
306 
307 	if (send(LogFile, tbuf, cnt, 0) < 0) {
308 		if (errno != ENOBUFS) {
309 			/*
310 			 * Scenario 1: syslogd was restarted
311 			 * reconnect and resend once
312 			 */
313 			disconnectlog();
314 			connectlog();
315 			if (send(LogFile, tbuf, cnt, 0) >= 0)
316 				return;
317 			/*
318 			 * if the resend failed, fall through to
319 			 * possible scenario 2
320 			 */
321 		}
322 		while (errno == ENOBUFS) {
323 			/*
324 			 * Scenario 2: out of socket buffer space
325 			 * possible DoS, fail fast on a privileged
326 			 * socket
327 			 */
328 			if (status == CONNPRIV)
329 				break;
330 			_usleep(1);
331 			if (send(LogFile, tbuf, cnt, 0) >= 0)
332 				return;
333 		}
334 	} else
335 		return;
336 
337 	/*
338 	 * Output the message to the console; try not to block
339 	 * as a blocking console should not stop other processes.
340 	 * Make sure the error reported is the one from the syslogd failure.
341 	 */
342 	if (LogStat & LOG_CONS &&
343 	    (fd = _open(_PATH_CONSOLE, O_WRONLY|O_NONBLOCK|O_CLOEXEC, 0)) >=
344 	    0) {
345 		struct iovec iov[2];
346 		struct iovec *v = iov;
347 
348 		p = strchr(tbuf, '>') + 3;
349 		v->iov_base = p;
350 		v->iov_len = cnt - (p - tbuf);
351 		++v;
352 		v->iov_base = "\r\n";
353 		v->iov_len = 2;
354 		(void)_writev(fd, iov, 2);
355 		(void)_close(fd);
356 	}
357 }
358 
359 static void
360 syslog_cancel_cleanup(void *arg __unused)
361 {
362 
363 	THREAD_UNLOCK();
364 }
365 
366 void
367 vsyslog(int pri, const char *fmt, va_list ap)
368 {
369 
370 	THREAD_LOCK();
371 	pthread_cleanup_push(syslog_cancel_cleanup, NULL);
372 	vsyslog1(pri, fmt, ap);
373 	pthread_cleanup_pop(1);
374 }
375 
376 /* Should be called with mutex acquired */
377 static void
378 disconnectlog(void)
379 {
380 	/*
381 	 * If the user closed the FD and opened another in the same slot,
382 	 * that's their problem.  They should close it before calling on
383 	 * system services.
384 	 */
385 	if (LogFile != -1) {
386 		_close(LogFile);
387 		LogFile = -1;
388 	}
389 	status = NOCONN;			/* retry connect */
390 }
391 
392 /* Should be called with mutex acquired */
393 static void
394 connectlog(void)
395 {
396 	struct sockaddr_un SyslogAddr;	/* AF_UNIX address of local logger */
397 
398 	if (LogFile == -1) {
399 		if ((LogFile = _socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC,
400 		    0)) == -1)
401 			return;
402 	}
403 	if (LogFile != -1 && status == NOCONN) {
404 		SyslogAddr.sun_len = sizeof(SyslogAddr);
405 		SyslogAddr.sun_family = AF_UNIX;
406 
407 		/*
408 		 * First try privileged socket. If no success,
409 		 * then try default socket.
410 		 */
411 		(void)strncpy(SyslogAddr.sun_path, _PATH_LOG_PRIV,
412 		    sizeof SyslogAddr.sun_path);
413 		if (_connect(LogFile, (struct sockaddr *)&SyslogAddr,
414 		    sizeof(SyslogAddr)) != -1)
415 			status = CONNPRIV;
416 
417 		if (status == NOCONN) {
418 			(void)strncpy(SyslogAddr.sun_path, _PATH_LOG,
419 			    sizeof SyslogAddr.sun_path);
420 			if (_connect(LogFile, (struct sockaddr *)&SyslogAddr,
421 			    sizeof(SyslogAddr)) != -1)
422 				status = CONNDEF;
423 		}
424 
425 		if (status == NOCONN) {
426 			/*
427 			 * Try the old "/dev/log" path, for backward
428 			 * compatibility.
429 			 */
430 			(void)strncpy(SyslogAddr.sun_path, _PATH_OLDLOG,
431 			    sizeof SyslogAddr.sun_path);
432 			if (_connect(LogFile, (struct sockaddr *)&SyslogAddr,
433 			    sizeof(SyslogAddr)) != -1)
434 				status = CONNDEF;
435 		}
436 
437 		if (status == NOCONN) {
438 			(void)_close(LogFile);
439 			LogFile = -1;
440 		}
441 	}
442 }
443 
444 static void
445 openlog_unlocked(const char *ident, int logstat, int logfac)
446 {
447 	if (ident != NULL)
448 		LogTag = ident;
449 	LogStat = logstat;
450 	if (logfac != 0 && (logfac &~ LOG_FACMASK) == 0)
451 		LogFacility = logfac;
452 
453 	if (LogStat & LOG_NDELAY)	/* open immediately */
454 		connectlog();
455 
456 	opened = 1;	/* ident and facility has been set */
457 }
458 
459 void
460 openlog(const char *ident, int logstat, int logfac)
461 {
462 
463 	THREAD_LOCK();
464 	pthread_cleanup_push(syslog_cancel_cleanup, NULL);
465 	openlog_unlocked(ident, logstat, logfac);
466 	pthread_cleanup_pop(1);
467 }
468 
469 
470 void
471 closelog(void)
472 {
473 	THREAD_LOCK();
474 	if (LogFile != -1) {
475 		(void)_close(LogFile);
476 		LogFile = -1;
477 	}
478 	LogTag = NULL;
479 	status = NOCONN;
480 	THREAD_UNLOCK();
481 }
482 
483 /* setlogmask -- set the log mask level */
484 int
485 setlogmask(int pmask)
486 {
487 	int omask;
488 
489 	THREAD_LOCK();
490 	omask = LogMask;
491 	if (pmask != 0)
492 		LogMask = pmask;
493 	THREAD_UNLOCK();
494 	return (omask);
495 }
496