1 /*
2  * Copyright (C) 2013 The Android Open Source Project
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  * This flex program reads /var/log/messages as it grows and saves kernel
17  * warnings to files.  It keeps track of warnings it has seen (based on
18  * file/line only, ignoring differences in the stack trace), and reports only
19  * the first warning of each kind, but maintains a count of all warnings by
20  * using their hashes as buckets in a UMA sparse histogram.  It also invokes
21  * the crash collector, which collects the warnings and prepares them for later
22  * shipment to the crash server.
23  */
24 
25 %option noyywrap
26 
27 %{
28 #include <fcntl.h>
29 #include <inttypes.h>
30 #include <pwd.h>
31 #include <stdarg.h>
32 #include <sys/inotify.h>
33 #include <sys/select.h>
34 #include <sys/stat.h>
35 #include <sys/types.h>
36 #include <unistd.h>
37 
38 #include "metrics/c_metrics_library.h"
39 
40 int WarnStart(void);
41 void WarnEnd(void);
42 void WarnInput(char *buf, yy_size_t *result, size_t max_size);
43 
44 #define YY_INPUT(buf, result, max_size) WarnInput(buf, &result, max_size)
45 
46 %}
47 
48 /* Define a few useful regular expressions. */
49 
50 D               [0-9]
51 PREFIX          .*" kernel: [ "*{D}+"."{D}+"]"
52 CUT_HERE        {PREFIX}" ------------[ cut here".*
53 WARNING         {PREFIX}" WARNING: at "
54 END_TRACE       {PREFIX}" ---[ end trace".*
55 
56 /* Use exclusive start conditions. */
57 %x PRE_WARN WARN
58 
59 %%
60  /* The scanner itself. */
61 
62 ^{CUT_HERE}\n{WARNING}          BEGIN(PRE_WARN);
63 .|\n                            /* ignore all other input in state 0 */
64 <PRE_WARN>[^ ]+.[^ ]+\n         if (WarnStart()) {
65                                   /* yytext is
66                                      "file:line func+offset/offset()\n" */
67                                   BEGIN(WARN); ECHO;
68                                 } else {
69                                   BEGIN(0);
70                                 }
71 
72  /* Assume the warning ends at the "end trace" line */
73 <WARN>^{END_TRACE}\n            ECHO; BEGIN(0); WarnEnd();
74 <WARN>^.*\n                     ECHO;
75 
76 %%
77 
78 #define HASH_BITMAP_SIZE        (1 << 15)  /* size in bits */
79 #define HASH_BITMAP_MASK        (HASH_BITMAP_SIZE - 1)
80 
81 const char warn_hist_name[] = "Platform.KernelWarningHashes";
82 uint32_t hash_bitmap[HASH_BITMAP_SIZE / 32];
83 CMetricsLibrary metrics_library;
84 
85 const char *prog_name;          /* the name of this program */
86 int yyin_fd;                    /* instead of FILE *yyin to avoid buffering */
87 int i_fd;                       /* for inotify, to detect file changes */
88 int testing;                    /* 1 if running test */
89 int filter;                     /* 1 when using as filter (for development) */
90 int fifo;                       /* 1 when reading from fifo (for devel) */
91 int draining;                   /* 1 when draining renamed log file */
92 
93 const char *msg_path = "/var/log/messages";
94 const char warn_dump_dir[]  = "/var/run/kwarn";
95 const char *warn_dump_path = "/var/run/kwarn/warning";
96 const char *crash_reporter_command;
97 
98 __attribute__((__format__(__printf__, 1, 2)))
Die(const char * format,...)99 static void Die(const char *format, ...) {
100   va_list ap;
101   va_start(ap, format);
102   fprintf(stderr, "%s: ", prog_name);
103   vfprintf(stderr, format, ap);
104   exit(1);
105 }
106 
RunCrashReporter(void)107 static void RunCrashReporter(void) {
108   int status = system(crash_reporter_command);
109   if (status != 0)
110     Die("%s exited with status %d\n", crash_reporter_command, status);
111 }
112 
StringHash(const char * string)113 static uint32_t StringHash(const char *string) {
114   uint32_t hash = 0;
115   while (*string != '\0') {
116     hash = (hash << 5) + hash + *string++;
117   }
118   return hash;
119 }
120 
121 /* We expect only a handful of different warnings per boot session, so the
122  * probability of a collision is very low, and statistically it won't matter
123  * (unless warnings with the same hash also happens in tandem, which is even
124  * rarer).
125  */
HashSeen(uint32_t hash)126 static int HashSeen(uint32_t hash) {
127   int word_index = (hash & HASH_BITMAP_MASK) / 32;
128   int bit_index = (hash & HASH_BITMAP_MASK) % 32;
129   return hash_bitmap[word_index] & 1 << bit_index;
130 }
131 
SetHashSeen(uint32_t hash)132 static void SetHashSeen(uint32_t hash) {
133   int word_index = (hash & HASH_BITMAP_MASK) / 32;
134   int bit_index = (hash & HASH_BITMAP_MASK) % 32;
135   hash_bitmap[word_index] |= 1 << bit_index;
136 }
137 
138 #pragma GCC diagnostic ignored "-Wwrite-strings"
WarnStart(void)139 int WarnStart(void) {
140   uint32_t hash;
141   char *spacep;
142 
143   if (filter)
144     return 1;
145 
146   hash = StringHash(yytext);
147   if (!(testing || fifo || filter)) {
148     CMetricsLibrarySendSparseToUMA(metrics_library, warn_hist_name, (int) hash);
149   }
150   if (HashSeen(hash))
151     return 0;
152   SetHashSeen(hash);
153 
154   yyout = fopen(warn_dump_path, "w");
155   if (yyout == NULL)
156     Die("fopen %s failed: %s\n", warn_dump_path, strerror(errno));
157   spacep = strchr(yytext, ' ');
158   if (spacep == NULL || spacep[1] == '\0')
159     spacep = "unknown-function";
160   fprintf(yyout, "%08x-%s\n", hash, spacep + 1);
161   return 1;
162 }
163 
WarnEnd(void)164 void WarnEnd(void) {
165   if (filter)
166     return;
167   fclose(yyout);
168   yyout = stdout;               /* for debugging */
169   RunCrashReporter();
170 }
171 
WarnOpenInput(const char * path)172 static void WarnOpenInput(const char *path) {
173   yyin_fd = open(path, O_RDONLY);
174   if (yyin_fd < 0)
175     Die("could not open %s: %s\n", path, strerror(errno));
176   if (!fifo) {
177     /* Go directly to the end of the file.  We don't want to parse the same
178      * warnings multiple times on reboot/restart.  We might miss some
179      * warnings, but so be it---it's too hard to keep track reliably of the
180      * last parsed position in the syslog.
181      */
182     if (lseek(yyin_fd, 0, SEEK_END) < 0)
183       Die("could not lseek %s: %s\n", path, strerror(errno));
184     /* Set up notification of file growth and rename. */
185     i_fd = inotify_init();
186     if (i_fd < 0)
187       Die("inotify_init: %s\n", strerror(errno));
188     if (inotify_add_watch(i_fd, path, IN_MODIFY | IN_MOVE_SELF) < 0)
189       Die("inotify_add_watch: %s\n", strerror(errno));
190   }
191 }
192 
193 /* We replace the default YY_INPUT() for the following reasons:
194  *
195  * 1.  We want to read data as soon as it becomes available, but the default
196  * YY_INPUT() uses buffered I/O.
197  *
198  * 2.  We want to block on end of input and wait for the file to grow.
199  *
200  * 3.  We want to detect log rotation, and reopen the input file as needed.
201  */
WarnInput(char * buf,yy_size_t * result,size_t max_size)202 void WarnInput(char *buf, yy_size_t *result, size_t max_size) {
203   while (1) {
204     ssize_t ret = read(yyin_fd, buf, max_size);
205     if (ret < 0)
206       Die("read: %s", strerror(errno));
207     *result = ret;
208     if (*result > 0 || fifo || filter)
209       return;
210     if (draining) {
211       /* Assume we're done with this log, and move to next
212        * log.  Rsyslogd may keep writing to the old log file
213        * for a while, but we don't care since we don't have
214        * to be exact.
215        */
216       close(yyin_fd);
217       if (YYSTATE == WARN) {
218         /* Be conservative in case we lose the warn
219          * terminator during the switch---or we may
220          * collect personally identifiable information.
221          */
222         WarnEnd();
223       }
224       BEGIN(0);        /* see above comment */
225       sleep(1);        /* avoid race with log rotator */
226       WarnOpenInput(msg_path);
227       draining = 0;
228       continue;
229     }
230     /* Nothing left to read, so we must wait. */
231     struct inotify_event event;
232     while (1) {
233       int n = read(i_fd, &event, sizeof(event));
234       if (n <= 0) {
235         if (errno == EINTR)
236           continue;
237         else
238           Die("inotify: %s\n", strerror(errno));
239       } else
240         break;
241     }
242     if (event.mask & IN_MOVE_SELF) {
243       /* The file has been renamed.  Before switching
244        * to the new one, we process any remaining
245        * content of this file.
246        */
247       draining = 1;
248     }
249   }
250 }
251 
main(int argc,char ** argv)252 int main(int argc, char **argv) {
253   int result;
254   struct passwd *user;
255   prog_name = argv[0];
256 
257   if (argc == 2 && strcmp(argv[1], "--test") == 0)
258     testing = 1;
259   else if (argc == 2 && strcmp(argv[1], "--filter") == 0)
260     filter = 1;
261   else if (argc == 2 && strcmp(argv[1], "--fifo") == 0) {
262     fifo = 1;
263   } else if (argc != 1) {
264     fprintf(stderr,
265             "usage: %s [single-flag]\n"
266             "flags (for testing only):\n"
267             "--fifo\tinput is fifo \"fifo\", output is stdout\n"
268             "--filter\tinput is stdin, output is stdout\n"
269             "--test\trun self-test\n",
270             prog_name);
271     exit(1);
272   }
273 
274   metrics_library = CMetricsLibraryNew();
275   CMetricsLibraryInit(metrics_library);
276 
277   crash_reporter_command = testing ?
278     "./warn_collector_test_reporter.sh" :
279     "/sbin/crash_reporter --kernel_warning";
280 
281   /* When filtering with --filter (for development) use stdin for input.
282    * Otherwise read input from a file or a fifo.
283    */
284   yyin_fd = fileno(stdin);
285   if (testing) {
286     msg_path = "messages";
287     warn_dump_path = "warning";
288   }
289   if (fifo) {
290     msg_path = "fifo";
291   }
292   if (!filter) {
293     WarnOpenInput(msg_path);
294   }
295 
296   /* Create directory for dump file.  Still need to be root here. */
297   unlink(warn_dump_path);
298   if (!testing && !fifo && !filter) {
299     rmdir(warn_dump_dir);
300     result = mkdir(warn_dump_dir, 0755);
301     if (result < 0)
302       Die("could not create %s: %s\n",
303           warn_dump_dir, strerror(errno));
304   }
305 
306   if (0) {
307     /* TODO(semenzato): put this back in once we decide it's safe
308      * to make /var/spool/crash rwxrwxrwx root, or use a different
309      * owner and setuid for the crash reporter as well.
310      */
311 
312     /* Get low privilege uid, gid. */
313     user = getpwnam("chronos");
314     if (user == NULL)
315       Die("getpwnam failed\n");
316 
317     /* Change dump directory ownership. */
318     if (chown(warn_dump_dir, user->pw_uid, user->pw_gid) < 0)
319       Die("chown: %s\n", strerror(errno));
320 
321     /* Drop privileges. */
322     if (setuid(user->pw_uid) < 0) {
323       Die("setuid: %s\n", strerror(errno));
324     }
325   }
326 
327   /* Go! */
328   return yylex();
329 }
330 
331 /* Flex should really know not to generate these functions.
332  */
UnusedFunctionWarningSuppressor(void)333 void UnusedFunctionWarningSuppressor(void) {
334   yyunput(0, 0);
335 }
336