root/sbin/ncheck_ffs/ncheck_ffs.c
/*      $OpenBSD: ncheck_ffs.c,v 1.56 2024/04/23 13:34:50 jsg Exp $     */

/*-
 * Copyright (c) 1995, 1996 SigmaSoft, Th. Lockert <tholo@sigmasoft.com>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
 * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/*-
 * Copyright (c) 1980, 1988, 1991, 1993
 *      The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 */

#include <sys/param.h>  /* MAXBSIZE DEV_BSIZE */
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/ioctl.h>
#include <sys/disklabel.h>
#include <sys/dkio.h>
#include <ufs/ffs/fs.h>
#include <ufs/ufs/dir.h>
#include <ufs/ufs/dinode.h>

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <limits.h>
#include <fstab.h>
#include <errno.h>
#include <err.h>
#include <util.h>

#define DIP(dp, field) \
    ((sblock->fs_magic == FS_UFS1_MAGIC) ? \
    ((struct ufs1_dinode *)(dp))->field : \
    ((struct ufs2_dinode *)(dp))->field)

char    *disk;          /* name of the disk file */
char    rdisk[PATH_MAX];/* resolved name of the disk file */
int     diskfd;         /* disk file descriptor */
struct  fs *sblock;     /* the file system super block */
char    sblock_buf[MAXBSIZE];
int     sblock_try[] = SBLOCKSEARCH; /* possible superblock locations */
ufsino_t *ilist;        /* list of inodes to check */
int     ninodes;        /* number of inodes in list */
int     sflag;          /* only suid and special files */
int     aflag;          /* print the . and .. entries too */
int     mflag;          /* verbose output */
int     iflag;          /* specific inode */
char    *format;        /* output format */

struct disklabel lab;

struct icache_s {
        ufsino_t        ino;
        union {
                struct ufs1_dinode dp1;
                struct ufs2_dinode dp2;
        } di;
} *icache;
int     nicache;
int     maxicache;

void addinode(ufsino_t inum);
void *getino(ufsino_t inum);
void findinodes(ufsino_t);
void bread(daddr_t, char *, int);
__dead void usage(void);
void scanonedir(ufsino_t, const char *);
void dirindir(ufsino_t, daddr_t, int, off_t *, const char *);
void searchdir(ufsino_t, daddr_t, long, off_t, const char *);
int matchino(const void *, const void *);
int matchcache(const void *, const void *);
void cacheino(ufsino_t, void *);
void *cached(ufsino_t);
int main(int, char *[]);
char *rawname(char *);
void format_entry(const char *, struct direct *);

/*
 * Check to see if the indicated inodes are the same
 */
int
matchino(const void *key, const void *val)
{
        ufsino_t k = *(ufsino_t *)key;
        ufsino_t v = *(ufsino_t *)val;

        if (k < v)
                return -1;
        else if (k > v)
                return 1;
        return 0;
}

/*
 * Check if the indicated inode match the entry in the cache
 */
int
matchcache(const void *key, const void *val)
{
        ufsino_t        ino = *(ufsino_t *)key;
        struct icache_s *ic = (struct icache_s *)val;

        if (ino < ic->ino)
                return -1;
        else if (ino > ic->ino)
                return 1;
        return 0;
}

/*
 * Add an inode to the cached entries
 */
void
cacheino(ufsino_t ino, void *dp)
{
        if (nicache == maxicache) {
                struct icache_s *newicache;

                /* grow exponentially */
                maxicache += 10 + maxicache/2;
                newicache = reallocarray(icache, maxicache, sizeof(*icache));
                if (newicache == NULL)
                        errx(1, "malloc");
                icache = newicache;

        }
        icache[nicache].ino = ino;
        if (sblock->fs_magic == FS_UFS1_MAGIC)
                icache[nicache++].di.dp1 = *(struct ufs1_dinode *)dp;
        else
                icache[nicache++].di.dp2 = *(struct ufs2_dinode *)dp;
}

/*
 * Get a cached inode
 */
