13 Commits
Author SHA1 Message Date
Ernesto A. Fernández a10279ce66 Plug buffer head leak on submit for the 7.2 kernel
Starting with the 7.2 kernel, buffer heads no longer lose a reference
during the submit-wait procedure. I didn't notice that change, so now I
would expect all buffer heads to be leaking. The fact that I still can't
tell the difference makes me believe, like I mentioned before, that
somehow the kernel is still disposing of the buffer heads. Either way,
fix this.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-14 21:35:59 -03:00
Ernesto A. Fernández 7711c4704e Plug buffer head leak for checkpoint mapping block
This is the last of the three leaks discovered by Valgrind in the
userland port. During the loop that writes ephemeral objects to disk,
checkpoint mapping blocks are created and kept around for multiple
iterations until they are full. At that point, the cpm buffer head gets
released and we get a new one. All that looks correct, but no release
happens after the final loop. Fix it.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-14 19:41:33 -03:00
Ernesto A. Fernández 9619c53686 Plug leak of ip bitmap buffer head
This is the second bug discovered by valgrind in the userland tool. The
buffer head for the ip bitmap gets nulled after joining the transaction,
but we still hold a reference to it (the transactions takes a new one).
Fix it.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-14 19:21:41 -03:00
Ernesto A. Fernández f5b71842e6 Plug checkpoint buffer head leak during mount
Work on the userland port with valgrind has reavealed a number of
longstanding leaks in the kernel driver. For some reason I always
assumed that kmemleak would eventually notice issues with buffer heads,
but it clearly doesn't. This bug in particular must have been around
since the very beginning: all buffer heads for checkpoint descriptor
blocks get leaked during mount.

