openbsd-ext4/writeup/1.md

481 lines
24 KiB
Markdown
Raw Permalink Normal View History

# A quick glance
We can start at ext2fs/ext2fs.h and see what's in there.
At first sight, we are presented with the following line:
```c
#include <sys/endian.h>
```
> Endianness is essentially bit order. On big endian systems, 0xABCDEF would be stored as the bytes [AB, CD, EF]. On little endian systems, the bytes would be [EF, CD, AB]. This is commonly implemented for computational efficiency, with little difference to the end user. Of course, as low level developers, this is something we will have to keep in mind.
Now if we quickly glance at kernel.org, we can see the following:
> All fields in ext4 are written to disk in little-endian order. HOWEVER, all fields in jbd2 (the journal) are written to disk in big-endian order.^[1](https://docs.kernel.org/filesystems/ext4/overview.html)^
Journaling was already implemented in ext3, and the fields in the super block are implemented as incomplete features in the ext2 source. This is irrelevant to us for now.
```c
#define BBSIZE 1024
#define SBSIZE 1024
#define BBOFF ((off_t)(0))
#define SBOFF ((off_t)(BBOFF + BBSIZE))
```
These are some standard macros to be used later in the code. All that matters is how the sizes of everything are all standardized.
The super-block is the first 1 KB of data on the disk. It contains information about what files are present, disk health information, the amount of cylinders on the disk, and other technical information that allows us to optimize reading the filesystem, instead of the OS having to analyze each disk.
The boot-block is 1 KB. ext4 allows no more than 1 KB of instructions to load up the filesystem. On MBR, the BIOS loads the first 512 bytes into memory, and on UEFI, there is a FAT32 filesystem with a bootable flag which instructs the BIOS on how to load the kernel. Those are "first-stage" bootloaders. The remainder of the 1 KB on disk is the "second-stage" bootloader. After this, you can load more bootloaders as necessary or get right into the kernel.
The following line:
```c
#define BBLOCK ((daddr_t)(0))
#define SBLOCK ((daddr_t)(BBLOCK + BBSIZE / DEV_BSIZE))
```
defines the block address of the boot and super block in memory. Obviously, the boot block is at address 0, but the super block's address depends on how many bytes fit in each block, or 1024 over that amount. `DEV_BSIZE` is a constant which has not been defined at this time, so hopefully we can figure out what it is going forward 🙏
> Inodes are, like in UFS, 32-bit unsigned integers and therefore ufsino_t.
> Disk blocks are 32-bit, if the filesystem isn't operating in 64-bit mode
> (the incompatible ext4 64BIT flag). More work is needed to properly use
> daddr_t as the disk block data type on both BE and LE architectures.
> XXX disk blocks are simply u_int32_t for now.
say the OpenBSD developers. The only point worth noting from this is that we have to implement 64-bit mode moving forward.
```c
#define LOG_MINBSIZE 10
#define MINBSIZE (1 << LOG_MINBSIZE)
#define LOG_MINFSIZE 10
#define MINFSIZE (1 << LOG_MINFSIZE)
```
Each block is a fragment of 1024 bytes at minimum. ext4 was likely designed with the hope that eventually block sizes would increase as disk drive storage increases, and that it would remain extensible for years to come.
```c
#define MAXMNTLEN 512
```
The maximum length of a mount point is 512 characters. You can test this with `mount -t ext2fs <filesystem>`. Even on other systems, it's not likely to work beyond 512 bytes.
2024-08-16 20:50:09 +02:00
```c
#define MINFREE 5
```
As the comment explains pretty well, 5% of blocks should be free. Read the comment from lines 89-100 for the full details.
2024-08-16 20:50:09 +02:00
```c
struct ext2fs {
u_int32_t e2fs_icount; /* Inode count */
u_int32_t e2fs_bcount; /* blocks count */
u_int32_t e2fs_rbcount; /* reserved blocks count */
u_int32_t e2fs_fbcount; /* free blocks count */
u_int32_t e2fs_ficount; /* free inodes count */
u_int32_t e2fs_first_dblock; /* first data block */
u_int32_t e2fs_log_bsize; /* block size = 1024*(2^e2fs_log_bsize) */
u_int32_t e2fs_log_fsize; /* fragment size log2 */
```
2024-08-16 20:50:09 +02:00
This is the beginning of the actual super block. We can break down the information line by line. inodes are data structures to hold the necessary information about a file, except the file name or the actual data itself; on some filesystems, the first few blocks after the inode are reserved for the file, and then the next few blocks contain pointers to other blocks on the disk, and the next few after that contain pointers to pointers to other blocks, etc. This allows for a much larger maximum file size. Momentarily, we will see if the case is such with ext4 as well.
2024-08-16 20:50:09 +02:00
The amount of reserved blocks is the amount of blocks the filesystem needs for journaling, etc. This would depend on what version of the ext filesystem it is, and so forth.
2024-08-16 20:50:09 +02:00
The block size is stored as powers of 2, so the `e2fs_log_xsize` hold just that.
2024-08-16 20:50:09 +02:00
```c
u_int32_t e2fs_bpg; /* blocks per group */
u_int32_t e2fs_fpg; /* frags per group */
u_int32_t e2fs_ipg; /* inodes per group */
u_int32_t e2fs_mtime; /* mount time */
u_int32_t e2fs_wtime; /* write time */
u_int16_t e2fs_mnt_count; /* mount count */
u_int16_t e2fs_max_mnt_count; /* max mount count */
u_int16_t e2fs_magic; /* magic number */
u_int16_t e2fs_state; /* file system state */
u_int16_t e2fs_beh; /* behavior on errors */
```
These lines are quite self explanatory; the only thing worth pointing out is that a group is an amount of bytes that can be read easily by the disk- exact amounts depend on whether its solid state, spinning disk, how old it is, etc. The mount and write time are stored as unix timestamps, and are held to check if the current timestamp match the recorded time, checking if the filesystem was modified externally. This is also useful in journal replay, so we know which transactions to start from. The last 3 are flags/values to differentiate filesystems.
```c
u_int16_t e2fs_minrev; /* minor revision level */
```
2024-08-16 20:50:09 +02:00
The minimum revision level that the filesystem needs- for example, if it's ext4, then the `e2fs_minrev` field would guarantee that it needs at least the features of ext4 (or if its 32-bit, ext3) to be read and mounted
2024-08-16 20:50:09 +02:00
```c
u_int32_t e2fs_lastfsck; /* time of last fsck */
u_int32_t e2fs_fsckintv; /* max time between fscks */
u_int32_t e2fs_creator; /* creator OS */
u_int32_t e2fs_rev; /* revision level */
```
This is to ensure fsck(3) works properly.
> fsck filesystem consistency check and interactive repair
As my manual page likes to call it. This is to replay journaling, or whatever else the filesystem requires to be cleaned up.
```c
u_int16_t e2fs_ruid; /* default uid for reserved blocks */
u_int16_t e2fs_rgid; /* default gid for reserved blocks */
```
The default user and group ID that processes need to have to access reserved blocks. As there isn't much information available online, let's just keep going for now.
```c
/* EXT2_DYNAMIC_REV superblocks */
u_int32_t e2fs_first_ino; /* first non-reserved inode */
u_int16_t e2fs_inode_size; /* size of inode structure */
u_int16_t e2fs_block_group_nr; /* block grp number of this sblk*/
u_int32_t e2fs_features_compat; /* compatible feature set */
u_int32_t e2fs_features_incompat; /* incompatible feature set */
u_int32_t e2fs_features_rocompat; /* RO-compatible feature set */
u_int8_t e2fs_uuid[16]; /* 128-bit uuid for volume */
char e2fs_vname[16]; /* volume name */
char e2fs_fsmnt[64]; /* name mounted on */
u_int32_t e2fs_algo; /* For compression */
u_int8_t e2fs_prealloc; /* # of blocks to preallocate */
u_int8_t e2fs_dir_prealloc; /* # of blocks to preallocate for dir */
u_int16_t e2fs_reserved_ngdb; /* # of reserved gd blocks for resize */
```
These look like what are meant to be the other part of the superblock if the revision number is 3 or higher. Probably added after ext2 support was initially added. The feature set are flags to be defined later. The UUID, name, where it's mounted, algorithm or whatever are informational flags to remain consistent, and this likely means ext2 didn't have these features. `e2fs_prealloc` must be for each file. `gd` likely stands for group descriptor. With that in mind, let's keep moving down the superblock.
2024-08-16 21:53:40 +02:00
```c
/* Ext3 JBD2 journaling. */
u_int8_t e2fs_journal_uuid[16];
u_int32_t e2fs_journal_ino;
u_int32_t e2fs_journal_dev;
u_int32_t e2fs_last_orphan; /* start of list of inodes to delete */
u_int32_t e2fs_hash_seed[4]; /* htree hash seed */
```
So it turns out a journal has an identifier, an inode number, also a unique device id? a list of inodes which for some reason is `u_int32_t` and a hash seed which is an array 💀
`e2fs_last_orphan` is the block number of the list of inodes to delete. The hash seed is an array of 4 `u_int32_t`, which is defined to have a primary and secondary hashing function for improved collision resistance.
```c
u_int8_t e2fs_def_hash_version;
u_int8_t e2fs_journal_backup_type;
u_int16_t e2fs_gdesc_size;
u_int32_t e2fs_default_mount_opts;
u_int32_t e2fs_first_meta_bg;
u_int32_t e2fs_mkfs_time;
```
As stated before, information is scare 😅 For now, we will do our best by trying to pick apart what we see and fully understand what's going on later in the code hopefully, or once we get in touch with other developers.
The first value seems to be the default hash version, so some set of flags that indicate what hash function's versions are used. Maybe if the `e2fs_hash_seed` is sha, this would specify between 256, 512, and so forth.
The second value is the type of backup journaling. It must again be a set of flags determining what items are recorded to be replayed.
The group descriptor must be information specific to each group, so its size must imply that it's held in a fixed position on disk and has a size dependent on the amount of metadata we want to hold.
The `e2fs_first_meta_bg` appears to be the first metadata block group in case the simplifications were cryptic. It contains other important information not specified in the superblock already.
The last value holds when the filesystem was created--when mkfs was last run-- which is useful metadata information but doesn't appear to be directly useful, unless we are checking data integrity.
```c
u_int32_t e2fs_journal_backup[17];
```
I could not find any information on this field so far. We want to allocate 17 double words for information the journal backs up? Or is this where the logs are stored (but wouldn't that be too small?) we don't know yet.
```c
u_int32_t e2fs_bcount_hi; /* high bits of blocks count */
u_int32_t e2fs_rbcount_hi; /* high bits of reserved blocks count */
u_int32_t e2fs_fbcount_hi; /* high bits of free blocks count */
u_int16_t e2fs_min_extra_isize; /* all inodes have some bytes */
u_int16_t e2fs_want_extra_isize;/* inodes must reserve some bytes */
```
While these have comments, they probably make no sense to the non kernel developer 🤣
At this point I think it is safe to say this is beyond the journaling part of the superblock, even though there is no padding or comment to indicate otherwise. The high bits mean the more significant bits, so it's easier to access, even though these are `u_int32_t` like the regular counts 🤔 must be more useful in 64-bit mode, I guess.
The last two fields are quiet confusing, comment wise. However all they mean is that there is a minimum amount of bytes an inode should allocate, and an ideal number.
```c
u_int32_t e2fs_flags; /* miscellaneous flags */
u_int16_t e2fs_raid_stride; /* RAID stride */
u_int16_t e2fs_mmpintv; /* seconds to wait in MMP checking */
u_int64_t e2fs_mmpblk; /* block for multi-mount protection */
u_int32_t e2fs_raid_stripe_wid; /* blocks on data disks (N * stride) */
```
These are other helpful utilities. RAID means Redundant Array of Independent Disks. The number of blocks on each disk is the `raid_stride` times the `raid_stripe_width`: in simple words, the amount of duplicate copies of the disk you have.
MMP is multi-mount protection: a block holds advanced information for that, and there are a few seconds you need to wait. The last item says how many blocks there are on each data disk to be multiplied with `e2fs_raid_stride`.
```c
u_int8_t e2fs_log_gpf; /* FLEX_BG group size */
u_int8_t e2fs_chksum_type; /* metadata checksum algorithm used */
u_int8_t e2fs_encrypt; /* versioning level for encryption */
u_int8_t e2fs_reserved_pad;
```
The group seize is for Flexible Block Groups. It is the number of blocks in each group in log powers again, for easy access. The `e2fs_chksum_type` is the algorithm used to determine the checksum of the entire disk. Not quite sure what exact role the `e2fs_encrypt` value holds, but it should have something to do with luks/encrypted files.
The reserved padding is, well reserved padding. Maybe it's something we have to implement 👀
```c
u_int64_t e2fs_kbytes_written; /* number of lifetime kilobytes */
u_int32_t e2fs_snapinum; /* inode number of active snapshot */
u_int32_t e2fs_snapid; /* sequential ID of active snapshot */
u_int64_t e2fs_snaprbcount; /* rsvd blocks for active snapshot */
u_int32_t e2fs_snaplist; /* inode number for on-disk snapshot */
u_int32_t e2fs_errcount; /* number of file system errors */
u_int32_t e2fs_first_errtime; /* first time an error happened */
u_int32_t e2fs_first_errino; /* inode involved in first error */
u_int64_t e2fs_first_errblk; /* block involved of first error */
u_int8_t e2fs_first_errfunc[32];/* function where error happened */
u_int32_t e2fs_first_errline; /* line number where error happened */
u_int32_t e2fs_last_errtime; /* most recent time of an error */
u_int32_t e2fs_last_errino; /* inode involved in last error */
u_int32_t e2fs_last_errline; /* line number where error happened */
u_int64_t e2fs_last_errblk; /* block involved of last error */
u_int8_t e2fs_last_errfunc[32];/* function where error happened */
```
I'm not quite sure why this would be stored, other than easy access to the journal, but I guess it is useful.
```c
u_int8_t e2fs_mount_opts[64];
u_int32_t e2fs_usrquota_inum; /* inode for tracking user quota */
u_int32_t e2fs_grpquota_inum; /* inode for tracking group quota */
u_int32_t e2fs_overhead_clusters;/* overhead blocks/clusters */
u_int32_t e2fs_backup_bgs[2]; /* groups with sparse_super2 SBs */
u_int8_t e2fs_encrypt_algos[4];/* encryption algorithms in use */
u_int8_t e2fs_encrypt_pw_salt[16];/* salt used for string2key */
u_int32_t e2fs_lpf_ino; /* location of the lost+found inode */
u_int32_t e2fs_proj_quota_inum; /* inode for tracking project quota */
```
Apart from the overhead clusters, they all make sense. Some inodes hold information; hence the user and groups have certain quotas on how much disk space they can use. The overhead blocks are probably for metadata again; this is a pointer to the block which we had previously looked at with size. Each group has it's own layout table, hence also its own "super-block"; `sparse_super2` must be a standard for the group's super block. Everything else can be looked up online easily, except for the project quota, which is a "set of users". projects(1) should explain it pretty well.
```c
u_int32_t e2fs_chksum_seed; /* checksum seed */
u_int32_t e2fs_reserved[98]; /* padding to the end of the block */
u_int32_t e2fs_sbchksum; /* superblock checksum */
};
```
All of which is self-explained. With that, we conclude our thorough analysis of the super block, and we get to the macro declarations which will define the rest of the information on this page.
```c
/* in-memory data for ext2fs */
struct m_ext2fs {
struct ext2fs e2fs;
u_char e2fs_fsmnt[MAXMNTLEN]; /* name mounted on */
int8_t e2fs_ronly; /* mounted read-only flag */
int8_t e2fs_fmod; /* super block modified flag */
int32_t e2fs_fsize; /* fragment size */
int32_t e2fs_bsize; /* block size */
int32_t e2fs_bshift; /* ``lblkno'' calc of logical blkno */
int32_t e2fs_bmask; /* ``blkoff'' calc of blk offsets */
int64_t e2fs_qbmask; /* ~fs_bmask - for use with quad size */
int32_t e2fs_fsbtodb; /* fsbtodb and dbtofsb shift constant */
int32_t e2fs_ncg; /* number of cylinder groups */
int32_t e2fs_ngdb; /* number of group descriptor block */
int32_t e2fs_ipb; /* number of inodes per block */
int32_t e2fs_itpg; /* number of inode table per group */
off_t e2fs_maxfilesize; /* depends on LARGE/HUGE flags */
struct ext2_gd *e2fs_gd; /* group descriptors */
};
```
Oop, here comes the memory table to crush my dreams 😭
Hopefully reading through it explains it, because it appears to be information only the system can use (like, why would you put if the system was mounted read only on the disk 💀) and some redundant values cached again for efficiency.
```c
static inline int
e2fs_overflow(struct m_ext2fs *fs, off_t lower, off_t value)
{
return (value < lower || value > fs->e2fs_maxfilesize);
}
```
Here we have a function checking for overflows, but for what? It checks if our current file offset goes beyond the end of the file 🤡 For obvious reasons, this is useful.
```c
#define E2FS_MAGIC 0xef53 /* the ext2fs magic number */
#define E2FS_REV0 0 /* revision levels */
#define E2FS_REV1 1 /* revision levels */
```
Here we have some ways we can identify filesystem: Contrary to prior belief, ext4 is not reivsion 4; there are only two major revisions here, probably between those who made the gap to ext3/4 and those on ext2.
```c
/* compatible/incompatible features */
#define EXT2F_COMPAT_PREALLOC 0x0001
#define EXT2F_COMPAT_IMAGIC_INODES 0x0002
#define EXT2F_COMPAT_HAS_JOURNAL 0x0004
#define EXT2F_COMPAT_EXT_ATTR 0x0008
#define EXT2F_COMPAT_RESIZE 0x0010
#define EXT2F_COMPAT_DIR_INDEX 0x0020
#define EXT2F_COMPAT_SPARSE_SUPER2 0x0200
```
Now we are getting to the features we need to implement ourselves and what is already out there for us. They can allocate, they can manage inodes magic numbers, they have a journal (so it is ext3 confirmed), you can resize, read disks, and oh look, you can also use the `sparse_super2` technology when you need to read metadata groups!
```c
#define EXT2F_ROCOMPAT_SPARSE_SUPER 0x0001
#define EXT2F_ROCOMPAT_LARGE_FILE 0x0002
#define EXT2F_ROCOMPAT_BTREE_DIR 0x0004
#define EXT2F_ROCOMPAT_HUGE_FILE 0x0008
#define EXT2F_ROCOMPAT_GDT_CSUM 0x0010
#define EXT2F_ROCOMPAT_DIR_NLINK 0x0020
#define EXT2F_ROCOMPAT_EXTRA_ISIZE 0x0040
#define EXT2F_ROCOMPAT_QUOTA 0x0100
#define EXT2F_ROCOMPAT_BIGALLOC 0x0200
#define EXT2F_ROCOMPAT_METADATA_CKSUM 0x0400
#define EXT2F_ROCOMPAT_READONLY 0x1000
#define EXT2F_ROCOMPAT_PROJECT 0x2000
```
Here are some of the features we need to implement in write mode. Looks pretty self explanatory.
```c
#define EXT2F_INCOMPAT_COMP 0x0001
#define EXT2F_INCOMPAT_FTYPE 0x0002
#define EXT2F_INCOMPAT_RECOVER 0x0004
#define EXT2F_INCOMPAT_JOURNAL_DEV 0x0008
#define EXT2F_INCOMPAT_META_BG 0x0010
#define EXT2F_INCOMPAT_EXTENTS 0x0040
#define EXT2F_INCOMPAT_64BIT 0x0080
#define EXT2F_INCOMPAT_MMP 0x0100
#define EXT2F_INCOMPAT_FLEX_BG 0x0200
#define EXT2F_INCOMPAT_EA_INODE 0x0400
#define EXT2F_INCOMPAT_DIRDATA 0x1000
#define EXT2F_INCOMPAT_CSUM_SEED 0x2000
#define EXT2F_INCOMPAT_LARGEDIR 0x4000
#define EXT2F_INCOMPAT_INLINE_DATA 0x8000
#define EXT2F_INCOMPAT_ENCRYPT 0x10000
```
And here are the features we need to implement from scratch!
On attempting to mount an ext4 filesystem read-only on OpenBSD, we get that we don't support `EXT2F_INCOMPAT_64BIT` and `EXT2F_INCOMPAT_CSUM_SEED`. So for our "Minimum Viable Product" we are to focus on that first. 🤓
Between lines 277 and 313, we have an array of a struct identifying each feature mask to it's string form. That's completely redundant for us; let's skip over it.
```c
/* features supported in this implementation */
#define EXT2F_COMPAT_SUPP 0x0000
#define EXT2F_ROCOMPAT_SUPP (EXT2F_ROCOMPAT_SPARSE_SUPER | \
EXT2F_ROCOMPAT_LARGE_FILE)
#define EXT2F_INCOMPAT_SUPP (EXT2F_INCOMPAT_FTYPE)
#define EXT4F_RO_INCOMPAT_SUPP (EXT2F_INCOMPAT_EXTENTS | \
EXT2F_INCOMPAT_FLEX_BG | \
EXT2F_INCOMPAT_META_BG | \
EXT2F_INCOMPAT_RECOVER)
```
Looks like not all of the features that were supported above are supported? It's hard to say. That might have been some sort of roadmap internal OpenBSD developers wanted to follow. Anyhow, considering that OpenBSD was pretty descriptive with what features we need to implement, we can get to read-only mode soon, and we can have plenty of kernel panics and filesystem corruptions to get to the point we need to get to!
```c
#define E2FS_BEH_CONTINUE 1 /* continue operation */
#define E2FS_BEH_READONLY 2 /* remount fs read only */
#define E2FS_BEH_PANIC 3 /* cause panic */
#define E2FS_BEH_DEFAULT E2FS_BEH_CONTINUE
```
The behavior on errors, now redefined.
```c
/*
* OS identification
*/
#define E2FS_OS_LINUX 0
#define E2FS_OS_HURD 1
#define E2FS_OS_MASIX 2
```
Hurd as in, GNU Hurd. 🤮
No one is using that anymore. I'm pretty sure all operating systems just use the Linux flag at this point.
```c
/*
* Filesystem clean flags
*/
#define E2FS_ISCLEAN 0x01
#define E2FS_ERRORS 0x02
```
This is the "dirty bit" you'll sometimes hear `fsck` talk about.
2024-08-16 23:47:55 +02:00
```c
/* ext2 file system block group descriptor */
struct ext2_gd {
u_int32_t ext2bgd_b_bitmap; /* blocks bitmap block */
u_int32_t ext2bgd_i_bitmap; /* inodes bitmap block */
u_int32_t ext2bgd_i_tables; /* inodes table block */
u_int16_t ext2bgd_nbfree; /* number of free blocks */
u_int16_t ext2bgd_nifree; /* number of free inodes */
u_int16_t ext2bgd_ndirs; /* number of directories */
u_int16_t reserved;
u_int32_t reserved2[3];
};
```
This must be the `sparse_super2` block group descriptor. Or, maybe not. We'll see. A bitmap block is quite literally a map of bits (bytes) and which blocks in the group manage how it's allocated. A table block is supposed to resemble a tree of inodes in the group.
```c
/*
* If the EXT2F_ROCOMPAT_SPARSE_SUPER flag is set, the cylinder group has a
* copy of the super and cylinder group descriptors blocks only if it's
* a power of 3, 5 or 7
*/
static __inline__ int cg_has_sb(int) __attribute__((__unused__));
static __inline int
cg_has_sb(int i)
{
int a3 ,a5 , a7;
if (i == 0 || i == 1)
return 1;
for (a3 = 3, a5 = 5, a7 = 7;
a3 <= i || a5 <= i || a7 <= i;
a3 *= 3, a5 *= 5, a7 *= 7)
if (i == a3 || i == a5 || i == a7)
return 1;
return 0;
}
```
For what reason..? I guess we will never know.
Lines 381-397 are just redundant utilities that we don't need to care about too much. They do what they do, so you should read over them, but it's not likely we will touch them anytime throughout the project's development.
```c
/*
* Turn file system block numbers into disk block addresses.
* This maps file system blocks to device size blocks.
*/
#define fsbtodb(fs, b) ((b) << (fs)->e2fs_fsbtodb)
#define dbtofsb(fs, b) ((b) >> (fs)->e2fs_fsbtodb)
```
This shows the purpose of the `fsbtodb` and `dbtofsb` macros. They are used to map the block number of the disk to the block number of the filesystem. But it's worth asking where these values come from; it's not stored in the super block!
Then we have more redundant and fancy utilities to abstract complex math behind converting inodes to disk addresses, block numbers, etc.
At the end of the file, we have one last macro which seems peculiar:
```c
/*
* Number of indirects in a file system block.
*/
#define NINDIR(fs) ((fs)->e2fs_bsize / sizeof(u_int32_t))
```
The size of a block over a size of a disk pointer... huh.
It's actually not that complicated.
2024-08-16 23:47:55 +02:00
Similar to the idea I mentioned above, we first have direct pointers to the disk (12), (1) single indirect, (1) double indirect, (1) triple indirect.[[1]](https://opensource.com/article/17/5/introduction-ext4-filesystem)
2024-08-16 23:47:55 +02:00
With that, I guess we've concluded our first deep dive into the ext4 filesystem.