[PATCH v2 1/3] hfsplus: fix timestamp wrapped issue
From: Viacheslav Dubeyko
Date: Fri Oct 09 2026 - 17:32:36 EST
The xfstests' test-case generic/258 fails to execute
correctly:
FSTYP -- hfsplus
PLATFORM -- Linux/x86_64 hfsplus-testing-0001 6.15.0-rc4+ #8 SMP PREEMPT_DYNAMIC Thu May 1 16:43:22 PDT 2025
MKFS_OPTIONS -- /dev/loop51
MOUNT_OPTIONS -- /dev/loop51 /mnt/scratch
generic/258 [failed, exit status 1]- output mismatch (see xfstests-dev/results//generic/258.out.bad)
The main reason of the issue is the logic:
cpu_to_be32(lower_32_bits(ut) + HFSPLUS_UTC_OFFSET)
At first, we take the lower 32 bits of the value and, then,
we add the time offset. However, if we have negative value
(timestamp before January 1, 1970), then we make completely
wrong calculation. The reverse conversion has the same issue
because it treats any on-disk timestamp under HFSPLUS_UTC_OFFSET
(January 1, 1970) as a time between 2040 and 2106.
This patch corrects the logic of __hfsp_mt2ut() and
__hfsp_ut2mt(). Now, the on-disk timestamp is interpreted
as unsigned number of seconds since January 1, 1904. As a result,
the supported time range is from January 1, 1904 till
February 6, 2040.
The HFS_UTC_OFFSET constant has been moved into
include/linux/hfs_common.h and HFSPLUS_UTC_OFFSET has been
removed. The HFS_MIN_TIMESTAMP_SECS and HFS_MAX_TIMESTAMP_SECS
constants have been introduced in include/linux/hfs_common.h.
The hfsplus_fill_super() logic defines sb->s_time_min,
sb->s_time_max, and sb->s_time_gran. It means that VFS clamps
the timestamps that are out of the supported range.
[1] https://github.com/hfs-linux-kernel/hfs-linux-kernel/issues/133
Signed-off-by: Viacheslav Dubeyko <slava@xxxxxxxxxxx>
cc: John Paul Adrian Glaubitz <glaubitz@xxxxxxxxxxxxxxxxxxx>
cc: Yangtao Li <frank.li@xxxxxxxx>
cc: linux-fsdevel@xxxxxxxxxxxxxxx
---
fs/hfs/hfs_fs.h | 13 -------------
fs/hfsplus/hfsplus_fs.h | 17 +++++------------
fs/hfsplus/super.c | 4 ++++
include/linux/hfs_common.h | 13 +++++++++++++
4 files changed, 22 insertions(+), 25 deletions(-)
diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h
index e250f87a5e33..659b0ba733d4 100644
--- a/fs/hfs/hfs_fs.h
+++ b/fs/hfs/hfs_fs.h
@@ -255,19 +255,6 @@ extern int hfs_mac2asc(struct super_block *sb,
/* super.c */
extern void hfs_mark_mdb_dirty(struct super_block *sb);
-/*
- * There are two time systems. Both are based on seconds since
- * a particular time/date.
- * Unix: signed little-endian since 00:00 GMT, Jan. 1, 1970
- * mac: unsigned big-endian since 00:00 GMT, Jan. 1, 1904
- *
- * HFS implementations are highly inconsistent, this one matches the
- * traditional behavior of 64-bit Linux, giving the most useful
- * time range between 1970 and 2106, by treating any on-disk timestamp
- * under HFS_UTC_OFFSET (Jan 1 1970) as a time between 2040 and 2106.
- */
-#define HFS_UTC_OFFSET 2082844800U
-
static inline time64_t __hfs_m_to_utime(__be32 mt)
{
time64_t ut = (u32)(be32_to_cpu(mt) - HFS_UTC_OFFSET);
diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h
index 916e6552e3f0..cd51afa22ece 100644
--- a/fs/hfsplus/hfsplus_fs.h
+++ b/fs/hfsplus/hfsplus_fs.h
@@ -525,26 +525,19 @@ bool is_hfs_thread_record_type(u16 type)
int hfsplus_brec_read_cat(struct hfs_find_data *fd, hfsplus_cat_entry *entry);
-/*
- * time helpers: convert between 1904-base and 1970-base timestamps
- *
- * HFS+ implementations are highly inconsistent, this one matches the
- * traditional behavior of 64-bit Linux, giving the most useful
- * time range between 1970 and 2106, by treating any on-disk timestamp
- * under HFSPLUS_UTC_OFFSET (Jan 1 1970) as a time between 2040 and 2106.
- */
-#define HFSPLUS_UTC_OFFSET 2082844800U
-
static inline time64_t __hfsp_mt2ut(__be32 mt)
{
- time64_t ut = (u32)(be32_to_cpu(mt) - HFSPLUS_UTC_OFFSET);
+ time64_t ut = (time64_t)be32_to_cpu(mt) - HFS_UTC_OFFSET;
return ut;
}
static inline __be32 __hfsp_ut2mt(time64_t ut)
{
- return cpu_to_be32(lower_32_bits(ut) + HFSPLUS_UTC_OFFSET);
+ ut += HFS_UTC_OFFSET;
+
+ return cpu_to_be32(lower_32_bits(ut));
+
}
static inline enum hfsplus_btree_mutex_classes
diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c
index ff7d6b3336a6..657dfa40bdf3 100644
--- a/fs/hfsplus/super.c
+++ b/fs/hfsplus/super.c
@@ -511,6 +511,10 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)
if (!sbi->rsrc_clump_blocks)
sbi->rsrc_clump_blocks = 1;
+ sb->s_time_gran = NSEC_PER_SEC;
+ sb->s_time_min = HFS_MIN_TIMESTAMP_SECS;
+ sb->s_time_max = HFS_MAX_TIMESTAMP_SECS;
+
err = -EFBIG;
last_fs_block = sbi->total_blocks - 1;
last_fs_page = (last_fs_block << sbi->alloc_blksz_shift) >>
diff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h
index d6a615e74b26..d4e91dea2362 100644
--- a/include/linux/hfs_common.h
+++ b/include/linux/hfs_common.h
@@ -667,4 +667,17 @@ typedef union {
struct hfsplus_attr_key attr;
} __packed hfsplus_btree_key;
+/*
+ * There are two time systems. Both are based on seconds since
+ * a particular time/date.
+ * Unix: signed little-endian since 00:00 GMT, Jan. 1, 1970
+ * mac: unsigned big-endian since 00:00 GMT, Jan. 1, 1904
+ */
+#define HFS_UTC_OFFSET 2082844800U
+
+/* January 1, 1904, 00:00:00 UTC */
+#define HFS_MIN_TIMESTAMP_SECS -2082844800LL
+/* February 6, 2040, 06:28:15 UTC */
+#define HFS_MAX_TIMESTAMP_SECS 2212122495LL
+
#endif /* _HFS_COMMON_H_ */
--
2.43.0