void *
cached(ufsino_t ino)
{
        struct icache_s *ic;
        void *dp = NULL;

        ic = bsearch(&ino, icache, nicache, sizeof(*icache), matchcache);
        if (ic != NULL) {
                if (sblock->fs_magic == FS_UFS1_MAGIC)
                        dp = &ic->di.dp1;
                else
                        dp = &ic->di.dp2;
        }
        return (dp);
}

/*
 * Walk the inode list for a filesystem to find all allocated inodes
 * Remember inodes we want to give information about and cache all
 * inodes pointing to directories
 */
void
findinodes(ufsino_t maxino)
{
        ufsino_t ino;
        void *dp;
        mode_t mode;

        for (ino = ROOTINO; ino < maxino; ino++) {
                dp = getino(ino);
                mode = DIP(dp, di_mode) & IFMT;
                if (!mode)
                        continue;
                if (mode == IFDIR)
                        cacheino(ino, dp);
                if (iflag ||
                    (sflag && (mode == IFDIR ||
                     ((DIP(dp, di_mode) & (ISGID | ISUID)) == 0 &&
                      (mode == IFREG || mode == IFLNK)))))
                        continue;
                addinode(ino);
        }
}

/*
 * Get a specified inode from disk.  Attempt to minimize reads to once
 * per cylinder group
 */
void *
getino(ufsino_t inum)
{
        static char *itab = NULL;
        static daddr_t iblk = -1;
        void *dp;
        size_t dsize;

        if (inum < ROOTINO || inum >= sblock->fs_ncg * sblock->fs_ipg)
                return NULL;
        if ((dp = cached(inum)) != NULL)
                return dp;
        if (sblock->fs_magic == FS_UFS1_MAGIC)
                dsize = sizeof(struct ufs1_dinode);
        else
                dsize = sizeof(struct ufs2_dinode);
        if ((inum / sblock->fs_ipg) != iblk || itab == NULL) {
                iblk = inum / sblock->fs_ipg;
                if (itab == NULL &&
                    (itab = reallocarray(NULL, sblock->fs_ipg, dsize)) == NULL)
                        errx(1, "no memory for inodes");
                bread(fsbtodb(sblock, cgimin(sblock, iblk)), itab,
                      sblock->fs_ipg * dsize);
        }
        return itab + (inum % sblock->fs_ipg) * dsize;
}

/*
 * Read a chunk of data from the disk.
 * Try to recover from hard errors by reading in sector sized pieces.
 * Error recovery is attempted at most BREADEMAX times before seeking
 * consent from the operator to continue.
 */
int     breaderrors = 0;
#define BREADEMAX 32

void
bread(daddr_t blkno, char *buf, int size)
{
        off_t offset;
        int cnt, i;
        u_int32_t secsize = lab.d_secsize;

        offset = blkno * DEV_BSIZE;

loop:
        if ((cnt = pread(diskfd, buf, size, offset)) == size)
                return;
        if (blkno + (size / DEV_BSIZE) >
            fsbtodb(sblock, sblock->fs_ffs1_size)) {
                /*
                 * Trying to read the final fragment.
                 *
                 * NB - dump only works in TP_BSIZE blocks, hence
                 * rounds `DEV_BSIZE' fragments up to TP_BSIZE pieces.
                 * It should be smarter about not actually trying to
                 * read more than it can get, but for the time being
                 * we punt and scale back the read only when it gets
                 * us into trouble. (mkm 9/25/83)
                 */
                size -= secsize;
                goto loop;
        }
        if (cnt == -1)
                warnx("read error from %s: %s: [block %lld]: count=%d",
                    disk, strerror(errno), (long long)blkno, size);
        else
                warnx("short read error from %s: [block %lld]: count=%d, "
                    "got=%d", disk, (long long)blkno, size, cnt);
        if (++breaderrors > BREADEMAX)
                errx(1, "More than %d block read errors from %s", BREADEMAX,
                    disk);
        /*
         * Zero buffer, then try to read each sector of buffer separately.
         */
        memset(buf, 0, size);
        for (i = 0; i < size; i += secsize, buf += secsize) {
                if ((cnt = pread(diskfd, buf, secsize, offset + i)) ==
                    secsize)
                        continue;
                if (cnt == -1) {
                        warnx("read error from %s: %s: [sector %lld]: "
                            "count=%u", disk, strerror(errno),
                            (long long)(offset + i) / DEV_BSIZE, secsize);
                        continue;
                }
                warnx("short read error from %s: [sector %lld]: count=%u, "
                    "got=%d", disk, (long long)(offset + i) / DEV_BSIZE,
                    secsize, cnt);
        }
}

