/*
* 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
* 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 <wtmpx1 >wtmpx2
*
* Can recover to some extent from wtmpx corruption.
*/
#include <stdio.h>
#include "acctdef.h"
#include <utmpx.h>
#include <time.h>
#include <ctype.h>
#include <locale.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
/*
* 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
* <utmp.h> 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.
*/
/*
* 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_t;
/* input filenames and record numbers, for diagnostics only */
static char *cur_input_name;
struct dtab
{
};
static int invalid(char *);
static void scanfile(void);
static void wcomplain(char *);
int
{
int year;
int month;
if (argc < 2) {
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
* to complete the remaining phases of last night's accounting.
*/
perror("Cannot create temporary file");
return (EXIT_FAILURE);
}
while (--argc > 0) {
argv++;
return (EXIT_FAILURE);
} else {
cur_input_name = *argv;
}
/*
* Filter records reading from current input stream Wtmpx,
* writing to Temp.
*/
scanfile();
}
/* flush and rewind Temp for readback */
perror("<temporary file>: fflush");
return (EXIT_FAILURE);
}
perror("<temporary file>: seek");
return (EXIT_FAILURE);
}
/* second pass: apply time adjustments */
rectmpin = 0;
perror("<stdout>: fwrite");
return (EXIT_FAILURE);
}
}
/*
* 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.
*/
perror("<stdout>: fflush");
return (EXIT_FAILURE);
}
return (EXIT_SUCCESS);
}
static int
{
return (0);
return (1);
else {
/*
* 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.
*/
"ut_line \"%-12.12s\" ut_user \"%-8.8s\" ut_xtime %ld\n",
}
/* NOTREACHED */
}
static void
{
}
}
}
static void
{
}
}
}
static void
{
pp = p;
continue;
}
}
/*
* 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
{
int i;
for (i = 0; i < NSZ; i++) {
if (name[i] == '\0')
return (VALID);
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()
{
/*
* 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.
*/
/*
* 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.
*/
int n;
"Cannot stat %s (will read sequentially): %s\n",
}
/* if residue != 0, part of the file may be misaligned */
for (recin = 0;
(residue > 0);
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.
*/
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
/* alignment recovery logic */
/*
* "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");
residue = 0;
}
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.
*/
/*
* Compensate for the loop continuation. Doing
* it this way gets the correct offset reported
* in the re-scan message above.
*/
continue;
}
/* assert: residue == 0 or is_ok >= INRANGE_DROP here */
/* record of no further interest */
continue;
if (want_ok == INRANGE_ALIGNED) {
wcomplain("now recognizing aligned records again");
}
/*
* 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.
*/
/* now we have a good wtmpx record, do more processing */
/* inrange() already checked the "run-level " part */
}
"wtmpfix: logname \"%*.*s\" changed "
"to \"INVALID\"\n", OUTPUT_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.
*/
/*
* Make recin refer to the expected NEW_TIME.
* Loop continuation will increment it again
* for the record we're about to read now.
*/
wcomplain("input truncated after OLD_TIME - "
"giving up");
}
/*
* 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.
*/
wcomplain("NEW_TIME expected but missing "
"after OLD_TIME - giving up");
}
perror("<temporary file>: fwrite");
}
continue;
}
perror("<temporary file>: fwrite");
}
}
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");
/*
* 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 \
static inrange_t
inrange()
{
/* pid_t is signed so that fork() can return -1. Exploit this. */
wcomplain("negative pid");
return (INRANGE_ERR);
}
/* the legal values for ut_type are enumerated in <utmp.h> */
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 */
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);
}
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);
}
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);
}
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].
*/
return (INRANGE_ALIGNED);
} else {
return (INRANGE_DROP);
}
}
return (INRANGE_ALIGNED);
} else {
return (INRANGE_DROP);
}
}
/* FALLTHROUGH */
case LOGIN_PROCESS:
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
* But in an INIT_PROCESS, LOGIN_PROCESS, or USER_PROCESS
* record, we expect a respectable ut_pid.
*/
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.
*/
wcomplain("non-positive time in process record");
return (INRANGE_ERR);
}
return (INRANGE_PASS);
} else {
return (INRANGE_DROP);
}
case ACCOUNTING:
/*
* If we recognize one of the three reason strings passed
* 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.
*/
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);
}
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
{
}