/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * This flex program reads /var/log/messages as it grows and saves kernel * warnings to files. It keeps track of warnings it has seen (based on * file/line only, ignoring differences in the stack trace), and reports only * the first warning of each kind, but maintains a count of all warnings by * using their hashes as buckets in a UMA sparse histogram. It also invokes * the crash collector, which collects the warnings and prepares them for later * shipment to the crash server. */ %option noyywrap %{ #include #include #include #include #include #include #include #include #include #include "metrics/c_metrics_library.h" int WarnStart(void); void WarnEnd(void); void WarnInput(char *buf, yy_size_t *result, size_t max_size); #define YY_INPUT(buf, result, max_size) WarnInput(buf, &result, max_size) %} /* Define a few useful regular expressions. */ D [0-9] PREFIX .*" kernel: [ "*{D}+"."{D}+"]" CUT_HERE {PREFIX}" ------------[ cut here".* WARNING {PREFIX}" WARNING: at " END_TRACE {PREFIX}" ---[ end trace".* /* Use exclusive start conditions. */ %x PRE_WARN WARN %% /* The scanner itself. */ ^{CUT_HERE}\n{WARNING} BEGIN(PRE_WARN); .|\n /* ignore all other input in state 0 */ [^ ]+.[^ ]+\n if (WarnStart()) { /* yytext is "file:line func+offset/offset()\n" */ BEGIN(WARN); ECHO; } else { BEGIN(0); } /* Assume the warning ends at the "end trace" line */ ^{END_TRACE}\n ECHO; BEGIN(0); WarnEnd(); ^.*\n ECHO; %% #define HASH_BITMAP_SIZE (1 << 15) /* size in bits */ #define HASH_BITMAP_MASK (HASH_BITMAP_SIZE - 1) const char warn_hist_name[] = "Platform.KernelWarningHashes"; uint32_t hash_bitmap[HASH_BITMAP_SIZE / 32]; CMetricsLibrary metrics_library; const char *prog_name; /* the name of this program */ int yyin_fd; /* instead of FILE *yyin to avoid buffering */ int i_fd; /* for inotify, to detect file changes */ int testing; /* 1 if running test */ int filter; /* 1 when using as filter (for development) */ int fifo; /* 1 when reading from fifo (for devel) */ int draining; /* 1 when draining renamed log file */ const char *msg_path = "/var/log/messages"; const char warn_dump_dir[] = "/var/run/kwarn"; const char *warn_dump_path = "/var/run/kwarn/warning"; const char *crash_reporter_command; __attribute__((__format__(__printf__, 1, 2))) static void Die(const char *format, ...) { va_list ap; va_start(ap, format); fprintf(stderr, "%s: ", prog_name); vfprintf(stderr, format, ap); exit(1); } static void RunCrashReporter(void) { int status = system(crash_reporter_command); if (status != 0) Die("%s exited with status %d\n", crash_reporter_command, status); } static uint32_t StringHash(const char *string) { uint32_t hash = 0; while (*string != '\0') { hash = (hash << 5) + hash + *string++; } return hash; } /* We expect only a handful of different warnings per boot session, so the * probability of a collision is very low, and statistically it won't matter * (unless warnings with the same hash also happens in tandem, which is even * rarer). */ static int HashSeen(uint32_t hash) { int word_index = (hash & HASH_BITMAP_MASK) / 32; int bit_index = (hash & HASH_BITMAP_MASK) % 32; return hash_bitmap[word_index] & 1 << bit_index; } static void SetHashSeen(uint32_t hash) { int word_index = (hash & HASH_BITMAP_MASK) / 32; int bit_index = (hash & HASH_BITMAP_MASK) % 32; hash_bitmap[word_index] |= 1 << bit_index; } #pragma GCC diagnostic ignored "-Wwrite-strings" int WarnStart(void) { uint32_t hash; char *spacep; if (filter) return 1; hash = StringHash(yytext); if (!(testing || fifo || filter)) { CMetricsLibrarySendSparseToUMA(metrics_library, warn_hist_name, (int) hash); } if (HashSeen(hash)) return 0; SetHashSeen(hash); yyout = fopen(warn_dump_path, "w"); if (yyout == NULL) Die("fopen %s failed: %s\n", warn_dump_path, strerror(errno)); spacep = strchr(yytext, ' '); if (spacep == NULL || spacep[1] == '\0') spacep = "unknown-function"; fprintf(yyout, "%08x-%s\n", hash, spacep + 1); return 1; } void WarnEnd(void) { if (filter) return; fclose(yyout); yyout = stdout; /* for debugging */ RunCrashReporter(); } static void WarnOpenInput(const char *path) { yyin_fd = open(path, O_RDONLY); if (yyin_fd < 0) Die("could not open %s: %s\n", path, strerror(errno)); if (!fifo) { /* Go directly to the end of the file. We don't want to parse the same * warnings multiple times on reboot/restart. We might miss some * warnings, but so be it---it's too hard to keep track reliably of the * last parsed position in the syslog. */ if (lseek(yyin_fd, 0, SEEK_END) < 0) Die("could not lseek %s: %s\n", path, strerror(errno)); /* Set up notification of file growth and rename. */ i_fd = inotify_init(); if (i_fd < 0) Die("inotify_init: %s\n", strerror(errno)); if (inotify_add_watch(i_fd, path, IN_MODIFY | IN_MOVE_SELF) < 0) Die("inotify_add_watch: %s\n", strerror(errno)); } } /* We replace the default YY_INPUT() for the following reasons: * * 1. We want to read data as soon as it becomes available, but the default * YY_INPUT() uses buffered I/O. * * 2. We want to block on end of input and wait for the file to grow. * * 3. We want to detect log rotation, and reopen the input file as needed. */ void WarnInput(char *buf, yy_size_t *result, size_t max_size) { while (1) { ssize_t ret = read(yyin_fd, buf, max_size); if (ret < 0) Die("read: %s", strerror(errno)); *result = ret; if (*result > 0 || fifo || filter) return; if (draining) { /* Assume we're done with this log, and move to next * log. Rsyslogd may keep writing to the old log file * for a while, but we don't care since we don't have * to be exact. */ close(yyin_fd); if (YYSTATE == WARN) { /* Be conservative in case we lose the warn * terminator during the switch---or we may * collect personally identifiable information. */ WarnEnd(); } BEGIN(0); /* see above comment */ sleep(1); /* avoid race with log rotator */ WarnOpenInput(msg_path); draining = 0; continue; } /* Nothing left to read, so we must wait. */ struct inotify_event event; while (1) { int n = read(i_fd, &event, sizeof(event)); if (n <= 0) { if (errno == EINTR) continue; else Die("inotify: %s\n", strerror(errno)); } else break; } if (event.mask & IN_MOVE_SELF) { /* The file has been renamed. Before switching * to the new one, we process any remaining * content of this file. */ draining = 1; } } } int main(int argc, char **argv) { int result; struct passwd *user; prog_name = argv[0]; if (argc == 2 && strcmp(argv[1], "--test") == 0) testing = 1; else if (argc == 2 && strcmp(argv[1], "--filter") == 0) filter = 1; else if (argc == 2 && strcmp(argv[1], "--fifo") == 0) { fifo = 1; } else if (argc != 1) { fprintf(stderr, "usage: %s [single-flag]\n" "flags (for testing only):\n" "--fifo\tinput is fifo \"fifo\", output is stdout\n" "--filter\tinput is stdin, output is stdout\n" "--test\trun self-test\n", prog_name); exit(1); } metrics_library = CMetricsLibraryNew(); CMetricsLibraryInit(metrics_library); crash_reporter_command = testing ? "./warn_collector_test_reporter.sh" : "/sbin/crash_reporter --kernel_warning"; /* When filtering with --filter (for development) use stdin for input. * Otherwise read input from a file or a fifo. */ yyin_fd = fileno(stdin); if (testing) { msg_path = "messages"; warn_dump_path = "warning"; } if (fifo) { msg_path = "fifo"; } if (!filter) { WarnOpenInput(msg_path); } /* Create directory for dump file. Still need to be root here. */ unlink(warn_dump_path); if (!testing && !fifo && !filter) { rmdir(warn_dump_dir); result = mkdir(warn_dump_dir, 0755); if (result < 0) Die("could not create %s: %s\n", warn_dump_dir, strerror(errno)); } if (0) { /* TODO(semenzato): put this back in once we decide it's safe * to make /var/spool/crash rwxrwxrwx root, or use a different * owner and setuid for the crash reporter as well. */ /* Get low privilege uid, gid. */ user = getpwnam("chronos"); if (user == NULL) Die("getpwnam failed\n"); /* Change dump directory ownership. */ if (chown(warn_dump_dir, user->pw_uid, user->pw_gid) < 0) Die("chown: %s\n", strerror(errno)); /* Drop privileges. */ if (setuid(user->pw_uid) < 0) { Die("setuid: %s\n", strerror(errno)); } } /* Go! */ return yylex(); } /* Flex should really know not to generate these functions. */ void UnusedFunctionWarningSuppressor(void) { yyunput(0, 0); }