/*
 * Add an inode to the in-memory list of inodes to dump
 */
void
addinode(ufsino_t ino)
{
        ufsino_t *newilist;

        newilist = reallocarray(ilist, ninodes + 1, sizeof(*ilist));
        if (newilist == NULL)
                errx(4, "not enough memory to allocate tables");
        ilist = newilist;
        ilist[ninodes] = ino;
        ninodes++;
}

/*
 * Scan the directory pointer at by ino
 */
void
scanonedir(ufsino_t ino, const char *path)
{
        void *dp;
        off_t filesize;
        int i;

        if ((dp = cached(ino)) == NULL)
                return;
        filesize = (off_t)DIP(dp, di_size);
        for (i = 0; filesize > 0 && i < NDADDR; i++) {
                if (DIP(dp, di_db[i]) != 0) {
                        searchdir(ino, DIP(dp, di_db[i]),
                            sblksize(sblock, DIP(dp, di_size), i),
                            filesize, path);
                }
                filesize -= sblock->fs_bsize;
        }
        for (i = 0; filesize > 0 && i < NIADDR; i++) {
                if (DIP(dp, di_ib[i]))
                        dirindir(ino, DIP(dp, di_ib[i]), i, &filesize, path);
        }
}

/*
 * Read indirect blocks, and pass the data blocks to be searched
 * as directories.
 */
void
dirindir(ufsino_t ino, daddr_t blkno, int ind_level, off_t *filesizep,
    const char *path)
{
        int i;
        void *idblk;

        if ((idblk = malloc(sblock->fs_bsize)) == NULL)
                errx(1, "dirindir: cannot allocate indirect memory.\n");
        bread(fsbtodb(sblock, blkno), idblk, (int)sblock->fs_bsize);
        if (ind_level <= 0) {
                for (i = 0; *filesizep > 0 && i < NINDIR(sblock); i++) {
                        if (sblock->fs_magic == FS_UFS1_MAGIC)
                                blkno = ((int32_t *)idblk)[i];
                        else
                                blkno = ((int64_t *)idblk)[i];
                        if (blkno != 0)
                                searchdir(ino, blkno, sblock->fs_bsize,
                                    *filesizep, path);
                        *filesizep -= sblock->fs_bsize;
                }
        } else {
                ind_level--;
                for (i = 0; *filesizep > 0 && i < NINDIR(sblock); i++) {
                        if (sblock->fs_magic == FS_UFS1_MAGIC)
                                blkno = ((int32_t *)idblk)[i];
                        else
                                blkno = ((int64_t *)idblk)[i];
                        if (blkno != 0)
                                dirindir(ino, blkno, ind_level, filesizep,
                                    path);
                }
        }
        free(idblk);
}

/*
 * Scan a disk block containing directory information looking to see if
 * any of the entries are on the dump list and to see if the directory
 * contains any subdirectories.
 */
