/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License (the "License"). * You may not use this file except in compliance with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */ /* All Rights Reserved */ /* * Copyright 2008 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ #pragma ident "%Z%%M% %I% %E% SMI" /* * wtmpfix - adjust wtmpx file and remove date changes. * wtmpfix wtmpx2 * * Can recover to some extent from wtmpx corruption. */ #include #include #include #include #include "acctdef.h" #include #include #include #include #include #include #include #define DAYEPOCH (60 * 60 * 24) #define UTRSZ (sizeof (struct futmpx)) /* file record size */ /* * The acctsh(1M) shell scripts startup(1M) and shutacct(1M) as well as the * runacct script each pass their own specific reason strings in the first * argument to acctwtmp(1M), to be propagated into ut_line fields. Additional * reasons (RUNLVL_MSG, ..., DOWN_MSG), used by compiled code, are defined in * as preprocessor constants. * For simplicity we predefine similar constants for the scripted strings * here, as no other compiled code uses those. * Moreover, we need a variant of RUNLVL_MSG without the "%c" at the end. * We shall use the fact that ut_line[RLVLMSG_LEN] will extract the char * in the %c position ('S', '2', ...). * Since all of these string constants are '\0' terminated, they can safely * be used with strcmp() even when ut_line is not. */ #define RUN_LEVEL_MSG "run-level " #define ACCTG_ON_MSG "acctg on" #define ACCTG_OFF_MSG "acctg off" #define RUNACCT_MSG "runacct" #define RLVLMSG_LEN (sizeof (RUN_LEVEL_MSG) - 1) /* * Records encountered are classified as one of the following: corrupted; * ok but devoid of interest to acctcon downstream; ok and interesting; * or ok and even redundant enough to latch onto a new alignment whilst * recovering from a corruption. * The ordering among these four symbolic values is significant. */ typedef enum { INRANGE_ERR = -1, INRANGE_DROP, INRANGE_PASS, INRANGE_ALIGNED } inrange_t; /* input filenames and record numbers, for diagnostics only */ #define STDIN_NAME "" static char *cur_input_name; static off_t recin; static FILE *Wtmpx, *Temp; struct dtab { off_t d_off1; /* file offset start */ off_t d_off2; /* file offset stop */ time_t d_adj; /* time adjustment */ struct dtab *d_ndp; /* next record */ }; static struct dtab *Fdp; /* list header */ static struct dtab *Ldp; /* list trailer */ static time_t lastmonth, nextmonth; static struct futmpx Ut, Ut2; static int winp(FILE *, struct futmpx *); static void mkdtab(off_t); static void setdtab(off_t, struct futmpx *, struct futmpx *); static void adjust(off_t, struct futmpx *); static int invalid(char *); static void scanfile(void); static inrange_t inrange(void); static void wcomplain(char *); int main(int argc, char **argv) { time_t tloc; struct tm *tmp; int year; int month; off_t rectmpin; (void) setlocale(LC_ALL, ""); setbuf(stdout, NULL); (void) time(&tloc); tmp = localtime(&tloc); year = tmp->tm_year; month = tmp->tm_mon + 1; lastmonth = ((year + 1900 - 1970) * 365 + (month - 1) * 30) * DAYEPOCH; nextmonth = ((year + 1900 - 1970) * 365 + (month + 1) * 30) * DAYEPOCH; if (argc < 2) { argv[argc] = "-"; argc++; } /* * Almost all system call failures in this program are unrecoverable * and therefore fatal. Typical causes might be lack of memory or * of space in a filesystem. If necessary, the system administrator * can invoke /usr/lib/acct/runacct interactively after making room * to complete the remaining phases of last night's accounting. */ if ((Temp = tmpfile()) == NULL) { perror("Cannot create temporary file"); return (EXIT_FAILURE); } while (--argc > 0) { argv++; if (strcmp(*argv, "-") == 0) { Wtmpx = stdin; cur_input_name = STDIN_NAME; } else if ((Wtmpx = fopen(*argv, "r")) == NULL) { (void) fprintf(stderr, "Cannot open %s: %s\n", *argv, strerror(errno)); return (EXIT_FAILURE); } else { cur_input_name = *argv; } /* * Filter records reading from current input stream Wtmpx, * writing to Temp. */ scanfile(); if (Wtmpx != stdin) (void) fclose(Wtmpx); } /* flush and rewind Temp for readback */ if (fflush(Temp) != 0) { perror(": fflush"); return (EXIT_FAILURE); } if (fseeko(Temp, (off_t)0L, SEEK_SET) != 0) { perror(": seek"); return (EXIT_FAILURE); } /* second pass: apply time adjustments */ rectmpin = 0; while (winp(Temp, &Ut)) { adjust(rectmpin, &Ut); rectmpin += UTRSZ; if (fwrite(&Ut, UTRSZ, 1, stdout) < 1) { perror(": fwrite"); return (EXIT_FAILURE); } } (void) fclose(Temp); /* * Detect if we've run out of space (say) and exit unsuccessfully * so that downstream accounting utilities won't start processing an * incomplete tmpwtmp file. */ if (fflush(stdout) != 0) { perror(": fflush"); return (EXIT_FAILURE); } return (EXIT_SUCCESS); } static int winp(FILE *f, struct futmpx *w) { if (fread(w, (size_t)UTRSZ, (size_t)1, f) != 1) return (0); if ((w->ut_type >= EMPTY) && (w->ut_type <= UTMAXTYPE)) return (1); else { (void) fprintf(stderr, "Bad temp file at offset %lld\n", (longlong_t)(ftell(f) - UTRSZ)); /* * If input was corrupt, neither ut_line nor ut_user can be * relied on to be \0-terminated. Even fixing the precision * does not entirely guard against this. */ (void) fprintf(stderr, "ut_line \"%-12.12s\" ut_user \"%-8.8s\" ut_xtime %ld\n", w->ut_line, w->ut_user, (long)w->ut_xtime); exit(EXIT_FAILURE); } /* NOTREACHED */ } static void mkdtab(off_t p) { struct dtab *dp; dp = Ldp; if (dp == NULL) { dp = calloc(sizeof (struct dtab), 1); if (dp == NULL) { (void) fprintf(stderr, "out of memory\n"); exit(EXIT_FAILURE); } Fdp = Ldp = dp; } dp->d_off1 = p; } static void setdtab(off_t p, struct futmpx *w1, struct futmpx *w2) { struct dtab *dp; if ((dp = Ldp) == NULL) { (void) fprintf(stderr, "no dtab\n"); exit(EXIT_FAILURE); } dp->d_off2 = p; dp->d_adj = w2->ut_xtime - w1->ut_xtime; if ((Ldp = calloc(sizeof (struct dtab), 1)) == NULL) { (void) fprintf(stderr, "out of memory\n"); exit(EXIT_FAILURE); } Ldp->d_off1 = dp->d_off1; dp->d_ndp = Ldp; } static void adjust(off_t p, struct futmpx *w) { off_t pp; struct dtab *dp; pp = p; for (dp = Fdp; dp != NULL; dp = dp->d_ndp) { if (dp->d_adj == 0) continue; if (pp >= dp->d_off1 && pp <= dp->d_off2) w->ut_xtime += dp->d_adj; } } /* * invalid() determines whether the name field adheres to the criteria * set forth in acctcon1. If returns VALID if the name is ok, or * INVALID if the name violates conventions. */ static int invalid(char *name) { int i; for (i = 0; i < NSZ; i++) { if (name[i] == '\0') return (VALID); if (! (isalnum(name[i]) || (name[i] == '$') || (name[i] == ' ') || (name[i] == '.') || (name[i] == '_') || (name[i] == '-'))) { return (INVALID); } } return (VALID); } /* * scanfile: * 1) reads the current input file * 2) filters for process records in time range of interest and for * other types of records deemed interesting to acctcon downstream * 3) picks up time changes with setdtab() if in multiuser mode, which * will be applied when the temp file is read back * 4) changes bad login names to INVALID * 5) recovers from common cases of wtmpx corruption (loss of record * alignment). * All of the static globals are used directly or indirectly. * * When wtmpfix is asked to process several input files in succession, * some state needs to be preserved from one scanfile() invocation to the * next. Aside from the temp file position, we remember whether we were * in multi-user mode or not. Absent evidence to the contrary, we begin * processing assuming multi-user mode, because runacct's wtmpx rotation * normally gives us a file recently initialized by utmp2wtmp(1M) with no * older RUN_LVL records surviving. */ static void scanfile() { struct stat Wtstat; off_t residue = 0; /* input file size mod UTRSZ */ /* * lastok will be the offset of the beginning of the most recent * manifestly plausible and interesting input record in the current * input file, if any. * An invariant at loop entry is -UTRSZ <= lastok <= recin - UTRSZ. */ off_t lastok = -(off_t)UTRSZ; static off_t rectmp; /* current temp file position */ static boolean_t multimode = B_TRUE; /* multi-user RUN_LVL in force */ inrange_t is_ok; /* caches inrange() result */ /* * During normal operation, records are of interest and copied to * the output when is_ok >= INRANGE_PASS, ignored and dropped when * is_ok == INRANGE_DROP, and evidence of corruption otherwise. * While we are trying to recover from a corruption and hunting for * records with sufficient redundancy to confirm that we have reached * proper alignment again, we'll want is_ok >= INRANGE_ALIGNED. * The value of want_ok is the minimum inrange() result of current * interest. It is raised to INRANGE_ALIGNED during ongoing recovery * and dropped back to INRANGE_PASS when we have recovered alignment. */ inrange_t want_ok = INRANGE_PASS; boolean_t recovered = B_FALSE; /* true after a successful recovery */ int n; if (fstat(fileno(Wtmpx), &Wtstat) == -1) { (void) fprintf(stderr, "Cannot stat %s (will read sequentially): %s\n", cur_input_name, strerror(errno)); } else if ((Wtstat.st_mode & S_IFMT) == S_IFREG) { residue = Wtstat.st_size % UTRSZ; } /* if residue != 0, part of the file may be misaligned */ for (recin = 0; ((n = fread(&Ut, (size_t)UTRSZ, (size_t)1, Wtmpx)) > 0) || (residue > 0); recin += UTRSZ) { if (n == 0) { /* * Implying residue > 0 and want_ok == INRANGE_PASS. * It isn't worth telling an I/O error from EOF here. * But one case is worth catching to avoid issuing a * confusing message below. When the previous record * had been ok, we just drop the current truncated * record and bail out of the loop -- no seeking back. */ if (lastok == recin - UTRSZ) { wcomplain("file ends in mid-record, " "final partial record dropped"); break; } else { wcomplain("file ends in mid-record"); /* handled below like a corrupted record */ is_ok = INRANGE_ERR; } } else is_ok = inrange(); /* alignment recovery logic */ if ((residue > 0) && (is_ok == INRANGE_ERR)) { /* * "Let's go back to the last place where we knew * where we were..." * In fact, if the last record had been fine and we * know there's at least one whole record ahead, we * might move forward here (by residue bytes, less * than one record's worth). In any case, we align * ourselves to an integral number of records before * the end of the file. */ wcomplain("suspecting misaligned records, " "repositioning"); recin = lastok + UTRSZ + residue; residue = 0; if (fseeko(Wtmpx, recin, SEEK_SET) != 0) { (void) fprintf(stderr, "%s: seek: %s\n", cur_input_name, strerror(errno)); exit(EXIT_FAILURE); } wcomplain("starting re-scan"); /* * While want_ok is elevated, only unequivocal records * with inrange() == INRANGE_ALIGNED will be admitted * to latch onto the tentative new alignment. */ want_ok = INRANGE_ALIGNED; /* * Compensate for the loop continuation. Doing * it this way gets the correct offset reported * in the re-scan message above. */ recin -= UTRSZ; continue; } /* assert: residue == 0 or is_ok >= INRANGE_DROP here */ if (is_ok < want_ok) /* record of no further interest */ continue; if (want_ok == INRANGE_ALIGNED) { wcomplain("now recognizing aligned records again"); want_ok = INRANGE_PASS; recovered = B_TRUE; } /* * lastok must track recin whenever the current record is * being processed and written out to our temp file, to avoid * reprocessing any bits already done when we readjust our * alignment. */ lastok = recin; /* now we have a good wtmpx record, do more processing */ if (rectmp == 0 || Ut.ut_type == BOOT_TIME) mkdtab(rectmp); if (Ut.ut_type == RUN_LVL) { /* inrange() already checked the "run-level " part */ if (Ut.ut_line[RLVLMSG_LEN] == 'S') multimode = B_FALSE; else if ((Ut.ut_line[RLVLMSG_LEN] == '2') || (Ut.ut_line[RLVLMSG_LEN] == '3') || (Ut.ut_line[RLVLMSG_LEN] == '4')) multimode = B_TRUE; } if (invalid(Ut.ut_name) == INVALID) { (void) fprintf(stderr, "wtmpfix: logname \"%*.*s\" changed " "to \"INVALID\"\n", OUTPUT_NSZ, OUTPUT_NSZ, Ut.ut_name); (void) strncpy(Ut.ut_name, "INVALID", NSZ); } /* * Special case: OLD_TIME should be immediately followed by * NEW_TIME. * We make no attempt at alignment recovery between these * two: if there's junk at this point in the input, then * a NEW_TIME seen after the junk probably won't be the one * we are looking for. */ if (Ut.ut_type == OLD_TIME) { /* * Make recin refer to the expected NEW_TIME. * Loop continuation will increment it again * for the record we're about to read now. */ recin += UTRSZ; if (!fread(&Ut2, (size_t)UTRSZ, (size_t)1, Wtmpx)) { wcomplain("input truncated after OLD_TIME - " "giving up"); exit(EXIT_FAILURE); } /* * Rudimentary NEW_TIME sanity check. Not as thorough * as in inrange(), but then we have redundancy from * context here, since we're just after a plausible * OLD_TIME record. */ if ((Ut2.ut_type != NEW_TIME) || (strcmp(Ut2.ut_line, NTIME_MSG) != 0)) { wcomplain("NEW_TIME expected but missing " "after OLD_TIME - giving up"); exit(EXIT_FAILURE); } lastok = recin; if (multimode == B_TRUE) setdtab(rectmp, &Ut, &Ut2); rectmp += 2 * UTRSZ; if ((fwrite(&Ut, UTRSZ, 1, Temp) < 1) || (fwrite(&Ut2, UTRSZ, 1, Temp) < 1)) { perror(": fwrite"); exit(EXIT_FAILURE); } continue; } if (fwrite(&Ut, UTRSZ, 1, Temp) < 1) { perror(": fwrite"); exit(EXIT_FAILURE); } rectmp += UTRSZ; } if (want_ok == INRANGE_ALIGNED) { wcomplain("EOF reached without recognizing another aligned " "record with certainty. This file may need to be " "repaired by hand.\n"); } else if (recovered == B_TRUE) { /* * There may have been a number of wcomplain() messages * since we reported about the re-scan, so it bears repeating * at the end that not all was well. */ wcomplain("EOF reached after recovering from corruption " "in the middle of the file. This file may need to be " "repaired by hand.\n"); } } /* * inrange: inspect what we hope to be one wtmpx record. * Globals: Ut, lastmonth, nextmonth; recin, cur_input_name (diagnostics) * Return values: * INRANGE_ERR -- an inconsistency was detected, input file corrupted * INRANGE_DROP -- Ut appears consistent but isn't of interest * (of process type and outside the time range we want) * INRANGE_PASS -- Ut appears consistent and this record is of interest * INRANGE_ALIGNED -- same, and it is also redundant enough to be sure * that we're correctly aligned on record boundaries */ #define UNEXPECTED_UT_PID \ (Ut.ut_pid != 0) || \ (Ut.ut_exit.e_termination != 0) || \ (Ut.ut_exit.e_exit != 0) static inrange_t inrange() { /* pid_t is signed so that fork() can return -1. Exploit this. */ if (Ut.ut_pid < 0) { wcomplain("negative pid"); return (INRANGE_ERR); } /* the legal values for ut_type are enumerated in */ switch (Ut.ut_type) { case EMPTY: if (UNEXPECTED_UT_PID) { wcomplain("nonzero pid or status in EMPTY record"); return (INRANGE_ERR); } /* * We'd like to have Ut.ut_user[0] == '\0' here, but sadly * this isn't always so, so we can't rely on it. */ return (INRANGE_DROP); case RUN_LVL: /* ut_line must have come from the RUNLVL_MSG pattern */ if (strncmp(Ut.ut_line, RUN_LEVEL_MSG, RLVLMSG_LEN) != 0) { wcomplain("RUN_LVL record doesn't say `" RUN_LEVEL_MSG "'"); return (INRANGE_ERR); } /* * The ut_pid, termination, and exit status fields have * special meaning in this case, and none of them is * suitable for checking. And we won't insist on ut_user * to always be an empty string. */ return (INRANGE_ALIGNED); case BOOT_TIME: if (UNEXPECTED_UT_PID) { wcomplain("nonzero pid or status in BOOT_TIME record"); return (INRANGE_ERR); } if (strcmp(Ut.ut_line, BOOT_MSG) != 0) { wcomplain("BOOT_TIME record doesn't say `" BOOT_MSG "'"); return (INRANGE_ERR); } return (INRANGE_ALIGNED); case OLD_TIME: if (UNEXPECTED_UT_PID) { wcomplain("nonzero pid or status in OLD_TIME record"); return (INRANGE_ERR); } if (strcmp(Ut.ut_line, OTIME_MSG) != 0) { wcomplain("OLD_TIME record doesn't say `" OTIME_MSG "'"); return (INRANGE_ERR); } return (INRANGE_ALIGNED); case NEW_TIME: /* * We don't actually expect to see any here. If they follow * an OLD_TIME record as they should, they'll be handled on * the fly in scanfile(). But we might still run into one * if the input is somehow corrupted. */ if (UNEXPECTED_UT_PID) { wcomplain("nonzero pid or status in NEW_TIME record"); return (INRANGE_ERR); } if (strcmp(Ut.ut_line, NTIME_MSG) != 0) { wcomplain("NEW_TIME record doesn't say `" NTIME_MSG "'"); return (INRANGE_ERR); } return (INRANGE_ALIGNED); /* the four *_PROCESS ut_types have a lot in common */ case USER_PROCESS: /* * Catch two special cases first: psradm records have no id * and no pid, while root login over FTP may not have a * valid ut_user and may have garbage in ut_id[3]. */ if ((strcmp(Ut.ut_user, "psradm") == 0) && (Ut.ut_id[0] == '\0') && (Ut.ut_pid > 0)) { if ((Ut.ut_xtime > lastmonth) && (Ut.ut_xtime < nextmonth)) { return (INRANGE_ALIGNED); } else { return (INRANGE_DROP); } } if ((Ut.ut_user[0] == '\0') && (strncmp(Ut.ut_id, "ftp", 3) == 0) && (strncmp(Ut.ut_line, "ftp", 3) == 0)) { if ((Ut.ut_xtime > lastmonth) && (Ut.ut_xtime < nextmonth)) { return (INRANGE_ALIGNED); } else { return (INRANGE_DROP); } } /* FALLTHROUGH */ case LOGIN_PROCESS: if (Ut.ut_user[0] == '\0') { wcomplain("missing username in process record"); return (INRANGE_ERR); } /* FALLTHROUGH */ case INIT_PROCESS: /* * INIT_PROCESS and DEAD_PROCESS records can come with an * empty ut_user in degenerate cases (e.g. syntax errors * like a comment-only process field in /etc/inittab). * But in an INIT_PROCESS, LOGIN_PROCESS, or USER_PROCESS * record, we expect a respectable ut_pid. */ if (Ut.ut_pid == 0) { wcomplain("null pid in process record"); return (INRANGE_ERR); } /* FALLTHROUGH */ case DEAD_PROCESS: /* * DEAD_PROCESS records with a null ut_pid can be produced * by gnome-terminal (normally seen in utmpx only, but they * can leak into wtmpx in rare circumstances). * Unfortunately, ut_id can't be relied on to contain * anything in particular. (E.g., sshd might leave it * 0-initialized.) This leaves almost no verifiable * redundancy here beyond the ut_type. * At least we insist on a reasonable timestamp. */ if (Ut.ut_xtime <= 0) { wcomplain("non-positive time in process record"); return (INRANGE_ERR); } if ((Ut.ut_xtime > lastmonth) && (Ut.ut_xtime < nextmonth)) { return (INRANGE_PASS); } else { return (INRANGE_DROP); } case ACCOUNTING: /* * If we recognize one of the three reason strings passed * by the /usr/lib/acct shell scripts to acctwtmp, we * exploit the available redundancy they offer. But * acctwtmp could have been invoked by custom scripts or * interactively with other reason strings in the first * argument, so anything we don't recognize does not * constitute evidence for corruption. */ if ((strcmp(Ut.ut_line, RUNACCT_MSG) != 0) && (strcmp(Ut.ut_line, ACCTG_ON_MSG) != 0) && (strcmp(Ut.ut_line, ACCTG_OFF_MSG) != 0)) { return (INRANGE_DROP); } return (INRANGE_ALIGNED); case DOWN_TIME: if (UNEXPECTED_UT_PID) { wcomplain("nonzero pid or status in DOWN_TIME record"); return (INRANGE_ERR); } if (strcmp(Ut.ut_line, DOWN_MSG) != 0) { wcomplain("DOWN_TIME record doesn't say `" DOWN_MSG "'"); return (INRANGE_ERR); } return (INRANGE_ALIGNED); default: wcomplain("ut_type out of range"); return (INRANGE_ERR); } /* NOTREACHED */ } static void wcomplain(char *msg) { (void) fprintf(stderr, "%s: offset %lld: %s\n", cur_input_name, (longlong_t)recin, msg); }