Since nobody noticed the problem so far, I'm guessing that the kernel
somehow disposes of abandoned buffer heads. Perhaps it has something to
do with the old issue from patch 517dd221df ("Forcefully free buffer
heads on transaction commit")? Either way, fix the leak and get the code
to match the userland port.

One thing to note: before pushing this patch I tried asking Github
Copilot to look for leaks in the super.c file and it spotted the problem
immediately. It seems to have become far more useful since the last time
I tried it, so once I'm done fixing the current crop of leaks I should
try and see what it can find.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-14 18:48:48 -03:00
Ernesto A. Fernández 83205caaad Fix build for RHEL 9.8
Kexianliang reports that the build is broken once more for RHEL:

  https://github.com/linux-apfs/linux-apfs-rw/issues/123

Fix it.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-12 17:16:00 -03:00
Ernesto A. Fernández 8a37600293 0.3.21
This release adds support for the upcoming 7.2 kernel version.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-08-12 15:42:53 -03:00
Ernesto A. Fernández f0e88ca368 Switch to bh_submit() for kernel 7.2
The build is broken for the 7.2 release candidate. The reason is that
submit_bh() is gone now, replaced with a new bh_submit() that also takes
the I/O end handler as an argument. Adjust apfs_submit_bh() to these
changes.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-07-14 19:38:12 -03:00
Ernesto A. Fernández 96d5614d04 Put macos macro guards around uintmax_t typedef
I've been trying to build the driver for userland lately. Most of the
code can be left as it is as long as I spoof the kernel headers and
everything else that the kernel provides. The one exception is my
uintmax_t typedef, because macos is very determined to provide its own
definition for that type. I guess that makes sense, given that the lzfse
code came from Apple in the first place.

Anyway, none of this is a problem for the driver, but since I want to
keep both codebases identical, put the same macro guards in here.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-06-24 17:41:11 -03:00
Ernesto A. Fernández b2418aaaa7 Clarify the modprobe situation in the README
I notice a lot of traffic to the following issue:

  https://github.com/linux-apfs/linux-apfs-rw/issues/96

Recent distro kernels seem to ship with a built-in libcrc32c, so the
modprobe step described in the README is not necessary and will fail. I
assumed it would be clear to the users that they can just keep going
when that happens, but some of them are being extra careful, which is
probably the right attitude to have when dealing with a filesystem.

Rephrase the README to make it clear that the modprobe step is not
usually needed anymore.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-05-27 16:58:08 -03:00
Ernesto A. Fernández 628b6810e4 0.3.20
This quick release adds support for the fsconfig() syscall, otherwise
newer versions of the mount tool won't accept any options.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-04-25 16:28:02 -03:00
Ernesto A. Fernández 7aca589f9e Use ->parse_param(), not ->parse_monolithic()
SergDL reports that mounts seem broken for the 7.0 kernel:

  https://github.com/linux-apfs/linux-apfs-rw/issues/119

I never encountered this problem during testing, probably because my
version of the mount tool is older. It seems that mount options are now
set one by one using the fsconfig() syscall, and that's not possible if
we parse them all at once with ->parse_monolithic().

Implement ->parse_param() instead, at least for the latest kernel. I do
wonder if this issue won't pop up with older kernels though, since the
fsconfig() syscall has been around since 5.2. I guess I'll wait to see
if somebody complains about it.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-04-24 17:10:07 -03:00
Ernesto A. Fernández 36df7e0b35 Set default mount options as soon as possible
Instead of setting the default mount options right before parsing the
actual options, set them on the superblock as soon as it gets allocated.
This will allow the parsing code to be called more than once by the new
mount api.

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-04-24 14:56:03 -03:00
Ernesto A. Fernández ae381fbc3d Move default mount options into separate function
The new mount api forces me to make one function call for each option to
parse. This change means that the default options will need to get set
elsewhere, otherwise each new call would reset to default.

As a first step, move that code from parse_options() into a new
apfs_set_default_opts().

Signed-off-by: Ernesto A. Fernández <ernesto@corellium.com>
2026-04-24 14:48:27 -03:00
9 changed files with 149 additions and 40 deletions

No files matched your search

+7 -2
View File
@@ -57,10 +57,15 @@ Now you can just cd to the linux-apfs-rw directory and run::
make
The resulting module is the apfs.ko file. Before you can use it you must insert
it into the kernel, as well as its dependencies. Again as root::
it into the kernel. Again as root::
insmod apfs.ko
If you get an "unknown symbol in module" error, run::
modprobe libcrc32c
insmod apfs.ko
and then try the insmod command again.
Mount
=====
+27 -4
View File
@@ -44,10 +44,33 @@ static inline bool sb_rdonly(const struct super_block *sb) { return sb->s_flags
#endif
/* Compatibility wrapper around submit_bh() */
#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 0, 0) || RHEL_VERSION_GE(9, 2)
#define apfs_submit_bh(op, op_flags, bh) submit_bh(op | op_flags, bh)
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 2, 0)
#define apfs_submit_bh(op, op_flags, bh) \
do { \
put_bh(bh); \
if (op == REQ_OP_READ) \
bh_submit(bh, op | op_flags, bh_end_read); \
else \
bh_submit(bh, op | op_flags, bh_end_write); \
} while (0)
#elif LINUX_VERSION_CODE >= KERNEL_VERSION(6, 0, 0) || RHEL_VERSION_GE(9, 2)
#define apfs_submit_bh(op, op_flags, bh) \
do { \
if (op == REQ_OP_READ) \
bh->b_end_io = end_buffer_read_sync; \
else \
bh->b_end_io = end_buffer_write_sync; \
submit_bh(op | op_flags, bh); \
} while (0)
#else
#define apfs_submit_bh(op, op_flags, bh) submit_bh(op, op_flags, bh)
#define apfs_submit_bh(op, op_flags, bh) \
do { \
if (op == REQ_OP_READ) \
bh->b_end_io = end_buffer_read_sync; \
else \
bh->b_end_io = end_buffer_write_sync; \
submit_bh(op, op_flags, bh); \
} while (0)
#endif
/*
@@ -1324,7 +1347,7 @@ __apfs_getblk(struct super_block *sb, sector_t block)
block -= nxi->nx_tier2_bno;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 7, 0)
#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 7, 0) && !RHEL_VERSION_GE(9, 8)
return __getblk_gfp(info->blki_bdev, block, sb->s_blocksize, __GFP_MOVABLE);
#else
return bdev_getblk(info->blki_bdev, block, sb->s_blocksize, __GFP_MOVABLE);
+1 -1
View File
@@ -1,5 +1,5 @@
PACKAGE_NAME="linux-apfs-rw"
PACKAGE_VERSION="0.3.19"
PACKAGE_VERSION="0.3.21"
BUILT_MODULE_NAME[0]="apfs"
DEST_MODULE_LOCATION[0]="/extra"
-2
View File
@@ -2298,7 +2298,6 @@ int apfs_nonsparse_dstream_read(struct apfs_dstream_info *dstream, void *buf, si
if (!buffer_uptodate(bh)) {
get_bh(bh);
lock_buffer(bh);
bh->b_end_io = end_buffer_read_sync;
apfs_submit_bh(REQ_OP_READ, 0, bh);
}
}
@@ -2365,7 +2364,6 @@ void apfs_nonsparse_dstream_preread(struct apfs_dstream_info *dstream)
if (!buffer_uptodate(bh)) {
get_bh(bh);
lock_buffer(bh);
bh->b_end_io = end_buffer_read_sync;
apfs_submit_bh(REQ_OP_READ, 0, bh);
}
brelse(bh);
-1
View File
@@ -556,7 +556,6 @@ int __apfs_write_begin(struct file *file, struct address_space *mapping, loff_t
if (buffer_mapped(bh) && !buffer_uptodate(bh)) {
get_bh(bh);
lock_buffer(bh);
bh->b_end_io = end_buffer_read_sync;
apfs_submit_bh(REQ_OP_READ, 0, bh);
wait_on_buffer(bh);
if (!buffer_uptodate(bh)) {
+2
View File
@@ -70,7 +70,9 @@ typedef int64_t lzfse_offset;
typedef int32_t lzfse_offset;
#endif
#if !defined(__APPLE__) || !defined(__MACH__)
typedef uint64_t uintmax_t;
#endif
/*! @abstract Decoder state object for lzfse compressed blocks. */
typedef struct {
+1
View File
@@ -308,6 +308,7 @@ static int apfs_write_single_ip_bitmap(struct super_block *sb, char *bitmap, u32
err = apfs_transaction_join(sb, bh);
if (err)
goto fail;
put_bh(bh);
bh = NULL;
spaceman->sm_ip_bmaps[idx].dirty = false;
+108 -29
View File
@@ -17,6 +17,7 @@
#include "version.h"
#if LINUX_VERSION_CODE >= KERNEL_VERSION(7, 0, 0)
#include <linux/fs_context.h>
#include <linux/fs_parser.h>
#endif
#define APFS_MODULE_ID_STRING "linux-apfs by eafer (" GIT_COMMIT ")"
@@ -342,8 +343,8 @@ static int apfs_read_main_super(struct super_block *sb)
xid = le64_to_cpu(msb_raw->nx_o.o_xid);
for (i = 0; i < desc_blocks; ++i) {
struct apfs_nx_superblock *desc_raw;
bool ignore;
brelse(desc_bh);
desc_bh = apfs_sb_bread(sb, desc_base + i);
if (!desc_bh) {
apfs_err(sb, "unable to read checkpoint descriptor");
@@ -351,18 +352,24 @@ static int apfs_read_main_super(struct super_block *sb)
}
desc_raw = (struct apfs_nx_superblock *)desc_bh->b_data;
ignore = false;
if (le32_to_cpu(desc_raw->nx_magic) != APFS_NX_MAGIC)
continue; /* Not a superblock */
ignore = true; /* Not a superblock */
if (le64_to_cpu(desc_raw->nx_o.o_xid) <= xid)
continue; /* Old */
ignore = true; /* Old */
if (!apfs_obj_verify_csum(sb, desc_bh))
continue; /* Corrupted */
ignore = true; /* Corrupted */
xid = le64_to_cpu(desc_raw->nx_o.o_xid);
msb_raw = desc_raw;
bno = desc_base + i;
brelse(bh);
bh = desc_bh;
if (ignore) {
brelse(desc_bh);
} else {
xid = le64_to_cpu(desc_raw->nx_o.o_xid);
msb_raw = desc_raw;
bno = desc_base + i;
brelse(bh);
bh = desc_bh;
}
desc_raw = NULL;
desc_bh = NULL;
}
@@ -1203,6 +1210,7 @@ enum {
Opt_readwrite, Opt_cknodes, Opt_uid, Opt_gid, Opt_vol, Opt_snap, Opt_tier2, Opt_err,
};
#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0)
static const match_table_t tokens = {
{Opt_readwrite, "readwrite"},
{Opt_cknodes, "cknodes"},
@@ -1213,6 +1221,18 @@ static const match_table_t tokens = {
{Opt_tier2, "tier2=%s"},
{Opt_err, NULL}
};
#else
static const struct fs_parameter_spec apfs_param_spec[] = {
fsparam_flag ("readwrite", Opt_readwrite),
fsparam_flag ("cknodes", Opt_cknodes),
fsparam_uid ("uid", Opt_uid),
fsparam_gid ("gid", Opt_gid),
fsparam_u32 ("vol", Opt_vol),
fsparam_string ("snap", Opt_snap),
fsparam_string ("tier2", Opt_tier2),
{}
};
#endif
/**
* apfs_set_nx_flags - Set the mount flags for the container, if allowed
@@ -1252,6 +1272,26 @@ static void parse_options_set_flags(struct super_block *sb, struct apfs_sb_info
}
}
/**
* apfs_set_default_opts - Set the default mount options in the superblock
* @sbi: in-memory superblock info
*/
static void apfs_set_default_opts(struct apfs_sb_info *sbi)
{
sbi->s_vol_nr = 0;
sbi->s_snap_name = NULL;
sbi->s_tier2_path = NULL;
sbi->s_mount_opt = 0;
#ifdef CONFIG_APFS_RW_ALWAYS
/* Still risky, but some packagers want writable mounts by default */
sbi->s_mount_opt |= APFS_READWRITE;
#endif
sbi->s_uid = INVALID_UID;
sbi->s_gid = INVALID_GID;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0)
/*
* Many of the parse_options() functions in other file systems return 0
* on error. This one returns an error code, and 0 on success.
@@ -1266,20 +1306,6 @@ static int parse_options(struct apfs_sb_info *sbi, char *options)
int option;
int err = 0;
/* Set default values before parsing */
sbi->s_vol_nr = 0;
sbi->s_snap_name = NULL;
sbi->s_tier2_path = NULL;
sbi->s_mount_opt = 0;
#ifdef CONFIG_APFS_RW_ALWAYS
/* Still risky, but some packagers want writable mounts by default */
sbi->s_mount_opt |= APFS_READWRITE;
#endif
sbi->s_uid = INVALID_UID;
sbi->s_gid = INVALID_GID;
if (!options)
return 0;
@@ -1351,6 +1377,62 @@ static int parse_options(struct apfs_sb_info *sbi, char *options)
return 0;
}
#else /* LINUX_VERSION_CODE < KERNEL_VERSION(7, 0, 0) */
static int apfs_parse_param(struct fs_context *fc, struct fs_parameter *param)
{
struct apfs_sb_info *sbi = fc->s_fs_info;
int opt;
struct fs_parse_result result;
opt = fs_parse(fc, apfs_param_spec, param, &result);
if (opt < 0)
return opt;
switch (opt) {
case Opt_readwrite:
/*
* Write support is not safe yet, so keep it disabled
* unless the user requests it explicitly.
*/
sbi->s_mount_opt |= APFS_READWRITE;
break;
case Opt_cknodes:
/*
* Right now, node checksums are too costly to enable
* by default. TODO: try to improve this.
*/
sbi->s_mount_opt |= APFS_CHECK_NODES;
break;
case Opt_uid:
sbi->s_uid = result.uid;
break;
case Opt_gid:
sbi->s_gid = result.gid;
break;
case Opt_vol:
sbi->s_vol_nr = result.uint_32;
break;
case Opt_snap:
kfree(sbi->s_snap_name);
sbi->s_snap_name = kstrdup(param->string, GFP_KERNEL);
if (!sbi->s_snap_name)
return -ENOMEM;
break;
case Opt_tier2:
kfree(sbi->s_tier2_path);
sbi->s_tier2_path = kstrdup(param->string, GFP_KERNEL);
if (!sbi->s_tier2_path)
return -ENOMEM;
break;
default:
return -EINVAL;
}
return 0;
}
#endif
/**
* apfs_check_nx_features - Check for unsupported features in the container
* @sb: superblock structure
@@ -1924,6 +2006,7 @@ static struct dentry *apfs_mount(struct file_system_type *fs_type, int flags,
sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
if (!sbi)
return ERR_PTR(-ENOMEM);
apfs_set_default_opts(sbi);
/* Set up the fields that sget() will need to id the superblock */
error = parse_options(sbi, data);
if (error)
@@ -2123,16 +2206,11 @@ static void apfs_free_fc(struct fs_context *fc)
}
}
static int apfs_parse_monolithic(struct fs_context *fc, void *data)
{
return parse_options(fc->s_fs_info, data);
}
static const struct fs_context_operations apfs_context_ops = {
.get_tree = apfs_get_tree,
.reconfigure = apfs_reconfigure,
.free = apfs_free_fc,
.parse_monolithic = apfs_parse_monolithic,
.parse_param = apfs_parse_param,
};
static int apfs_init_fs_context(struct fs_context *fc)
@@ -2143,6 +2221,7 @@ static int apfs_init_fs_context(struct fs_context *fc)
sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
if (!sbi)
return -ENOMEM;
apfs_set_default_opts(sbi);
fc->s_fs_info = sbi;
fc->ops = &apfs_context_ops;
+3 -1
View File
@@ -640,6 +640,9 @@ static int apfs_write_ephemeral_objects(struct super_block *sb)
}
data_len += obj_blkcnt;
}
brelse(cpm_bh);
cpm = NULL;
cpm_bh = NULL;
/*
* The checkpoint superblock can't be set until the very end of the
@@ -721,7 +724,6 @@ static int apfs_transaction_commit_nx(struct super_block *sb)
clear_buffer_dirty(bh);
get_bh(bh);
lock_buffer(bh);
bh->b_end_io = end_buffer_write_sync;
apfs_submit_bh(REQ_OP_WRITE, REQ_SYNC, bh);
}
list_for_each_entry_safe(bhi, tmp, &nx_trans->t_buffers, list) {