void
searchdir(ufsino_t ino, daddr_t blkno, long size, off_t filesize,
    const char *path)
{
        char *dblk;
        struct direct *dp;
        void *di;
        mode_t mode;
        char *npath;
        ufsino_t subino;
        long loc;

        if ((dblk = malloc(sblock->fs_bsize)) == NULL)
                errx(1, "searchdir: cannot allocate directory memory.");
        bread(fsbtodb(sblock, blkno), dblk, (int)size);
        if (filesize < size)
                size = filesize;
        for (loc = 0; loc < size; ) {
                dp = (struct direct *)(dblk + loc);
                if (dp->d_reclen == 0) {
                        warnx("corrupted directory, inode %llu",
                            (unsigned long long)ino);
                        break;
                }
                loc += dp->d_reclen;
                if (dp->d_ino == 0)
                        continue;
                if (dp->d_name[0] == '.') {
                        if (!aflag && (dp->d_name[1] == '\0' ||
                            (dp->d_name[1] == '.' && dp->d_name[2] == '\0')))
                                continue;
                }
                di = getino(dp->d_ino);
                mode = DIP(di, di_mode) & IFMT;
                subino = dp->d_ino;
                if (bsearch(&subino, ilist, ninodes, sizeof(*ilist), matchino)) {
                        if (format) {
                                format_entry(path, dp);
                        } else {
                                if (mflag)
                                        printf("mode %-6o uid %-5u gid %-5u ino ",
                                            DIP(di, di_mode), DIP(di, di_uid),
                                            DIP(di, di_gid));
                                printf("%-7llu %s/%s%s\n",
                                    (unsigned long long)dp->d_ino, path,
                                    dp->d_name, mode == IFDIR ? "/." : "");
                        }
                }
                if (mode == IFDIR) {
                        if (dp->d_name[0] == '.') {
                                if (dp->d_name[1] == '\0' ||
                                    (dp->d_name[1] == '.' && dp->d_name[2] == '\0'))
                                        continue;
                        }
                        if (asprintf(&npath, "%s/%s", path, dp->d_name) == -1)
                                errx(1, "malloc");
                        scanonedir(dp->d_ino, npath);
                        free(npath);
                }
        }
        free(dblk);
}

char *
rawname(char *name)
{
        static char newname[PATH_MAX];
        char *p;

        if ((p = strrchr(name, '/')) == NULL)
                return name;
        *p = '\0';
        strlcpy(newname, name, sizeof newname - 2);
        *p++ = '/';
        strlcat(newname, "/r", sizeof newname);
        strlcat(newname, p, sizeof newname);
        return(newname);
}

__dead void
usage(void)
{
        extern char *__progname;

        fprintf(stderr,
            "usage: %s [-ams] [-f format] [-i number ...] filesystem\n",
            __progname);
        exit(3);
}

