From 8f829a5cf0fa341a95fd4bed7091d6c44129d68f Mon Sep 17 00:00:00 2001 From: Kirk McKusick Date: Tue, 11 Dec 2018 22:14:37 +0000 Subject: [PATCH] Continuing efforts to provide hardening of FFS. This change adds a check hash to the filesystem inodes. Access attempts to files associated with an inode with an invalid check hash will fail with EINVAL (Invalid argument). Access is reestablished after an fsck is run to find and validate the inodes with invalid check-hashes. This check avoids a class of filesystem panics related to corrupted inodes. The hash is done using crc32c. Note this check-hash is for the inode itself and not any of its indirect blocks. Check-hash validation may be extended to also cover indirect block pointers, but that will be a separate (and more costly) feature. Check hashes are added only to UFS2 and not to UFS1 as UFS1 is primarily used in embedded systems with small memories and low-powered processors which need as light-weight a filesystem as possible. Reviewed by: kib Tested by: Peter Holm Sponsored by: Netflix --- lib/libufs/inode.c | 7 +++++- lib/libufs/libufs.h | 2 ++ sbin/fsck_ffs/inode.c | 30 +++++++++++++++++++++++- sbin/fsck_ffs/main.c | 2 +- sbin/fsirand/fsirand.c | 1 + sbin/newfs/mkfs.c | 2 ++ sys/sys/param.h | 3 ++- sys/ufs/ffs/ffs_extern.h | 2 ++ sys/ufs/ffs/ffs_inode.c | 1 + sys/ufs/ffs/ffs_snapshot.c | 2 ++ sys/ufs/ffs/ffs_softdep.c | 11 ++++++++- sys/ufs/ffs/ffs_subr.c | 47 +++++++++++++++++++++++++++++++++++++- sys/ufs/ffs/ffs_vfsops.c | 2 +- sys/ufs/ufs/dinode.h | 3 ++- 14 files changed, 107 insertions(+), 8 deletions(-) diff --git a/lib/libufs/inode.c b/lib/libufs/inode.c index 11e263f08ad5..497ff4c854f6 100644 --- a/lib/libufs/inode.c +++ b/lib/libufs/inode.c @@ -90,7 +90,10 @@ gotit: switch (disk->d_ufs) { disk->d_dp.dp2 = &((struct ufs2_dinode *)inoblock)[inum - min]; if (dp != NULL) *dp = disk->d_dp; - return (0); + if (ffs_verify_dinode_ckhash(fs, disk->d_dp.dp2) == 0) + return (0); + ERROR(disk, "check-hash failed for inode read from disk"); + return (-1); default: break; } @@ -108,6 +111,8 @@ putinode(struct uufsd *disk) ERROR(disk, "No inode block allocated"); return (-1); } + if (disk->d_ufs == 2) + ffs_update_dinode_ckhash(fs, disk->d_dp.dp2); if (bwrite(disk, fsbtodb(fs, ino_to_fsba(&disk->d_fs, disk->d_inomin)), disk->d_inoblock, disk->d_fs.fs_bsize) <= 0) return (-1); diff --git a/lib/libufs/libufs.h b/lib/libufs/libufs.h index 9d1e7355c2f7..44eaed83ccbd 100644 --- a/lib/libufs/libufs.h +++ b/lib/libufs/libufs.h @@ -116,6 +116,8 @@ int ffs_sbget(void *, struct fs **, off_t, char *, int (*)(void *, off_t, void **, int)); int ffs_sbput(void *, struct fs *, off_t, int (*)(void *, off_t, void *, int)); +void ffs_update_dinode_ckhash(struct fs *, struct ufs2_dinode *); +int ffs_verify_dinode_ckhash(struct fs *, struct ufs2_dinode *); /* * Request standard superblock location in ffs_sbget diff --git a/sbin/fsck_ffs/inode.c b/sbin/fsck_ffs/inode.c index a914be8b87a5..6957a41f70c0 100644 --- a/sbin/fsck_ffs/inode.c +++ b/sbin/fsck_ffs/inode.c @@ -286,6 +286,7 @@ union dinode * ginode(ino_t inumber) { ufs2_daddr_t iblk; + union dinode *dp; if (inumber < UFS_ROOTINO || inumber > maxino) errx(EEXIT, "bad inode number %ju to ginode", @@ -301,7 +302,17 @@ ginode(ino_t inumber) if (sblock.fs_magic == FS_UFS1_MAGIC) return ((union dinode *) &pbp->b_un.b_dinode1[inumber % INOPB(&sblock)]); - return ((union dinode *)&pbp->b_un.b_dinode2[inumber % INOPB(&sblock)]); + dp = (union dinode *)&pbp->b_un.b_dinode2[inumber % INOPB(&sblock)]; + if (ffs_verify_dinode_ckhash(&sblock, (struct ufs2_dinode *)dp) != 0) { + pwarn("INODE CHECK-HASH FAILED"); + prtinode(inumber, dp); + if (preen || reply("FIX") != 0) { + if (preen) + printf(" (FIXED)\n"); + inodirty(dp); + } + } + return (dp); } /* @@ -347,6 +358,21 @@ getnextinode(ino_t inumber, int rebuildcg) nextinop += sizeof(struct ufs1_dinode); else nextinop += sizeof(struct ufs2_dinode); + if ((ckhashadd & CK_INODE) != 0) { + ffs_update_dinode_ckhash(&sblock, (struct ufs2_dinode *)dp); + dirty(&inobuf); + } + if (ffs_verify_dinode_ckhash(&sblock, (struct ufs2_dinode *)dp) != 0) { + pwarn("INODE CHECK-HASH FAILED"); + prtinode(inumber, dp); + if (preen || reply("FIX") != 0) { + if (preen) + printf(" (FIXED)\n"); + ffs_update_dinode_ckhash(&sblock, + (struct ufs2_dinode *)dp); + dirty(&inobuf); + } + } if (rebuildcg && (char *)dp == inobuf.b_un.b_buf) { /* * Try to determine if we have reached the end of the @@ -522,6 +548,8 @@ void inodirty(union dinode *dp) { + if (sblock.fs_magic == FS_UFS2_MAGIC) + ffs_update_dinode_ckhash(&sblock, (struct ufs2_dinode *)dp); dirty(pbp); } diff --git a/sbin/fsck_ffs/main.c b/sbin/fsck_ffs/main.c index ce4d0a89a36a..47bd3987c8e8 100644 --- a/sbin/fsck_ffs/main.c +++ b/sbin/fsck_ffs/main.c @@ -468,13 +468,13 @@ checkfilesys(char *filesys) ckhashadd |= CK_SUPERBLOCK; sblock.fs_metackhash |= CK_SUPERBLOCK; } -#ifdef notyet if ((sblock.fs_metackhash & CK_INODE) == 0 && getosreldate() >= P_OSREL_CK_INODE && reply("ADD INODE CHECK-HASH PROTECTION") != 0) { ckhashadd |= CK_INODE; sblock.fs_metackhash |= CK_INODE; } +#ifdef notyet if ((sblock.fs_metackhash & CK_INDIR) == 0 && getosreldate() >= P_OSREL_CK_INDIR && reply("ADD INDIRECT BLOCK CHECK-HASH PROTECTION") != 0) { diff --git a/sbin/fsirand/fsirand.c b/sbin/fsirand/fsirand.c index 8675f7c3fd33..f24d34705b72 100644 --- a/sbin/fsirand/fsirand.c +++ b/sbin/fsirand/fsirand.c @@ -202,6 +202,7 @@ fsirand(char *device) dp1++; } else { dp2->di_gen = arc4random(); + ffs_update_dinode_ckhash(sblock, dp2); dp2++; } } diff --git a/sbin/newfs/mkfs.c b/sbin/newfs/mkfs.c index 2bea89cf2bc9..96849a791714 100644 --- a/sbin/newfs/mkfs.c +++ b/sbin/newfs/mkfs.c @@ -499,6 +499,8 @@ restart: sblock.fs_metackhash |= CK_CYLGRP; if (getosreldate() >= P_OSREL_CK_SUPERBLOCK) sblock.fs_metackhash |= CK_SUPERBLOCK; + if (getosreldate() >= P_OSREL_CK_INODE) + sblock.fs_metackhash |= CK_INODE; } /* diff --git a/sys/sys/param.h b/sys/sys/param.h index 64aa65bef7b0..a28bc884d25c 100644 --- a/sys/sys/param.h +++ b/sys/sys/param.h @@ -60,7 +60,7 @@ * in the range 5 to 9. */ #undef __FreeBSD_version -#define __FreeBSD_version 1300004 /* Master, propagated to newvers */ +#define __FreeBSD_version 1300005 /* Master, propagated to newvers */ /* * __FreeBSD_kernel__ indicates that this system uses the kernel of FreeBSD, @@ -89,6 +89,7 @@ #define P_OSREL_CK_CYLGRP 1200046 #define P_OSREL_VMTOTAL64 1200054 #define P_OSREL_CK_SUPERBLOCK 1300000 +#define P_OSREL_CK_INODE 1300005 #define P_OSREL_MAJOR(x) ((x) / 100000) #endif diff --git a/sys/ufs/ffs/ffs_extern.h b/sys/ufs/ffs/ffs_extern.h index 16f517d5f12e..f7d95404c046 100644 --- a/sys/ufs/ffs/ffs_extern.h +++ b/sys/ufs/ffs/ffs_extern.h @@ -108,6 +108,8 @@ void ffs_sync_snap(struct mount *, int); int ffs_syncvnode(struct vnode *vp, int waitfor, int flags); int ffs_truncate(struct vnode *, off_t, int, struct ucred *); int ffs_update(struct vnode *, int); +void ffs_update_dinode_ckhash(struct fs *, struct ufs2_dinode *); +int ffs_verify_dinode_ckhash(struct fs *, struct ufs2_dinode *); int ffs_valloc(struct vnode *, int, struct ucred *, struct vnode **); int ffs_vfree(struct vnode *, ino_t, int); vfs_vget_t ffs_vget; diff --git a/sys/ufs/ffs/ffs_inode.c b/sys/ufs/ffs/ffs_inode.c index d146d14980c3..bbacaaa59e5f 100644 --- a/sys/ufs/ffs/ffs_inode.c +++ b/sys/ufs/ffs/ffs_inode.c @@ -154,6 +154,7 @@ loop: */ random_harvest_queue(&(ip->i_din1), sizeof(ip->i_din1), RANDOM_FS_ATIME); } else { + ffs_update_dinode_ckhash(fs, ip->i_din2); *((struct ufs2_dinode *)bp->b_data + ino_to_fsbo(fs, ip->i_number)) = *ip->i_din2; /* diff --git a/sys/ufs/ffs/ffs_snapshot.c b/sys/ufs/ffs/ffs_snapshot.c index e230d4b0c6c8..092720a9cbeb 100644 --- a/sys/ufs/ffs/ffs_snapshot.c +++ b/sys/ufs/ffs/ffs_snapshot.c @@ -1340,6 +1340,8 @@ expunge_ufs2(snapvp, cancelip, fs, acctfunc, expungetype, clearmode) bzero(&dip->di_db[0], (UFS_NDADDR + UFS_NIADDR) * sizeof(ufs2_daddr_t)); if (clearmode || cancelip->i_effnlink == 0) dip->di_mode = 0; + else + ffs_update_dinode_ckhash(fs, dip); bdwrite(bp); /* * Now go through and expunge all the blocks in the file diff --git a/sys/ufs/ffs/ffs_softdep.c b/sys/ufs/ffs/ffs_softdep.c index aad19c6014cd..eb093d4bf419 100644 --- a/sys/ufs/ffs/ffs_softdep.c +++ b/sys/ufs/ffs/ffs_softdep.c @@ -6702,6 +6702,7 @@ softdep_journal_freeblocks(ip, cred, length, flags) *((struct ufs1_dinode *)bp->b_data + ino_to_fsbo(fs, ip->i_number)) = *ip->i_din1; } else { + ffs_update_dinode_ckhash(fs, ip->i_din2); *((struct ufs2_dinode *)bp->b_data + ino_to_fsbo(fs, ip->i_number)) = *ip->i_din2; } @@ -6957,6 +6958,7 @@ softdep_setup_freeblocks(ip, length, flags) dp2 = ((struct ufs2_dinode *)bp->b_data + ino_to_fsbo(fs, ip->i_number)); ip->i_din2->di_freelink = dp2->di_freelink; + ffs_update_dinode_ckhash(fs, ip->i_din2); *dp2 = *ip->i_din2; } /* @@ -9752,6 +9754,7 @@ clear_unlinked_inodedep(inodedep) dip = (struct ufs2_dinode *)bp->b_data + ino_to_fsbo(fs, pino); dip->di_freelink = nino; + ffs_update_dinode_ckhash(fs, dip); } /* * If the bwrite fails we have no recourse to recover. The @@ -10392,6 +10395,7 @@ initiate_write_inodeblock_ufs2(inodedep, bp) inon = TAILQ_NEXT(inodedep, id_unlinked); dp->di_freelink = inon ? inon->id_ino : 0; + ffs_update_dinode_ckhash(fs, dp); } /* * If the bitmap is not yet written, then the allocated @@ -10553,6 +10557,7 @@ initiate_write_inodeblock_ufs2(inodedep, bp) #endif /* INVARIANTS */ dp->di_ib[i] = 0; } + ffs_update_dinode_ckhash(fs, dp); return; } /* @@ -10581,6 +10586,7 @@ initiate_write_inodeblock_ufs2(inodedep, bp) */ for (; adp; adp = TAILQ_NEXT(adp, ad_next)) dp->di_ib[adp->ad_offset - UFS_NDADDR] = 0; + ffs_update_dinode_ckhash(fs, dp); } /* @@ -11619,8 +11625,11 @@ handle_written_inodeblock(inodedep, bp, flags) * marked dirty so that its will eventually get written back in * its correct form. */ - if (hadchanges) + if (hadchanges) { + if (fstype == UFS2) + ffs_update_dinode_ckhash(inodedep->id_fs, dp2); bdirty(bp); + } bufwait: /* * If the write did not succeed, we have done all the roll-forward diff --git a/sys/ufs/ffs/ffs_subr.c b/sys/ufs/ffs/ffs_subr.c index 64354bbd5383..2891c8d8fe96 100644 --- a/sys/ufs/ffs/ffs_subr.c +++ b/sys/ufs/ffs/ffs_subr.c @@ -119,6 +119,7 @@ ffs_load_inode(struct buf *bp, struct inode *ip, struct fs *fs, ino_t ino) { struct ufs1_dinode *dip1; struct ufs2_dinode *dip2; + int error; if (I_IS_UFS1(ip)) { dip1 = ip->i_din1; @@ -142,10 +143,54 @@ ffs_load_inode(struct buf *bp, struct inode *ip, struct fs *fs, ino_t ino) ip->i_gen = dip2->di_gen; ip->i_uid = dip2->di_uid; ip->i_gid = dip2->di_gid; - return (0); + if ((error = ffs_verify_dinode_ckhash(fs, dip2)) != 0) + printf("Inode %jd: check-hash failed\n", (intmax_t)ino); + return (error); } #endif /* _KERNEL */ +/* + * Verify an inode check-hash. + */ +int +ffs_verify_dinode_ckhash(struct fs *fs, struct ufs2_dinode *dip) +{ + uint32_t save_ckhash; + + /* + * Return success if unallocated or we are not doing inode check-hash. + */ + if (dip->di_mode == 0 || (fs->fs_metackhash & CK_INODE) == 0) + return (0); + /* + * Exclude di_ckhash from the crc32 calculation, e.g., always use + * a check-hash value of zero when calculating the check-hash. + */ + save_ckhash = dip->di_ckhash; + dip->di_ckhash = 0; + if (save_ckhash != calculate_crc32c(~0L, (void *)dip, sizeof(*dip))) + return (EINVAL); + dip->di_ckhash = save_ckhash; + return (0); +} + +/* + * Update an inode check-hash. + */ +void +ffs_update_dinode_ckhash(struct fs *fs, struct ufs2_dinode *dip) +{ + + if (dip->di_mode == 0 || (fs->fs_metackhash & CK_INODE) == 0) + return; + /* + * Exclude old di_ckhash from the crc32 calculation, e.g., always use + * a check-hash value of zero when calculating the new check-hash. + */ + dip->di_ckhash = 0; + dip->di_ckhash = calculate_crc32c(~0L, (void *)dip, sizeof(*dip)); +} + /* * These are the low-level functions that actually read and write * the superblock and its associated data. diff --git a/sys/ufs/ffs/ffs_vfsops.c b/sys/ufs/ffs/ffs_vfsops.c index 7cb81cfa450a..15b208c5c06a 100644 --- a/sys/ufs/ffs/ffs_vfsops.c +++ b/sys/ufs/ffs/ffs_vfsops.c @@ -817,7 +817,7 @@ ffs_mountfs(devvp, mp, td) if ((error = ffs_sbget(devvp, &fs, loc, M_UFSMNT, ffs_use_bread)) != 0) goto out; /* none of these types of check-hashes are maintained by this kernel */ - fs->fs_metackhash &= ~(CK_INODE | CK_INDIR | CK_DIR); + fs->fs_metackhash &= ~(CK_INDIR | CK_DIR); /* no support for any undefined flags */ fs->fs_flags &= FS_SUPPORTED; fs->fs_flags &= ~FS_UNCLEAN; diff --git a/sys/ufs/ufs/dinode.h b/sys/ufs/ufs/dinode.h index 0a201ce0b43b..1f0f25c4d5ec 100644 --- a/sys/ufs/ufs/dinode.h +++ b/sys/ufs/ufs/dinode.h @@ -149,7 +149,8 @@ struct ufs2_dinode { ufs2_daddr_t di_ib[UFS_NIADDR]; /* 208: Indirect disk blocks. */ u_int64_t di_modrev; /* 232: i_modrev for NFSv4 */ uint32_t di_freelink; /* 240: SUJ: Next unlinked inode. */ - uint32_t di_spare[3]; /* 244: Reserved; currently unused */ + uint32_t di_ckhash; /* 244: if CK_INODE, its check-hash */ + uint32_t di_spare[2]; /* 248: Reserved; currently unused */ }; /*