int
main(int argc, char *argv[])
{
        struct stat stblock;
        struct fstab *fsp;
        unsigned long long ullval;
        ssize_t n;
        char *ep;
        int c, i;

        while ((c = getopt(argc, argv, "af:i:ms")) != -1)
                switch (c) {
                case 'a':
                        aflag = 1;
                        break;
                case 'i':
                        iflag = 1;

                        errno = 0;
                        ullval = strtoull(optarg, &ep, 10);
                        if (optarg[0] == '\0' || *ep != '\0')
                                errx(1, "%s is not a number",
                                    optarg);
                        if ((errno == ERANGE && ullval == ULLONG_MAX) ||
                            (ufsino_t)ullval != ullval)
                                errx(1, "%s is out of range",
                                    optarg);
                        addinode((ufsino_t)ullval);

                        while (optind < argc) {
                                errno = 0;
                                ullval = strtoull(argv[optind], &ep, 10);
                                if (argv[optind][0] == '\0' || *ep != '\0')
                                        break;
                                if ((errno == ERANGE && ullval == ULLONG_MAX)
                                    || (ufsino_t)ullval != ullval)
                                        errx(1, "%s is out of range",
                                            argv[optind]);
                                addinode((ufsino_t)ullval);
                                optind++;
                        }
                        break;
                case 'f':
                        format = optarg;
                        break;
                case 'm':
                        mflag = 1;
                        break;
                case 's':
                        sflag = 1;
                        break;
                default:
                        usage();
                        exit(2);
                }
        if (optind != argc - 1 || (mflag && format))
                usage();

        disk = argv[optind];
        if ((diskfd = opendev(disk, O_RDONLY, 0, NULL)) >= 0) {
                if (fstat(diskfd, &stblock))
                        err(1, "cannot stat %s", disk);
                if (S_ISCHR(stblock.st_mode))
                        goto gotdev;
                close(diskfd);
        }

        if (realpath(disk, rdisk) == NULL)
                err(1, "cannot find real path for %s", disk);
        disk = rdisk;

        if (stat(disk, &stblock) == -1)
                err(1, "cannot stat %s", disk);

        if (S_ISBLK(stblock.st_mode)) {
                disk = rawname(disk);
        } else if (!S_ISCHR(stblock.st_mode)) {
                if ((fsp = getfsfile(disk)) == NULL)
                        err(1, "could not find file system %s", disk);
                disk = rawname(fsp->fs_spec);
        }

        if ((diskfd = opendev(disk, O_RDONLY, 0, NULL)) == -1)
                err(1, "cannot open %s", disk);

gotdev:
        if (ioctl(diskfd, DIOCGDINFO, (char *)&lab) == -1)
                err(1, "ioctl (DIOCGDINFO)");
        if (ioctl(diskfd, DIOCGPDINFO, (char *)&lab) == -1)
                err(1, "ioctl (DIOCGPDINFO)");

        if (pledge("stdio", NULL) == -1)
                err(1, "pledge");

        sblock = (struct fs *)sblock_buf;
        for (i = 0; sblock_try[i] != -1; i++) {
                n = pread(diskfd, sblock, SBLOCKSIZE, (off_t)sblock_try[i]);
                if (n == SBLOCKSIZE && (sblock->fs_magic == FS_UFS1_MAGIC ||
                     (sblock->fs_magic == FS_UFS2_MAGIC &&
                      sblock->fs_sblockloc == sblock_try[i])) &&
                    sblock->fs_bsize <= MAXBSIZE &&
                    sblock->fs_bsize >= sizeof(struct fs))
                        break;
        }
        if (sblock_try[i] == -1)
                errx(1, "cannot find filesystem superblock");

        findinodes(sblock->fs_ipg * sblock->fs_ncg);
        if (!format)
                printf("%s:\n", disk);
        scanonedir(ROOTINO, "");
        close(diskfd);
        exit (0);
}

void
format_entry(const char *path, struct direct *dp)
{
        static size_t size;
        static char *buf;
        char *src, *dst, *newbuf;
        int len;

        if (buf == NULL) {
                if ((buf = malloc(LINE_MAX)) == NULL)
                        err(1, "malloc");
                size = LINE_MAX;
        }

        for (src = format, dst = buf; *src; src++) {
                /* Need room for at least one character in buf. */
                if (size <= dst - buf) {
                    expand_buf:
                        if ((newbuf = reallocarray(buf, size, 2)) == NULL)
                                err(1, "realloc");
                        buf = newbuf;
                        size = size * 2;
                }
                if (src[0] =='\\') {
                        switch (src[1]) {
                        case 'I':
                                len = snprintf(dst, size - (dst - buf), "%llu",
                                    (unsigned long long)dp->d_ino);
                                if (len < 0 || len >= size - (dst - buf))
                                        goto expand_buf;
                                dst += len;
                                break;
                        case 'P':
                                len = snprintf(dst, size - (dst - buf), "%s/%s",
                                    path, dp->d_name);
                                if (len < 0 || len >= size - (dst - buf))
                                        goto expand_buf;
                                dst += len;
                                break;
                        case '\\':
                                *dst++ = '\\';
                                break;
                        case '0':
                                /* XXX - support other octal numbers? */
                                *dst++ = '\0';
                                break;
                        case 'a':
                                *dst++ = '\a';
                                break;
                        case 'b':
                                *dst++ = '\b';
                                break;
                        case 'e':
                                *dst++ = '\e';
                                break;
                        case 'f':
                                *dst++ = '\f';
                                break;
                        case 'n':
                                *dst++ = '\n';
                                break;
                        case 'r':
                                *dst++ = '\r';
                                break;
                        case 't':
                                *dst++ = '\t';
                                break;
                        case 'v':
                                *dst++ = '\v';
                                break;
                        default:
                                *dst++ = src[1];
                                break;
                        }
                        src++;
                } else
                        *dst++ = *src;
        }
        fwrite(buf, dst - buf, 1, stdout);
}