AI Trajectory


Trajectory: (Expand/Collapse All)
Seq Timestamp Type Name Duration
0/0 2026/10/10 00:10 flow patch-triage
3m Args:
null

Results:
{
  "EnableConfigs": [
    "HFSPLUS_FS"
  ],
  "FocusSymbols": [
    "hfsplus_read_timestamp",
    "hfsplus_get_timestamp_xattr",
    "hfsplus_set_timestamp_xattr",
    "hfsplus_remove_timestamp_xattr",
    "hfsplus_set_attr_cnid",
    "hfsplus_cat_write_inode",
    "hfsplus_cat_read_inode",
    "hfsplus_create_cat"
  ],
  "KMSANReasoning": "The patch series adds support for 64-bit timestamps in the HFS+ filesystem by encoding timestamps beyond February 2040 (HFS_MAX_TIMESTAMP_SECS) in internal extended attributes prefixed with \"linux_hfs.\", along with corresponding KUnit tests.\n\nEvaluating against KMSAN vs KASAN:\n1. Stack and Heap Initialization: All new local stack structures (`struct hfsplus_timestamps`, `struct timespec64`, `struct hfsplus_xattr_changes`) are zero-initialized with `= {0}` at declaration. Inode structure fields such as `birthdate` are explicitly initialized to 0 in `hfsplus_iget()`. Dynamic name allocations use `kzalloc()`.\n2. Info-leak risks to user space: Internal timestamp xattrs are explicitly filtered out from user space visibility in `can_list()`, `hfsplus_osx_getxattr()`, and `hfsplus_osx_setxattr()`. In `hfsplus_getattr()`, both `stat-\u003ebtime.tv_sec` and `stat-\u003ebtime.tv_nsec` are explicitly assigned without leaving padding or fields uninitialized.\n3. Read safety: Output pointers in `hfsplus_read_timestamp()` are initialized on entry, and `hfsplus_get_timestamp_xattr()` validates that the xattr payload size matches `sizeof(value)` before interpreting the data.\n4. Applicability of other sanitizers: Any potential defects related to corrupted on-disk btree metadata, buffer bounds, or lock contention are detected by standard KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory reads or info-leak risks are exposed, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false,
  "Reasoning": "The patch implements an extension in the HFS+ filesystem to support timestamps beyond February 2040 by storing 64-bit timestamps in internal extended attributes ('linux_hfs.*'). This adds new logic for catalog record creation, inode read/write operations, and btree attribute management. The code is fully reachable via standard filesystem syscalls (mount, creat, utimes/futimens, statx) on loopback HFS+ images in virtualized environments, making it a good candidate for fuzzing to catch btree inconsistencies, locking issues, or edge cases in timestamp conversion and xattr handling.",
  "WorthFuzzing": true
}

1/1 2026/10/10 00:10 action read-patch-diff
0m Args:
null

Results:
{
  "PatchDiff": "commit 9a5a9dae0ad59d6f64ca490471bca3aebb1151b4\nAuthor: syz-cluster \u003ctriage@syzkaller.com\u003e\nDate:   Sat Oct 10 00:10:33 2026 +0000\n\n    syz-cluster: applied patch under review\n\ndiff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h\nindex e250f87a5e336..659b0ba733d49 100644\n--- a/fs/hfs/hfs_fs.h\n+++ b/fs/hfs/hfs_fs.h\n@@ -255,19 +255,6 @@ extern int hfs_mac2asc(struct super_block *sb,\n /* super.c */\n extern void hfs_mark_mdb_dirty(struct super_block *sb);\n \n-/*\n- * There are two time systems.  Both are based on seconds since\n- * a particular time/date.\n- *\tUnix:\tsigned little-endian since 00:00 GMT, Jan. 1, 1970\n- *\tmac:\tunsigned big-endian since 00:00 GMT, Jan. 1, 1904\n- *\n- * HFS implementations are highly inconsistent, this one matches the\n- * traditional behavior of 64-bit Linux, giving the most useful\n- * time range between 1970 and 2106, by treating any on-disk timestamp\n- * under HFS_UTC_OFFSET (Jan 1 1970) as a time between 2040 and 2106.\n- */\n-#define HFS_UTC_OFFSET 2082844800U\n-\n static inline time64_t __hfs_m_to_utime(__be32 mt)\n {\n \ttime64_t ut = (u32)(be32_to_cpu(mt) - HFS_UTC_OFFSET);\ndiff --git a/fs/hfsplus/Makefile b/fs/hfsplus/Makefile\nindex f2a9ae697e811..94a262ae91776 100644\n--- a/fs/hfsplus/Makefile\n+++ b/fs/hfsplus/Makefile\n@@ -10,4 +10,4 @@ hfsplus-objs := super.o options.o inode.o ioctl.o extents.o catalog.o dir.o btre\n \t\tattributes.o xattr.o xattr_user.o xattr_security.o xattr_trusted.o\n \n # KUnit tests\n-obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o\n+obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o time_test.o\ndiff --git a/fs/hfsplus/attributes.c b/fs/hfsplus/attributes.c\nindex 7c2e589d45538..51b18d780f2c0 100644\n--- a/fs/hfsplus/attributes.c\n+++ b/fs/hfsplus/attributes.c\n@@ -206,7 +206,8 @@ int hfsplus_attr_exists(struct inode *inode, const char *name)\n }\n \n static\n-int hfsplus_create_attr_nolock(struct inode *inode, const char *name,\n+int hfsplus_create_attr_nolock(struct inode *inode, u32 cnid,\n+\t\t\t\tconst char *name,\n \t\t\t\tconst void *value, size_t size,\n \t\t\t\tstruct hfs_find_data *fd,\n \t\t\t\thfsplus_attr_entry *entry_ptr)\n@@ -215,12 +216,12 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,\n \tint entry_size;\n \tint err;\n \n-\thfs_dbg(\"name %s, ino %llu\\n\",\n-\t\tname ? name : NULL, inode-\u003ei_ino);\n+\thfs_dbg(\"name %s, ino %llu, cnid %u\\n\",\n+\t\tname ? name : NULL, inode-\u003ei_ino, cnid);\n \n \tif (name) {\n \t\terr = hfsplus_attr_build_key(sb, fd-\u003esearch_key,\n-\t\t\t\t\t\tinode-\u003ei_ino, name);\n+\t\t\t\t\t\tcnid, name);\n \t\tif (err)\n \t\t\treturn err;\n \t} else\n@@ -229,7 +230,7 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,\n \t/* Mac OS X supports only inline data attributes. */\n \tentry_size = hfsplus_attr_build_record(entry_ptr,\n \t\t\t\t\tHFSPLUS_ATTR_INLINE_DATA,\n-\t\t\t\t\tinode-\u003ei_ino,\n+\t\t\t\t\tcnid,\n \t\t\t\t\tvalue, size);\n \tif (entry_size == HFSPLUS_INVALID_ATTR_RECORD) {\n \t\tif (size \u003e HFSPLUS_MAX_INLINE_DATA_SIZE)\n@@ -289,8 +290,8 @@ int hfsplus_create_attr(struct inode *inode,\n \tif (err)\n \t\tgoto failed_create_attr;\n \n-\terr = hfsplus_create_attr_nolock(inode, name, value, size,\n-\t\t\t\t\t \u0026fd, entry_ptr);\n+\terr = hfsplus_create_attr_nolock(inode, (u32)inode-\u003ei_ino, name,\n+\t\t\t\t\t value, size, \u0026fd, entry_ptr);\n \tif (err)\n \t\tgoto failed_create_attr;\n \n@@ -347,18 +348,19 @@ static int __hfsplus_delete_attr(struct inode *inode, u32 cnid,\n }\n \n static\n-int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,\n+int hfsplus_delete_attr_nolock(struct inode *inode, u32 cnid,\n+\t\t\t\tconst char *name,\n \t\t\t\tstruct hfs_find_data *fd)\n {\n \tstruct super_block *sb = inode-\u003ei_sb;\n \tint err;\n \n-\thfs_dbg(\"name %s, ino %llu\\n\",\n-\t\tname ? name : NULL, inode-\u003ei_ino);\n+\thfs_dbg(\"name %s, ino %llu, cnid %u\\n\",\n+\t\tname ? name : NULL, inode-\u003ei_ino, cnid);\n \n \tif (name) {\n \t\terr = hfsplus_attr_build_key(sb, fd-\u003esearch_key,\n-\t\t\t\t\t\tinode-\u003ei_ino, name);\n+\t\t\t\t\t\tcnid, name);\n \t\tif (err)\n \t\t\treturn err;\n \t} else {\n@@ -373,7 +375,7 @@ int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,\n \t} else if (err)\n \t\treturn err;\n \n-\terr = __hfsplus_delete_attr(inode, inode-\u003ei_ino, fd);\n+\terr = __hfsplus_delete_attr(inode, cnid, fd);\n \tif (err)\n \t\treturn err;\n \n@@ -403,7 +405,7 @@ int hfsplus_delete_attr(struct inode *inode, const char *name)\n \tif (err)\n \t\tgoto out;\n \n-\terr = hfsplus_delete_attr_nolock(inode, name, \u0026fd);\n+\terr = hfsplus_delete_attr_nolock(inode, (u32)inode-\u003ei_ino, name, \u0026fd);\n \tif (err)\n \t\tgoto out;\n \n@@ -481,12 +483,12 @@ int hfsplus_replace_attr(struct inode *inode,\n \tif (err)\n \t\tgoto failed_replace_attr;\n \n-\terr = hfsplus_delete_attr_nolock(inode, name, \u0026fd);\n+\terr = hfsplus_delete_attr_nolock(inode, (u32)inode-\u003ei_ino, name, \u0026fd);\n \tif (err)\n \t\tgoto failed_replace_attr;\n \n-\terr = hfsplus_create_attr_nolock(inode, name, value, size,\n-\t\t\t\t\t \u0026fd, entry_ptr);\n+\terr = hfsplus_create_attr_nolock(inode, (u32)inode-\u003ei_ino, name,\n+\t\t\t\t\t value, size, \u0026fd, entry_ptr);\n \tif (err)\n \t\tgoto failed_replace_attr;\n \n@@ -497,3 +499,54 @@ int hfsplus_replace_attr(struct inode *inode,\n \thfsplus_destroy_attr_entry(entry_ptr);\n \treturn err;\n }\n+\n+/*\n+ * Create or replace the xattr @name of catalog record @cnid.\n+ * The @inode is used only for access to the superblock and for\n+ * marking the attributes tree as dirty. It is the caller's\n+ * responsibility to set HFSPLUS_XATTR_EXISTS in the catalog record.\n+ */\n+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,\n+\t\t\t  const char *name,\n+\t\t\t  const void *value, size_t size)\n+{\n+\tstruct super_block *sb = inode-\u003ei_sb;\n+\tstruct hfs_find_data fd;\n+\thfsplus_attr_entry *entry_ptr;\n+\tint err = 0;\n+\n+\thfs_dbg(\"name %s, ino %llu, cnid %u\\n\",\n+\t\tname ? name : NULL, inode-\u003ei_ino, cnid);\n+\n+\tif (!HFSPLUS_SB(sb)-\u003eattr_tree) {\n+\t\tpr_err(\"attributes file doesn't exist\\n\");\n+\t\treturn -EINVAL;\n+\t}\n+\n+\tentry_ptr = hfsplus_alloc_attr_entry();\n+\tif (!entry_ptr)\n+\t\treturn -ENOMEM;\n+\n+\terr = hfs_find_init(HFSPLUS_SB(sb)-\u003eattr_tree, \u0026fd);\n+\tif (err)\n+\t\tgoto failed_init_set_attr;\n+\n+\t/* Fail early and avoid ENOSPC during the btree operation */\n+\terr = hfs_bmap_reserve(fd.tree, fd.tree-\u003edepth + 1);\n+\tif (err)\n+\t\tgoto failed_set_attr;\n+\n+\terr = hfsplus_delete_attr_nolock(inode, cnid, name, \u0026fd);\n+\tif (err \u0026\u0026 err != -ENOENT \u0026\u0026 err != -ENODATA)\n+\t\tgoto failed_set_attr;\n+\n+\terr = hfsplus_create_attr_nolock(inode, cnid, name,\n+\t\t\t\t\t value, size, \u0026fd, entry_ptr);\n+\n+failed_set_attr:\n+\thfs_find_exit(\u0026fd);\n+\n+failed_init_set_attr:\n+\thfsplus_destroy_attr_entry(entry_ptr);\n+\treturn err;\n+}\ndiff --git a/fs/hfsplus/catalog.c b/fs/hfsplus/catalog.c\nindex fe11c4b6dd997..d710419db3591 100644\n--- a/fs/hfsplus/catalog.c\n+++ b/fs/hfsplus/catalog.c\n@@ -12,6 +12,7 @@\n \n #include \"hfsplus_fs.h\"\n #include \"hfsplus_raw.h\"\n+#include \"xattr.h\"\n \n int hfsplus_cat_case_cmp_key(const hfsplus_btree_key *k1,\n \t\t\t     const hfsplus_btree_key *k2)\n@@ -102,10 +103,63 @@ void hfsplus_cat_set_perms(struct inode *inode, struct hfsplus_perm *perms)\n \t\tperms-\u003edev = 0;\n }\n \n+static void hfsplus_copy_timestamps2folder(struct hfsplus_timestamps *timestamps,\n+\t\t\t\t\t    struct hfsplus_cat_folder *folder)\n+{\n+\tfolder-\u003ecreate_date = timestamps-\u003ecreate_date;\n+\tfolder-\u003econtent_mod_date = timestamps-\u003econtent_mod_date;\n+\tfolder-\u003eattribute_mod_date = timestamps-\u003eattribute_mod_date;\n+\tfolder-\u003eaccess_date = timestamps-\u003eaccess_date;\n+}\n+\n+static void hfsplus_copy_timestamps2file(struct hfsplus_timestamps *timestamps,\n+\t\t\t\t\t  struct hfsplus_cat_file *file)\n+{\n+\tfile-\u003ecreate_date = timestamps-\u003ecreate_date;\n+\tfile-\u003econtent_mod_date = timestamps-\u003econtent_mod_date;\n+\tfile-\u003eattribute_mod_date = timestamps-\u003eattribute_mod_date;\n+\tfile-\u003eaccess_date = timestamps-\u003eaccess_date;\n+}\n+\n+/*\n+ * Set creation date of new inode. The creation date after February 2040\n+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK in the catalog record. The real\n+ * value is saved into internal xattr by hfsplus_create_cat().\n+ */\n+static void hfsplus_set_create_date(struct inode *inode,\n+\t\t\t\t     struct hfsplus_timestamps *timestamps)\n+{\n+\ttime64_t ut = ktime_get_real_seconds();\n+\t__be32 create_date = __hfsp_ut2mt(ut);\n+\n+\tHFSPLUS_I(inode)-\u003ebirthdate = ut;\n+\tHFSPLUS_I(inode)-\u003ecreate_date = create_date;\n+\n+\ttimestamps-\u003ecreate_date = create_date;\n+\ttimestamps-\u003econtent_mod_date = create_date;\n+\ttimestamps-\u003eattribute_mod_date = create_date;\n+\ttimestamps-\u003eaccess_date = create_date;\n+}\n+\n+/*\n+ * Real creation date of new catalog record. The hard link record\n+ * inherits the creation date of the hidden directory.\n+ */\n+static time64_t hfsplus_cat_record_birthdate(u32 cnid, struct inode *inode)\n+{\n+\tstruct hfsplus_sb_info *sbi = HFSPLUS_SB(inode-\u003ei_sb);\n+\n+\tif (!S_ISDIR(inode-\u003ei_mode) \u0026\u0026 cnid != inode-\u003ei_ino)\n+\t\treturn HFSPLUS_I(sbi-\u003ehidden_dir)-\u003ebirthdate;\n+\n+\treturn HFSPLUS_I(inode)-\u003ebirthdate;\n+}\n+\n static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,\n-\t\tu32 cnid, struct inode *inode)\n+\t\t\t\t    u32 cnid, struct inode *inode)\n {\n \tstruct hfsplus_sb_info *sbi = HFSPLUS_SB(inode-\u003ei_sb);\n+\tstruct hfsplus_timestamps timestamps = {0};\n \n \tif (S_ISDIR(inode-\u003ei_mode)) {\n \t\tstruct hfsplus_cat_folder *folder;\n@@ -116,11 +170,12 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,\n \t\tif (test_bit(HFSPLUS_SB_HFSX, \u0026sbi-\u003eflags))\n \t\t\tfolder-\u003eflags |= cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT);\n \t\tfolder-\u003eid = cpu_to_be32(inode-\u003ei_ino);\n-\t\tHFSPLUS_I(inode)-\u003ecreate_date =\n-\t\t\tfolder-\u003ecreate_date =\n-\t\t\tfolder-\u003econtent_mod_date =\n-\t\t\tfolder-\u003eattribute_mod_date =\n-\t\t\tfolder-\u003eaccess_date = hfsp_now2mt();\n+\n+\t\thfsplus_set_create_date(inode, \u0026timestamps);\n+\t\thfsplus_copy_timestamps2folder(\u0026timestamps, folder);\n+\t\tif (hfsp_ut_needs_xattr(HFSPLUS_I(inode)-\u003ebirthdate))\n+\t\t\tfolder-\u003eflags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);\n+\n \t\thfsplus_cat_set_perms(inode, \u0026folder-\u003epermissions);\n \t\tif (inode == sbi-\u003ehidden_dir)\n \t\t\t/* invisible and namelocked */\n@@ -134,11 +189,23 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,\n \t\tfile-\u003etype = cpu_to_be16(HFSPLUS_FILE);\n \t\tfile-\u003eflags = cpu_to_be16(HFSPLUS_FILE_THREAD_EXISTS);\n \t\tfile-\u003eid = cpu_to_be32(cnid);\n-\t\tHFSPLUS_I(inode)-\u003ecreate_date =\n-\t\t\tfile-\u003ecreate_date =\n-\t\t\tfile-\u003econtent_mod_date =\n-\t\t\tfile-\u003eattribute_mod_date =\n-\t\t\tfile-\u003eaccess_date = hfsp_now2mt();\n+\n+\t\tif (cnid == inode-\u003ei_ino) {\n+\t\t\thfsplus_set_create_date(inode, \u0026timestamps);\n+\t\t} else {\n+\t\t\t/*\n+\t\t\t * Hard link record: don't change creation date\n+\t\t\t * of the linked inode.\n+\t\t\t */\n+\t\t\t__be32 now = __hfsp_ut2mt(ktime_get_real_seconds());\n+\n+\t\t\ttimestamps.create_date = now;\n+\t\t\ttimestamps.content_mod_date = now;\n+\t\t\ttimestamps.attribute_mod_date = now;\n+\t\t\ttimestamps.access_date = now;\n+\t\t}\n+\t\thfsplus_copy_timestamps2file(\u0026timestamps, file);\n+\n \t\tif (cnid == inode-\u003ei_ino) {\n \t\t\thfsplus_cat_set_perms(inode, \u0026file-\u003epermissions);\n \t\t\tif (S_ISLNK(inode-\u003ei_mode)) {\n@@ -169,6 +236,10 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,\n \t\t\tfile-\u003epermissions.dev =\n \t\t\t\tcpu_to_be32(HFSPLUS_I(inode)-\u003elinkid);\n \t\t}\n+\n+\t\tif (hfsp_ut_needs_xattr(hfsplus_cat_record_birthdate(cnid, inode)))\n+\t\t\tfile-\u003eflags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);\n+\n \t\treturn sizeof(*file);\n \t}\n }\n@@ -256,6 +327,7 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,\n \tstruct super_block *sb = dir-\u003ei_sb;\n \tstruct hfs_find_data fd;\n \thfsplus_cat_entry entry;\n+\ttime64_t birthdate;\n \tint entry_size;\n \tint err;\n \n@@ -309,6 +381,22 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,\n \tif (err)\n \t\tgoto err1;\n \n+\tbirthdate = hfsplus_cat_record_birthdate(cnid, inode);\n+\tif (hfsp_ut_needs_xattr(birthdate)) {\n+\t\t/*\n+\t\t * Failure is not critical: the creation date\n+\t\t * will be read as HFS_MAX_TIMESTAMP_SECS.\n+\t\t */\n+\t\terr = hfsplus_set_timestamp_xattr(inode, cnid,\n+\t\t\t\t\t\t  XATTR_LINUX_CREATE_DATE_NAME,\n+\t\t\t\t\t\t  birthdate);\n+\t\tif (err) {\n+\t\t\tpr_warn(\"fail to save creation date: cnid %u, err %d\\n\",\n+\t\t\t\tcnid, err);\n+\t\t\terr = 0;\n+\t\t}\n+\t}\n+\n \tdir-\u003ei_size++;\n \tif (S_ISDIR(inode-\u003ei_mode))\n \t\thfsplus_subfolders_inc(dir);\ndiff --git a/fs/hfsplus/dir.c b/fs/hfsplus/dir.c\nindex 51fcba2e6d403..af0520ebced70 100644\n--- a/fs/hfsplus/dir.c\n+++ b/fs/hfsplus/dir.c\n@@ -26,6 +26,43 @@ static inline void hfsplus_instantiate(struct dentry *dentry,\n \td_instantiate(dentry, inode);\n }\n \n+/*\n+ * Check that creation date of catalog record @cnid is identical to\n+ * creation date of @inode. If the creation date is after February 2040,\n+ * then the real value is compared by means of internal xattr.\n+ */\n+static inline bool is_create_date_identical(struct inode *inode,\n+\t\t\t\t\t    u32 cnid, __be32 create_date)\n+{\n+\ttime64_t timestamp;\n+\tbool has_xattr;\n+\tint err;\n+\n+\tif (create_date != HFSPLUS_I(inode)-\u003ecreate_date)\n+\t\treturn false;\n+\n+\tif (!hfsp_mt_is_ext_timestamp(create_date))\n+\t\treturn true;\n+\n+\terr = hfsplus_read_timestamp(inode-\u003ei_sb, cnid,\n+\t\t\t\t     XATTR_LINUX_CREATE_DATE_NAME,\n+\t\t\t\t     create_date, \u0026timestamp, \u0026has_xattr);\n+\tif (err) {\n+\t\tpr_err(\"timestamp extraction failure: cnid %u, err %d\\n\",\n+\t\t\tcnid, err);\n+\t\treturn false;\n+\t}\n+\n+\t/*\n+\t * The record without xattr (for example, created by Mac OS X)\n+\t * cannot be distinguished by means of 64-bit timestamp.\n+\t */\n+\tif (!has_xattr)\n+\t\treturn true;\n+\n+\treturn timestamp == HFSPLUS_I(inode)-\u003ebirthdate;\n+}\n+\n /* Find the entry inside dir named dentry-\u003ed_name */\n static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,\n \t\t\t\t     unsigned int flags)\n@@ -78,12 +115,12 @@ static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,\n \t\t\t\tentry.file.user_info.fdCreator ==\n \t\t\t\tcpu_to_be32(HFSP_HFSPLUS_CREATOR) \u0026\u0026\n \t\t\t\tHFSPLUS_SB(sb)-\u003ehidden_dir \u0026\u0026\n-\t\t\t\t(entry.file.create_date ==\n-\t\t\t\t\tHFSPLUS_I(HFSPLUS_SB(sb)-\u003ehidden_dir)-\u003e\n-\t\t\t\t\t\tcreate_date ||\n-\t\t\t\tentry.file.create_date ==\n-\t\t\t\t\tHFSPLUS_I(d_inode(sb-\u003es_root))-\u003e\n-\t\t\t\t\t\tcreate_date)) {\n+\t\t\t\t(is_create_date_identical(\n+\t\t\t\t\tHFSPLUS_SB(sb)-\u003ehidden_dir,\n+\t\t\t\t\tcnid, entry.file.create_date) ||\n+\t\t\t\tis_create_date_identical(\n+\t\t\t\t\td_inode(sb-\u003es_root),\n+\t\t\t\t\tcnid, entry.file.create_date))) {\n \t\t\tstruct qstr str;\n \t\t\tchar name[32];\n \ndiff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h\nindex 1e5b58e6a13fc..637bba65568ec 100644\n--- a/fs/hfsplus/hfsplus_fs.h\n+++ b/fs/hfsplus/hfsplus_fs.h\n@@ -175,6 +175,13 @@ static inline struct hfsplus_sb_info *HFSPLUS_SB(struct super_block *sb)\n \treturn sb-\u003es_fs_info;\n }\n \n+struct hfsplus_timestamps {\n+\t__be32 create_date;\n+\t__be32 content_mod_date;\n+\t__be32 attribute_mod_date;\n+\t__be32 access_date;\n+\t__be32 backup_date;\n+};\n \n struct hfsplus_inode_info {\n \tatomic_t opencnt;\n@@ -196,7 +203,8 @@ struct hfsplus_inode_info {\n \t * Immutable data.\n \t */\n \tstruct inode *rsrc_inode;\n-\t__be32 create_date;\n+\t__be32 create_date;\t/* on-disk (maybe clamped) creation date */\n+\ttime64_t birthdate;\t/* real creation date (seconds since 1970) */\n \n \t/*\n \t * Protected by sbi-\u003evh_mutex.\n@@ -227,6 +235,9 @@ struct hfsplus_inode_info {\n #define HFSPLUS_I_EXT_DIRTY\t2\t/* has changes in the extent tree */\n #define HFSPLUS_I_ALLOC_DIRTY\t3\t/* has changes in the allocation file */\n #define HFSPLUS_I_ATTR_DIRTY\t4\t/* has changes in the attributes tree */\n+#define HFSPLUS_I_ATIME_XATTR\t5\t/* access date is stored in xattr */\n+#define HFSPLUS_I_MTIME_XATTR\t6\t/* content mod date is stored in xattr */\n+#define HFSPLUS_I_CTIME_XATTR\t7\t/* attribute mod date is stored in xattr */\n \n #define HFSPLUS_IS_RSRC(inode) \\\n \ttest_bit(HFSPLUS_I_RSRC, \u0026HFSPLUS_I(inode)-\u003eflags)\n@@ -351,6 +362,9 @@ int hfsplus_delete_all_attrs(struct inode *dir, u32 cnid);\n int hfsplus_replace_attr(struct inode *inode,\n \t\t\t const char *name,\n \t\t\t const void *value, size_t size);\n+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,\n+\t\t\t  const char *name,\n+\t\t\t  const void *value, size_t size);\n \n /* bitmap.c */\n int hfsplus_block_allocate(struct super_block *sb, u32 size, u32 offset,\n@@ -467,6 +481,9 @@ int hfsplus_file_fsync(struct file *file, loff_t start, loff_t end,\n int hfsplus_fileattr_get(struct dentry *dentry, struct file_kattr *fa);\n int hfsplus_fileattr_set(struct mnt_idmap *idmap,\n \t\t\t struct dentry *dentry, struct file_kattr *fa);\n+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,\n+\t\t\t   const char *name, __be32 mt,\n+\t\t\t   time64_t *ut, bool *has_xattr);\n \n /* ioctl.c */\n long hfsplus_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);\n@@ -532,23 +549,42 @@ int hfsplus_brec_read_cat(struct hfs_find_data *fd, hfsplus_cat_entry *entry);\n /*\n  * time helpers: convert between 1904-base and 1970-base timestamps\n  *\n- * HFS+ implementations are highly inconsistent, this one matches the\n- * traditional behavior of 64-bit Linux, giving the most useful\n- * time range between 1970 and 2106, by treating any on-disk timestamp\n- * under HFSPLUS_UTC_OFFSET (Jan 1 1970) as a time between 2040 and 2106.\n+ * The on-disk 32-bit timestamps cover the range from\n+ * HFS_MIN_TIMESTAMP_SECS (Jan. 1, 1904) till HFS_MAX_TIMESTAMP_SECS\n+ * (Feb. 6, 2040). The timestamps out of this range are clamped.\n+ * The on-disk value HFSPLUS_EXT_TIMESTAMP_MARK (maximal value) means\n+ * that real timestamp could be stored in the internal xattr of\n+ * the catalog record. It is Linux specific extension of\n+ * HFS+ on-disk layout. It makes possible to support timestamps\n+ * till HFSPLUS_MAX_TIMESTAMP_SECS.\n  */\n-#define HFSPLUS_UTC_OFFSET 2082844800U\n+#define HFSPLUS_EXT_TIMESTAMP_MARK\tcpu_to_be32(U32_MAX)\n+#define HFSPLUS_MAX_TIMESTAMP_SECS\tTIME64_MAX\n \n static inline time64_t __hfsp_mt2ut(__be32 mt)\n {\n-\ttime64_t ut = (u32)(be32_to_cpu(mt) - HFSPLUS_UTC_OFFSET);\n+\ttime64_t ut = (time64_t)be32_to_cpu(mt) - HFS_UTC_OFFSET;\n \n \treturn ut;\n }\n \n static inline __be32 __hfsp_ut2mt(time64_t ut)\n {\n-\treturn cpu_to_be32(lower_32_bits(ut) + HFSPLUS_UTC_OFFSET);\n+\tut = clamp_t(time64_t, ut,\n+\t\t     HFS_MIN_TIMESTAMP_SECS, HFS_MAX_TIMESTAMP_SECS);\n+\tut += HFS_UTC_OFFSET;\n+\n+\treturn cpu_to_be32(lower_32_bits(ut));\n+}\n+\n+static inline bool hfsp_ut_needs_xattr(time64_t ut)\n+{\n+\treturn ut \u003e= HFS_MAX_TIMESTAMP_SECS;\n+}\n+\n+static inline bool hfsp_mt_is_ext_timestamp(__be32 mt)\n+{\n+\treturn mt == HFSPLUS_EXT_TIMESTAMP_MARK;\n }\n \n static inline enum hfsplus_btree_mutex_classes\ndiff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c\nindex 2ce6de574fa6e..0df58dfda5a3e 100644\n--- a/fs/hfsplus/inode.c\n+++ b/fs/hfsplus/inode.c\n@@ -18,6 +18,7 @@\n #include \u003clinux/cred.h\u003e\n #include \u003clinux/uio.h\u003e\n #include \u003clinux/fileattr.h\u003e\n+#include \u003ckunit/visibility.h\u003e\n \n #include \"hfsplus_fs.h\"\n #include \"hfsplus_raw.h\"\n@@ -344,7 +345,8 @@ int hfsplus_getattr(struct mnt_idmap *idmap, const struct path *path,\n \n \tif (request_mask \u0026 STATX_BTIME) {\n \t\tstat-\u003eresult_mask |= STATX_BTIME;\n-\t\tstat-\u003ebtime = hfsp_mt2ut(hip-\u003ecreate_date);\n+\t\tstat-\u003ebtime.tv_sec = hip-\u003ebirthdate;\n+\t\tstat-\u003ebtime.tv_nsec = 0;\n \t}\n \n \tif (inode-\u003ei_flags \u0026 S_APPEND)\n@@ -598,9 +600,139 @@ void hfsplus_inode_write_fork(struct inode *inode,\n \tfork-\u003etotal_blocks = cpu_to_be32(HFSPLUS_I(inode)-\u003ealloc_blocks);\n }\n \n+/*\n+ * hfsplus_read_timestamp - convert on-disk timestamp into 1970-base one\n+ * @sb: superblock\n+ * @cnid: catalog record ID\n+ * @name: name of internal xattr that can keep the real timestamp\n+ * @mt: on-disk timestamp\n+ * @ut: pointer on converted timestamp [out]\n+ * @has_xattr: pointer on flag that xattr exists [out]\n+ *\n+ * If on-disk timestamp is equal to HFSPLUS_EXT_TIMESTAMP_MARK, then\n+ * the real timestamp (after February 2040) could be stored in\n+ * the internal xattr. If the xattr is absent (for example, the record\n+ * has been created by Mac OS X), then HFS_MAX_TIMESTAMP_SECS is used.\n+ */\n+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,\n+\t\t\t   const char *name, __be32 mt,\n+\t\t\t   time64_t *ut, bool *has_xattr)\n+{\n+\ttime64_t timestamp;\n+\tint err;\n+\n+\t*ut = __hfsp_mt2ut(mt);\n+\t*has_xattr = false;\n+\n+\tif (!hfsp_mt_is_ext_timestamp(mt))\n+\t\treturn 0;\n+\n+\terr = hfsplus_get_timestamp_xattr(sb, cnid, name, \u0026timestamp);\n+\tif (err == -ENODATA || err == -EOPNOTSUPP) {\n+\t\t/* timestamp is really equal to HFS_MAX_TIMESTAMP_SECS */\n+\t\treturn 0;\n+\t} else if (unlikely(err)) {\n+\t\tpr_warn(\"fail to extract timestamp: cnid %u, name %s, err %d\\n\",\n+\t\t\tcnid, name, err);\n+\t\treturn err;\n+\t}\n+\n+\tif (timestamp \u003c HFS_MAX_TIMESTAMP_SECS) {\n+\t\tpr_warn(\"invalid timestamp in xattr: cnid %u, name %s, timestamp %lld\\n\",\n+\t\t\tcnid, name, timestamp);\n+\t\t/* keep xattr to be deleted or overwritten */\n+\t\t*has_xattr = true;\n+\t} else {\n+\t\t*ut = timestamp;\n+\t\t*has_xattr = true;\n+\t}\n+\n+\treturn 0;\n+}\n+EXPORT_SYMBOL_IF_KUNIT(hfsplus_read_timestamp);\n+\n+static int hfsplus_inode_read_timestamps(struct inode *inode,\n+\t\t\t\t\t struct hfsplus_timestamps *timestamps)\n+{\n+\tstruct super_block *sb = inode-\u003ei_sb;\n+\tstruct hfsplus_inode_info *hip = HFSPLUS_I(inode);\n+\tu32 cnid = inode-\u003ei_ino;\n+\tstruct timespec64 ts = {0};\n+\tbool has_xattr;\n+\tint res = 0;\n+\n+\tres = hfsplus_read_timestamp(sb, cnid,\n+\t\t\t\t     XATTR_LINUX_ACCESS_DATE_NAME,\n+\t\t\t\t     timestamps-\u003eaccess_date,\n+\t\t\t\t     \u0026ts.tv_sec, \u0026has_xattr);\n+\tif (res)\n+\t\tgoto finish_read_timestamps;\n+\n+\tinode_set_atime_to_ts(inode, ts);\n+\tif (has_xattr)\n+\t\tset_bit(HFSPLUS_I_ATIME_XATTR, \u0026hip-\u003eflags);\n+\n+\tres = hfsplus_read_timestamp(sb, cnid,\n+\t\t\t\t     XATTR_LINUX_CONTENT_MOD_DATE_NAME,\n+\t\t\t\t     timestamps-\u003econtent_mod_date,\n+\t\t\t\t     \u0026ts.tv_sec, \u0026has_xattr);\n+\tif (res)\n+\t\tgoto finish_read_timestamps;\n+\n+\tinode_set_mtime_to_ts(inode, ts);\n+\tif (has_xattr)\n+\t\tset_bit(HFSPLUS_I_MTIME_XATTR, \u0026hip-\u003eflags);\n+\n+\tres = hfsplus_read_timestamp(sb, cnid,\n+\t\t\t\t     XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,\n+\t\t\t\t     timestamps-\u003eattribute_mod_date,\n+\t\t\t\t     \u0026ts.tv_sec, \u0026has_xattr);\n+\tif (res)\n+\t\tgoto finish_read_timestamps;\n+\n+\tinode_set_ctime_to_ts(inode, ts);\n+\tif (has_xattr)\n+\t\tset_bit(HFSPLUS_I_CTIME_XATTR, \u0026hip-\u003eflags);\n+\n+\tres = hfsplus_read_timestamp(sb, cnid,\n+\t\t\t\t     XATTR_LINUX_CREATE_DATE_NAME,\n+\t\t\t\t     timestamps-\u003ecreate_date,\n+\t\t\t\t     \u0026hip-\u003ebirthdate, \u0026has_xattr);\n+\tif (res)\n+\t\tgoto finish_read_timestamps;\n+\n+\thip-\u003ecreate_date = timestamps-\u003ecreate_date;\n+\n+finish_read_timestamps:\n+\treturn res;\n+}\n+\n+static inline void\n+hfsplus_copy_folder2timestamps(struct hfsplus_cat_folder *folder,\n+\t\t\t\tstruct hfsplus_timestamps *timestamps)\n+{\n+\ttimestamps-\u003ecreate_date = folder-\u003ecreate_date;\n+\ttimestamps-\u003econtent_mod_date = folder-\u003econtent_mod_date;\n+\ttimestamps-\u003eattribute_mod_date = folder-\u003eattribute_mod_date;\n+\ttimestamps-\u003eaccess_date = folder-\u003eaccess_date;\n+\ttimestamps-\u003ebackup_date = folder-\u003ebackup_date;\n+}\n+\n+static inline void\n+hfsplus_copy_file2timestamps(struct hfsplus_cat_file *file,\n+\t\t\t     struct hfsplus_timestamps *timestamps)\n+{\n+\ttimestamps-\u003ecreate_date = file-\u003ecreate_date;\n+\ttimestamps-\u003econtent_mod_date = file-\u003econtent_mod_date;\n+\ttimestamps-\u003eattribute_mod_date = file-\u003eattribute_mod_date;\n+\ttimestamps-\u003eaccess_date = file-\u003eaccess_date;\n+\ttimestamps-\u003ebackup_date = file-\u003ebackup_date;\n+}\n+\n int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)\n {\n \thfsplus_cat_entry entry;\n+\tstruct hfsplus_timestamps timestamps = {0};\n \tint res = 0;\n \tu16 type;\n \n@@ -622,12 +754,14 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)\n \t\t\tgoto out;\n \t\tset_nlink(inode, 1);\n \t\tinode-\u003ei_size = 2 + be32_to_cpu(folder-\u003evalence);\n-\t\tinode_set_atime_to_ts(inode, hfsp_mt2ut(folder-\u003eaccess_date));\n-\t\tinode_set_mtime_to_ts(inode,\n-\t\t\t\t      hfsp_mt2ut(folder-\u003econtent_mod_date));\n-\t\tinode_set_ctime_to_ts(inode,\n-\t\t\t\t      hfsp_mt2ut(folder-\u003eattribute_mod_date));\n-\t\tHFSPLUS_I(inode)-\u003ecreate_date = folder-\u003ecreate_date;\n+\n+\t\thfsplus_copy_folder2timestamps(folder, \u0026timestamps);\n+\t\tres = hfsplus_inode_read_timestamps(inode, \u0026timestamps);\n+\t\tif (res) {\n+\t\t\tres = -EIO;\n+\t\t\tgoto out;\n+\t\t}\n+\n \t\tHFSPLUS_I(inode)-\u003efs_blocks = 0;\n \t\tif (folder-\u003eflags \u0026 cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {\n \t\t\tHFSPLUS_I(inode)-\u003esubfolders =\n@@ -668,12 +802,13 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)\n \t\t\tinit_special_inode(inode, inode-\u003ei_mode,\n \t\t\t\t\t   be32_to_cpu(file-\u003epermissions.dev));\n \t\t}\n-\t\tinode_set_atime_to_ts(inode, hfsp_mt2ut(file-\u003eaccess_date));\n-\t\tinode_set_mtime_to_ts(inode,\n-\t\t\t\t      hfsp_mt2ut(file-\u003econtent_mod_date));\n-\t\tinode_set_ctime_to_ts(inode,\n-\t\t\t\t      hfsp_mt2ut(file-\u003eattribute_mod_date));\n-\t\tHFSPLUS_I(inode)-\u003ecreate_date = file-\u003ecreate_date;\n+\n+\t\thfsplus_copy_file2timestamps(file, \u0026timestamps);\n+\t\tres = hfsplus_inode_read_timestamps(inode, \u0026timestamps);\n+\t\tif (res) {\n+\t\t\tres = -EIO;\n+\t\t\tgoto out;\n+\t\t}\n \t} else {\n \t\tpr_err(\"bad catalog entry used to create inode\\n\");\n \t\tres = -EIO;\n@@ -682,12 +817,126 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)\n \treturn res;\n }\n \n+struct hfsplus_xattr_changes {\n+\tbool added;\n+\tbool removed;\n+};\n+\n+/*\n+ * Convert @ts into on-disk timestamp. The timestamp after February 2040\n+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK with the real value in\n+ * the internal xattr @name. The stale xattr is removed if timestamp\n+ * becomes representable by on-disk field.\n+ */\n+static int hfsplus_inode_save_timestamp(struct inode *inode,\n+\t\t\t\t\tconst char *name, int xattr_bit,\n+\t\t\t\t\tstruct timespec64 ts, __be32 *date,\n+\t\t\t\t\tstruct hfsplus_xattr_changes *xattr)\n+{\n+\tstruct hfsplus_inode_info *hip = HFSPLUS_I(inode);\n+\ttime64_t stored;\n+\tint err;\n+\n+\t*date = __hfsp_ut2mt(ts.tv_sec);\n+\n+\tif (hfsp_ut_needs_xattr(ts.tv_sec)) {\n+\t\t/* don't rewrite xattr if nothing has been changed */\n+\t\tif (test_bit(xattr_bit, \u0026hip-\u003eflags)) {\n+\t\t\terr = hfsplus_get_timestamp_xattr(inode-\u003ei_sb,\n+\t\t\t\t\t\t\t  inode-\u003ei_ino,\n+\t\t\t\t\t\t\t  name,\n+\t\t\t\t\t\t\t  \u0026stored);\n+\t\t\tif (!err \u0026\u0026 stored == ts.tv_sec)\n+\t\t\t\treturn 0;\n+\t\t}\n+\n+\t\terr = hfsplus_set_timestamp_xattr(inode, inode-\u003ei_ino,\n+\t\t\t\t\t\t  name, ts.tv_sec);\n+\t\tif (err) {\n+\t\t\tclear_bit(xattr_bit, \u0026hip-\u003eflags);\n+\t\t\treturn err;\n+\t\t}\n+\n+\t\tset_bit(xattr_bit, \u0026hip-\u003eflags);\n+\t\txattr-\u003eadded = true;\n+\t} else if (test_bit(xattr_bit, \u0026hip-\u003eflags)) {\n+\t\terr = hfsplus_remove_timestamp_xattr(inode, name);\n+\t\tif (err)\n+\t\t\treturn err;\n+\n+\t\tclear_bit(xattr_bit, \u0026hip-\u003eflags);\n+\t\txattr-\u003eremoved = true;\n+\t}\n+\n+\treturn 0;\n+}\n+\n+static void hfsplus_inode_save_timestamps(struct inode *inode,\n+\t\t\t\t\t  struct hfsplus_timestamps *timestamps,\n+\t\t\t\t\t  struct hfsplus_xattr_changes *changes)\n+{\n+\tint res;\n+\n+\t/*\n+\t * Failure of xattr operation is not critical. The timestamp\n+\t * is clamped by HFS_MAX_TIMESTAMP_SECS in such case.\n+\t * But the rest of the catalog record must be saved anyway.\n+\t */\n+\n+\tres = hfsplus_inode_save_timestamp(inode,\n+\t\t\t\t\t   XATTR_LINUX_ACCESS_DATE_NAME,\n+\t\t\t\t\t   HFSPLUS_I_ATIME_XATTR,\n+\t\t\t\t\t   inode_get_atime(inode),\n+\t\t\t\t\t   \u0026timestamps-\u003eaccess_date,\n+\t\t\t\t\t   changes);\n+\tif (res) {\n+\t\tpr_warn_ratelimited(\"fail to save access date: ino %llu, err %d\\n\",\n+\t\t\t\t    inode-\u003ei_ino, res);\n+\t}\n+\n+\tres = hfsplus_inode_save_timestamp(inode,\n+\t\t\t\t\t   XATTR_LINUX_CONTENT_MOD_DATE_NAME,\n+\t\t\t\t\t   HFSPLUS_I_MTIME_XATTR,\n+\t\t\t\t\t   inode_get_mtime(inode),\n+\t\t\t\t\t   \u0026timestamps-\u003econtent_mod_date,\n+\t\t\t\t\t   changes);\n+\tif (res) {\n+\t\tpr_warn_ratelimited(\"fail to save content mod date: ino %llu, err %d\\n\",\n+\t\t\t\t    inode-\u003ei_ino, res);\n+\t}\n+\n+\tres = hfsplus_inode_save_timestamp(inode,\n+\t\t\t\t\t   XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,\n+\t\t\t\t\t   HFSPLUS_I_CTIME_XATTR,\n+\t\t\t\t\t   inode_get_ctime(inode),\n+\t\t\t\t\t   \u0026timestamps-\u003eattribute_mod_date,\n+\t\t\t\t\t   changes);\n+\tif (res) {\n+\t\tpr_warn_ratelimited(\"fail to save attribute mod date: ino %llu, err %d\\n\",\n+\t\t\t\t    inode-\u003ei_ino, res);\n+\t}\n+}\n+\n+static inline __be16\n+hfsplus_update_xattr_exists_flag(struct inode *inode, __be16 flags,\n+\t\t\t\t struct hfsplus_xattr_changes *xattr)\n+{\n+\tif (xattr-\u003eadded)\n+\t\tflags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);\n+\telse if (xattr-\u003eremoved \u0026\u0026 !hfsplus_attr_exists(inode, NULL))\n+\t\tflags \u0026= cpu_to_be16(~HFSPLUS_XATTR_EXISTS);\n+\n+\treturn flags;\n+}\n+\n int hfsplus_cat_write_inode(struct inode *inode)\n {\n \tstruct inode *main_inode = inode;\n \tstruct hfs_btree *tree = HFSPLUS_SB(inode-\u003ei_sb)-\u003ecat_tree;\n \tstruct hfs_find_data fd;\n \thfsplus_cat_entry entry;\n+\tstruct hfsplus_timestamps timestamps = {0};\n+\tstruct hfsplus_xattr_changes changes = {0};\n \tint res = 0;\n \n \thfs_dbg(\"inode-\u003ei_ino %llu\\n\", inode-\u003ei_ino);\n@@ -718,9 +967,15 @@ int hfsplus_cat_write_inode(struct inode *inode)\n \t\t\t\t\tsizeof(struct hfsplus_cat_folder));\n \t\t/* simple node checks? */\n \t\thfsplus_cat_set_perms(inode, \u0026folder-\u003epermissions);\n-\t\tfolder-\u003eaccess_date = hfsp_ut2mt(inode_get_atime(inode));\n-\t\tfolder-\u003econtent_mod_date = hfsp_ut2mt(inode_get_mtime(inode));\n-\t\tfolder-\u003eattribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));\n+\n+\t\thfsplus_inode_save_timestamps(inode, \u0026timestamps, \u0026changes);\n+\t\tfolder-\u003eaccess_date = timestamps.access_date;\n+\t\tfolder-\u003econtent_mod_date = timestamps.content_mod_date;\n+\t\tfolder-\u003eattribute_mod_date = timestamps.attribute_mod_date;\n+\t\tfolder-\u003eflags = hfsplus_update_xattr_exists_flag(inode,\n+\t\t\t\t\t\t\t\t folder-\u003eflags,\n+\t\t\t\t\t\t\t\t \u0026changes);\n+\n \t\tfolder-\u003evalence = cpu_to_be32(inode-\u003ei_size - 2);\n \t\tif (folder-\u003eflags \u0026 cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {\n \t\t\tfolder-\u003esubfolders =\n@@ -753,9 +1008,15 @@ int hfsplus_cat_write_inode(struct inode *inode)\n \t\t\tfile-\u003eflags |= cpu_to_be16(HFSPLUS_FILE_LOCKED);\n \t\telse\n \t\t\tfile-\u003eflags \u0026= cpu_to_be16(~HFSPLUS_FILE_LOCKED);\n-\t\tfile-\u003eaccess_date = hfsp_ut2mt(inode_get_atime(inode));\n-\t\tfile-\u003econtent_mod_date = hfsp_ut2mt(inode_get_mtime(inode));\n-\t\tfile-\u003eattribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));\n+\n+\t\thfsplus_inode_save_timestamps(inode, \u0026timestamps, \u0026changes);\n+\t\tfile-\u003eaccess_date = timestamps.access_date;\n+\t\tfile-\u003econtent_mod_date = timestamps.content_mod_date;\n+\t\tfile-\u003eattribute_mod_date = timestamps.attribute_mod_date;\n+\t\tfile-\u003eflags = hfsplus_update_xattr_exists_flag(inode,\n+\t\t\t\t\t\t\t       file-\u003eflags,\n+\t\t\t\t\t\t\t       \u0026changes);\n+\n \t\thfs_bnode_write(fd.bnode, \u0026entry, fd.entryoffset,\n \t\t\t\t\t sizeof(struct hfsplus_cat_file));\n \t}\ndiff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c\nindex ff7d6b3336a66..b10b53f6f8f9e 100644\n--- a/fs/hfsplus/super.c\n+++ b/fs/hfsplus/super.c\n@@ -86,6 +86,7 @@ struct inode *hfsplus_iget(struct super_block *sb, unsigned long ino)\n \tmutex_init(\u0026HFSPLUS_I(inode)-\u003eextents_lock);\n \tHFSPLUS_I(inode)-\u003ersrc_inode = NULL;\n \tHFSPLUS_I(inode)-\u003ecreate_date = 0;\n+\tHFSPLUS_I(inode)-\u003ebirthdate = 0;\n \tHFSPLUS_I(inode)-\u003elinkid = 0;\n \tHFSPLUS_I(inode)-\u003eflags = 0;\n \tHFSPLUS_I(inode)-\u003efs_blocks = 0;\n@@ -511,6 +512,13 @@ static int hfsplus_fill_super(struct super_block *sb, struct fs_context *fc)\n \tif (!sbi-\u003ersrc_clump_blocks)\n \t\tsbi-\u003ersrc_clump_blocks = 1;\n \n+\tsb-\u003es_time_gran = NSEC_PER_SEC;\n+\tsb-\u003es_time_min = HFS_MIN_TIMESTAMP_SECS;\n+\t/*\n+\t * Timestamps after HFS_MAX_TIMESTAMP_SECS are stored in xattrs.\n+\t */\n+\tsb-\u003es_time_max = HFS_MAX_TIMESTAMP_SECS;\n+\n \terr = -EFBIG;\n \tlast_fs_block = sbi-\u003etotal_blocks - 1;\n \tlast_fs_page = (last_fs_block \u003c\u003c sbi-\u003ealloc_blksz_shift) \u003e\u003e\ndiff --git a/fs/hfsplus/time_test.c b/fs/hfsplus/time_test.c\nnew file mode 100644\nindex 0000000000000..4b3312dc0048b\n--- /dev/null\n+++ b/fs/hfsplus/time_test.c\n@@ -0,0 +1,354 @@\n+// SPDX-License-Identifier: GPL-2.0\n+/*\n+ * KUnit tests for HFS+ date/time conversion\n+ *\n+ * Copyright (C) 2026 Viacheslav Dubeyko \u003cslava@dubeyko.com\u003e\n+ */\n+\n+#include \u003ckunit/test.h\u003e\n+#include \"hfsplus_fs.h\"\n+#include \"xattr.h\"\n+\n+/* January 1, 1970, 00:00:00 UTC */\n+#define TEST_UNIX_EPOCH_SECS\t\t0LL\n+/* December 31, 1969, 23:59:59 UTC */\n+#define TEST_BEFORE_UNIX_EPOCH_SECS\t-1LL\n+/* January 1, 2000, 00:00:00 UTC */\n+#define TEST_Y2000_SECS\t\t\t946684800LL\n+/* January 19, 2038, 03:14:08 UTC (signed 32-bit overflow) */\n+#define TEST_Y2038_SECS\t\t\t2147483648LL\n+/* January 1, 2026, 00:00:00 UTC */\n+#define TEST_Y2026_SECS\t\t\t1767225600LL\n+/* January 1, 2100, 00:00:00 UTC */\n+#define TEST_Y2100_SECS\t\t\t4102444800LL\n+/* January 1, 1900, 00:00:00 UTC */\n+#define TEST_Y1900_SECS\t\t\t-2208988800LL\n+\n+static u32 mt_raw(__be32 mt)\n+{\n+\treturn be32_to_cpu(mt);\n+}\n+\n+/* Test the time range constants */\n+static void hfsplus_time_constants_test(struct kunit *test)\n+{\n+\t/* 32-bit unsigned on-disk field covers exactly U32_MAX seconds */\n+\tKUNIT_EXPECT_EQ(test, (s64)U32_MAX,\n+\t\t\tHFS_MAX_TIMESTAMP_SECS - HFS_MIN_TIMESTAMP_SECS);\n+\tKUNIT_EXPECT_EQ(test, -(s64)HFS_UTC_OFFSET, HFS_MIN_TIMESTAMP_SECS);\n+\n+\t/* xattrs extend the supported range beyond February 2040 */\n+\tKUNIT_EXPECT_GT(test, (s64)HFSPLUS_MAX_TIMESTAMP_SECS,\n+\t\t\t(s64)HFS_MAX_TIMESTAMP_SECS);\n+\n+\t/* the extended timestamp mark is the maximal on-disk value */\n+\tKUNIT_EXPECT_EQ(test, U32_MAX, mt_raw(HFSPLUS_EXT_TIMESTAMP_MARK));\n+}\n+\n+/* Test conversion of on-disk (1904-based) timestamp into 1970-based one */\n+static void hfsplus_mt2ut_test(struct kunit *test)\n+{\n+\tKUNIT_EXPECT_EQ(test, HFS_MIN_TIMESTAMP_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(0)));\n+\tKUNIT_EXPECT_EQ(test, TEST_UNIX_EPOCH_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET)));\n+\tKUNIT_EXPECT_EQ(test, TEST_BEFORE_UNIX_EPOCH_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET - 1)));\n+\tKUNIT_EXPECT_EQ(test, TEST_Y2000_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(3029529600U)));\n+\tKUNIT_EXPECT_EQ(test, TEST_Y2026_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(3850070400U)));\n+\tKUNIT_EXPECT_EQ(test, TEST_Y2038_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(4230328448U)));\n+\tKUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,\n+\t\t\t__hfsp_mt2ut(cpu_to_be32(U32_MAX)));\n+\tKUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,\n+\t\t\t__hfsp_mt2ut(HFSPLUS_EXT_TIMESTAMP_MARK));\n+}\n+\n+/* Test conversion of 1970-based timestamp into on-disk (1904-based) one */\n+static void hfsplus_ut2mt_test(struct kunit *test)\n+{\n+\tKUNIT_EXPECT_EQ(test, 0U,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS)));\n+\tKUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_UNIX_EPOCH_SECS)));\n+\tKUNIT_EXPECT_EQ(test, 3029529600U,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_Y2000_SECS)));\n+\tKUNIT_EXPECT_EQ(test, 3850070400U,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_Y2026_SECS)));\n+\tKUNIT_EXPECT_EQ(test, 4230328448U,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_Y2038_SECS)));\n+\tKUNIT_EXPECT_EQ(test, U32_MAX - 1,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));\n+}\n+\n+/*\n+ * Test timestamps before January 1, 1970 (xfstests generic/258).\n+ * The old logic added HFS_UTC_OFFSET to lower 32 bits of negative\n+ * value and produced completely wrong result.\n+ */\n+static void hfsplus_ut2mt_before_1970_test(struct kunit *test)\n+{\n+\ttime64_t ut;\n+\n+\tKUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET - 1,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_BEFORE_UNIX_EPOCH_SECS)));\n+\n+\t/* January 1, 1960, 00:00:00 UTC */\n+\tut = -315619200LL;\n+\tKUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));\n+\n+\t/* January 1, 1904, 00:00:01 UTC */\n+\tut = HFS_MIN_TIMESTAMP_SECS + 1;\n+\tKUNIT_EXPECT_EQ(test, 1U, mt_raw(__hfsp_ut2mt(ut)));\n+\tKUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));\n+}\n+\n+/* Test that out-of-range timestamps are clamped instead of wrapped */\n+static void hfsplus_ut2mt_clamp_test(struct kunit *test)\n+{\n+\t/* below January 1, 1904 */\n+\tKUNIT_EXPECT_EQ(test, 0U,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS - 1)));\n+\tKUNIT_EXPECT_EQ(test, 0U,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_Y1900_SECS)));\n+\tKUNIT_EXPECT_EQ(test, 0U,\n+\t\t\tmt_raw(__hfsp_ut2mt(TIME64_MIN)));\n+\n+\t/* after February 6, 2040 */\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS + 1)));\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(TEST_Y2100_SECS)));\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFSPLUS_MAX_TIMESTAMP_SECS)));\n+\n+\t/* lower 32 bits of these values would wrap into small dates */\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS +\n+\t\t\t\t\t    (s64)U32_MAX + 1)));\n+\tKUNIT_EXPECT_EQ(test, U32_MAX,\n+\t\t\tmt_raw(__hfsp_ut2mt(1LL \u003c\u003c 32)));\n+}\n+\n+/* Test that representable timestamps survive the round trip */\n+static void hfsplus_time_roundtrip_test(struct kunit *test)\n+{\n+\tstatic const time64_t values[] = {\n+\t\tHFS_MIN_TIMESTAMP_SECS,\n+\t\tHFS_MIN_TIMESTAMP_SECS + 1,\n+\t\tTEST_BEFORE_UNIX_EPOCH_SECS,\n+\t\tTEST_UNIX_EPOCH_SECS,\n+\t\t1,\n+\t\tTEST_Y2000_SECS,\n+\t\tTEST_Y2026_SECS,\n+\t\t(s64)S32_MAX,\n+\t\tTEST_Y2038_SECS,\n+\t\tHFS_MAX_TIMESTAMP_SECS - 1,\n+\t\tHFS_MAX_TIMESTAMP_SECS,\n+\t};\n+\ttime64_t ut;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(values); i++) {\n+\t\tKUNIT_EXPECT_EQ_MSG(test, values[i],\n+\t\t\t\t    __hfsp_mt2ut(__hfsp_ut2mt(values[i])),\n+\t\t\t\t    \"round trip failed: index %d\", i);\n+\t}\n+\n+\t/* sweep the whole range with a large prime step */\n+\tfor (ut = HFS_MIN_TIMESTAMP_SECS; ut \u003c= HFS_MAX_TIMESTAMP_SECS;\n+\t     ut += 999983) {\n+\t\tif (__hfsp_mt2ut(__hfsp_ut2mt(ut)) != ut) {\n+\t\t\tKUNIT_FAIL(test, \"round trip failed: ut %lld\", ut);\n+\t\t\tbreak;\n+\t\t}\n+\t}\n+\n+\t/* every on-disk value maps back to itself */\n+\tfor (i = 0; i \u003c 32; i++) {\n+\t\tu32 raw = (u32)BIT_ULL(i);\n+\n+\t\tKUNIT_EXPECT_EQ(test, raw,\n+\t\t\t\tmt_raw(__hfsp_ut2mt(__hfsp_mt2ut(cpu_to_be32(raw)))));\n+\t}\n+}\n+\n+/* Test the decision to keep timestamp in xattr */\n+static void hfsplus_ut_needs_xattr_test(struct kunit *test)\n+{\n+\tKUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(HFS_MIN_TIMESTAMP_SECS));\n+\tKUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_BEFORE_UNIX_EPOCH_SECS));\n+\tKUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_UNIX_EPOCH_SECS));\n+\tKUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2026_SECS));\n+\tKUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2038_SECS));\n+\tKUNIT_EXPECT_FALSE(test,\n+\t\t\t   hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS - 1));\n+\n+\tKUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS));\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\t\t  hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS + 1));\n+\tKUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(TEST_Y2100_SECS));\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\t\t  hfsp_ut_needs_xattr(HFSPLUS_MAX_TIMESTAMP_SECS));\n+}\n+\n+/* Test detection of the extended timestamp mark */\n+static void hfsplus_mt_is_ext_timestamp_test(struct kunit *test)\n+{\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\thfsp_mt_is_ext_timestamp(HFSPLUS_EXT_TIMESTAMP_MARK));\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\thfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX)));\n+\n+\tKUNIT_EXPECT_FALSE(test, hfsp_mt_is_ext_timestamp(cpu_to_be32(0)));\n+\tKUNIT_EXPECT_FALSE(test,\n+\t\thfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX - 1)));\n+\tKUNIT_EXPECT_FALSE(test,\n+\t\thfsp_mt_is_ext_timestamp(cpu_to_be32(HFS_UTC_OFFSET)));\n+\n+\t/* every timestamp that needs xattr is stored as the mark */\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\thfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));\n+\tKUNIT_EXPECT_TRUE(test,\n+\t\thfsp_mt_is_ext_timestamp(__hfsp_ut2mt(TEST_Y2100_SECS)));\n+\tKUNIT_EXPECT_FALSE(test,\n+\t\thfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));\n+}\n+\n+static struct super_block *alloc_mock_sb(struct kunit *test)\n+{\n+\tstruct super_block *sb;\n+\tstruct hfsplus_sb_info *sbi;\n+\n+\tsb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, sb);\n+\n+\tsbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);\n+\tKUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);\n+\n+\t/* no attributes tree: xattr lookup returns -EOPNOTSUPP */\n+\tsbi-\u003eattr_tree = NULL;\n+\tsb-\u003es_fs_info = sbi;\n+\n+\treturn sb;\n+}\n+\n+/*\n+ * Test reading of the on-disk timestamp that doesn't need xattr.\n+ * The superblock must not be touched in such case.\n+ */\n+static void hfsplus_read_timestamp_regular_test(struct kunit *test)\n+{\n+\tstatic const time64_t values[] = {\n+\t\tHFS_MIN_TIMESTAMP_SECS,\n+\t\tTEST_BEFORE_UNIX_EPOCH_SECS,\n+\t\tTEST_UNIX_EPOCH_SECS,\n+\t\tTEST_Y2026_SECS,\n+\t\tHFS_MAX_TIMESTAMP_SECS - 1,\n+\t};\n+\ttime64_t ut;\n+\tbool has_xattr;\n+\tint err;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(values); i++) {\n+\t\tut = 0;\n+\t\thas_xattr = true;\n+\n+\t\terr = hfsplus_read_timestamp(NULL, HFSPLUS_FIRSTUSER_CNID,\n+\t\t\t\t\t     XATTR_LINUX_ACCESS_DATE_NAME,\n+\t\t\t\t\t     __hfsp_ut2mt(values[i]),\n+\t\t\t\t\t     \u0026ut, \u0026has_xattr);\n+\t\tKUNIT_EXPECT_EQ(test, 0, err);\n+\t\tKUNIT_EXPECT_EQ(test, values[i], ut);\n+\t\tKUNIT_EXPECT_FALSE(test, has_xattr);\n+\t}\n+}\n+\n+/*\n+ * Test reading of the extended timestamp mark without xattr\n+ * (for example, the record has been created by Mac OS X or\n+ * the volume has no attributes tree). The timestamp must be\n+ * HFS_MAX_TIMESTAMP_SECS without any error.\n+ */\n+static void hfsplus_read_timestamp_mark_no_xattr_test(struct kunit *test)\n+{\n+\tstruct super_block *sb = alloc_mock_sb(test);\n+\tstatic const char * const names[] = {\n+\t\tXATTR_LINUX_CREATE_DATE_NAME,\n+\t\tXATTR_LINUX_CONTENT_MOD_DATE_NAME,\n+\t\tXATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,\n+\t\tXATTR_LINUX_ACCESS_DATE_NAME,\n+\t};\n+\ttime64_t ut;\n+\tbool has_xattr;\n+\tint err;\n+\tint i;\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(names); i++) {\n+\t\tut = 0;\n+\t\thas_xattr = true;\n+\n+\t\terr = hfsplus_read_timestamp(sb, HFSPLUS_FIRSTUSER_CNID,\n+\t\t\t\t\t     names[i],\n+\t\t\t\t\t     HFSPLUS_EXT_TIMESTAMP_MARK,\n+\t\t\t\t\t     \u0026ut, \u0026has_xattr);\n+\t\tKUNIT_EXPECT_EQ(test, 0, err);\n+\t\tKUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS, ut);\n+\t\tKUNIT_EXPECT_FALSE(test, has_xattr);\n+\t}\n+}\n+\n+/* Test that names of internal xattrs fit the name buffer */\n+static void hfsplus_timestamp_xattr_names_test(struct kunit *test)\n+{\n+\tstatic const char * const names[] = {\n+\t\tXATTR_LINUX_CREATE_DATE_NAME,\n+\t\tXATTR_LINUX_CONTENT_MOD_DATE_NAME,\n+\t\tXATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,\n+\t\tXATTR_LINUX_ACCESS_DATE_NAME,\n+\t\tXATTR_LINUX_BACKUP_DATE_NAME,\n+\t};\n+\tint i;\n+\n+\tKUNIT_EXPECT_EQ(test, strlen(XATTR_LINUX_HFS_PREFIX),\n+\t\t\t(size_t)XATTR_LINUX_HFS_PREFIX_LEN);\n+\n+\tfor (i = 0; i \u003c ARRAY_SIZE(names); i++) {\n+\t\tsize_t len = XATTR_LINUX_HFS_PREFIX_LEN + strlen(names[i]);\n+\n+\t\tKUNIT_EXPECT_LT_MSG(test, len, (size_t)XATTR_LINUX_HFS_NAME_MAX,\n+\t\t\t\t    \"name %s is too long\", names[i]);\n+\t\tKUNIT_EXPECT_LE(test, len, (size_t)HFSPLUS_ATTR_MAX_STRLEN);\n+\t}\n+}\n+\n+static struct kunit_case hfsplus_time_test_cases[] = {\n+\tKUNIT_CASE(hfsplus_time_constants_test),\n+\tKUNIT_CASE(hfsplus_mt2ut_test),\n+\tKUNIT_CASE(hfsplus_ut2mt_test),\n+\tKUNIT_CASE(hfsplus_ut2mt_before_1970_test),\n+\tKUNIT_CASE(hfsplus_ut2mt_clamp_test),\n+\tKUNIT_CASE(hfsplus_time_roundtrip_test),\n+\tKUNIT_CASE(hfsplus_ut_needs_xattr_test),\n+\tKUNIT_CASE(hfsplus_mt_is_ext_timestamp_test),\n+\tKUNIT_CASE(hfsplus_read_timestamp_regular_test),\n+\tKUNIT_CASE(hfsplus_read_timestamp_mark_no_xattr_test),\n+\tKUNIT_CASE(hfsplus_timestamp_xattr_names_test),\n+\t{}\n+};\n+\n+static struct kunit_suite hfsplus_time_test_suite = {\n+\t.name = \"hfsplus_time\",\n+\t.test_cases = hfsplus_time_test_cases,\n+};\n+\n+kunit_test_suite(hfsplus_time_test_suite);\n+\n+MODULE_DESCRIPTION(\"KUnit tests for HFS+ date/time conversion\");\n+MODULE_LICENSE(\"GPL\");\n+MODULE_IMPORT_NS(\"EXPORTED_FOR_KUNIT_TESTING\");\ndiff --git a/fs/hfsplus/xattr.c b/fs/hfsplus/xattr.c\nindex 21a1c196c71f2..d3b1734496168 100644\n--- a/fs/hfsplus/xattr.c\n+++ b/fs/hfsplus/xattr.c\n@@ -50,6 +50,12 @@ static bool is_known_namespace(const char *name)\n \treturn true;\n }\n \n+static bool is_linux_hfs_internal_xattr(const char *name)\n+{\n+\treturn !strncmp(name, XATTR_LINUX_HFS_PREFIX,\n+\t\t\tXATTR_LINUX_HFS_PREFIX_LEN);\n+}\n+\n static u32 hfsplus_init_header_node(struct inode *attr_file,\n \t\t\t\t\tu32 clump_size,\n \t\t\t\t\tchar *buf, u16 node_size)\n@@ -319,6 +325,19 @@ static int hfsplus_create_attributes_file(struct super_block *sb)\n \n \thfsplus_mark_inode_dirty(attr_file, HFSPLUS_I_ATTR_DIRTY);\n \n+\t/*\n+\t * Store the fork of AttributesFile into the volume header right now.\n+\t * The AttributesFile can be created by hfsplus_cat_write_inode()\n+\t * (timestamps after 2040 are stored in xattrs) in the middle of\n+\t * sync operation. Then, the dirty AttributesFile's inode can be\n+\t * skipped by writeback and the volume header will be committed\n+\t * without AttributesFile. As a result, the AttributesFile will be\n+\t * not opened during the next mount.\n+\t */\n+\thfsplus_inode_write_fork(attr_file, \u0026sbi-\u003es_vhdr-\u003eattr_file);\n+\tset_bit(HFSPLUS_SB_WRITEBACKUP, \u0026sbi-\u003eflags);\n+\thfsplus_mark_mdb_dirty(sb);\n+\n \tsbi-\u003eattr_tree = hfs_btree_open(sb, HFSPLUS_ATTR_CNID);\n \tif (!sbi-\u003eattr_tree)\n \t\tpr_err(\"failed to load attributes file\\n\");\n@@ -339,8 +358,7 @@ static int hfsplus_create_attributes_file(struct super_block *sb)\n \treturn err;\n }\n \n-static inline\n-bool is_xattr_operation_supported(struct inode *inode)\n+static inline bool is_xattr_operation_supported(struct inode *inode)\n {\n \tif (HFSPLUS_IS_RSRC(inode))\n \t\treturn false;\n@@ -609,8 +627,9 @@ static ssize_t hfsplus_getxattr_finder_info(struct inode *inode,\n \treturn res;\n }\n \n-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n-\t\t\t void *value, size_t size)\n+static ssize_t __hfsplus_getxattr_nocheck(struct super_block *sb,\n+\t\t\t\t\t  u32 cnid, const char *name,\n+\t\t\t\t\t  void *value, size_t size)\n {\n \tstruct hfs_find_data fd;\n \thfsplus_attr_entry *entry;\n@@ -619,13 +638,7 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n \tu16 record_length = 0;\n \tssize_t res;\n \n-\tif (!is_xattr_operation_supported(inode))\n-\t\treturn -EOPNOTSUPP;\n-\n-\tif (!strcmp_xattr_finder_info(name))\n-\t\treturn hfsplus_getxattr_finder_info(inode, value, size);\n-\n-\tif (!HFSPLUS_SB(inode-\u003ei_sb)-\u003eattr_tree)\n+\tif (!HFSPLUS_SB(sb)-\u003eattr_tree)\n \t\treturn -EOPNOTSUPP;\n \n \tentry = hfsplus_alloc_attr_entry();\n@@ -634,18 +647,18 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n \t\treturn -ENOMEM;\n \t}\n \n-\tres = hfs_find_init(HFSPLUS_SB(inode-\u003ei_sb)-\u003eattr_tree, \u0026fd);\n+\tres = hfs_find_init(HFSPLUS_SB(sb)-\u003eattr_tree, \u0026fd);\n \tif (res) {\n \t\tpr_err(\"can't init xattr find struct\\n\");\n \t\tgoto failed_getxattr_init;\n \t}\n \n-\tres = hfsplus_find_attr(inode-\u003ei_sb, inode-\u003ei_ino, name, \u0026fd);\n+\tres = hfsplus_find_attr(sb, cnid, name, \u0026fd);\n \tif (res) {\n \t\tif (res == -ENOENT || res == -ENODATA)\n \t\t\tres = -ENODATA;\n \t\telse\n-\t\t\tpr_err(\"xattr search failed\\n\");\n+\t\t\tpr_err(\"xattr searching failed\\n\");\n \t\tgoto out;\n \t}\n \n@@ -693,6 +706,38 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n \treturn res;\n }\n \n+static inline ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n+\t\t\t\t\t void *value, size_t size)\n+{\n+\tif (!is_xattr_operation_supported(inode))\n+\t\treturn -EOPNOTSUPP;\n+\n+\tif (!strcmp_xattr_finder_info(name))\n+\t\treturn hfsplus_getxattr_finder_info(inode, value, size);\n+\n+\treturn __hfsplus_getxattr_nocheck(inode-\u003ei_sb, (u32)inode-\u003ei_ino,\n+\t\t\t\t\t  name, value, size);\n+}\n+\n+static inline char *hfsplus_build_xattr_name(const char *name,\n+\t\t\t\t\t     const char *prefix,\n+\t\t\t\t\t     size_t prefixlen)\n+{\n+\tchar *xattr_name;\n+\tsize_t len = NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1;\n+\n+\txattr_name = kzalloc(len, GFP_KERNEL);\n+\tif (!xattr_name)\n+\t\treturn ERR_PTR(-ENOMEM);\n+\n+\tif (snprintf(xattr_name, len, \"%s%s\", prefix, name) \u003e= len) {\n+\t\tkfree(xattr_name);\n+\t\treturn ERR_PTR(-ENAMETOOLONG);\n+\t}\n+\n+\treturn xattr_name;\n+}\n+\n ssize_t hfsplus_getxattr(struct inode *inode, const char *name,\n \t\t\t void *value, size_t size,\n \t\t\t const char *prefix, size_t prefixlen)\n@@ -704,13 +749,9 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,\n \t\tinode-\u003ei_ino, name ? name : NULL,\n \t\tprefix ? prefix : NULL);\n \n-\txattr_name = kmalloc(NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1,\n-\t\t\t     GFP_KERNEL);\n-\tif (!xattr_name)\n-\t\treturn -ENOMEM;\n-\n-\tstrcpy(xattr_name, prefix);\n-\tstrcpy(xattr_name + prefixlen, name);\n+\txattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);\n+\tif (IS_ERR(xattr_name))\n+\t\treturn PTR_ERR(xattr_name);\n \n \tres = __hfsplus_getxattr(inode, xattr_name, value, size);\n \tkfree(xattr_name);\n@@ -718,7 +759,114 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,\n \thfs_dbg(\"finished: res %d\\n\", res);\n \n \treturn res;\n+}\n+\n+static ssize_t hfsplus_getxattr_nocheck(struct super_block *sb, u32 cnid,\n+\t\t\t\t\tconst char *name,\n+\t\t\t\t\tvoid *value, size_t size,\n+\t\t\t\t\tconst char *prefix, size_t prefixlen)\n+{\n+\tint res;\n+\tchar *xattr_name;\n \n+\thfs_dbg(\"cnid %u, name %s, prefix %s\\n\",\n+\t\tcnid, name ? name : NULL,\n+\t\tprefix ? prefix : NULL);\n+\n+\txattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);\n+\tif (IS_ERR(xattr_name))\n+\t\treturn PTR_ERR(xattr_name);\n+\n+\tres = __hfsplus_getxattr_nocheck(sb, cnid, xattr_name, value, size);\n+\tkfree(xattr_name);\n+\n+\thfs_dbg(\"finished: res %d\\n\", res);\n+\n+\treturn res;\n+}\n+\n+/*\n+ * Get the timestamp stored in the internal xattr @name\n+ * of catalog record @cnid. The caller can hold the catalog tree lock.\n+ */\n+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,\n+\t\t\t\tconst char *name, time64_t *ts)\n+{\n+\t__be64 value;\n+\tssize_t res;\n+\n+\tres = hfsplus_getxattr_nocheck(sb, cnid, name,\n+\t\t\t\t\t\u0026value, sizeof(value),\n+\t\t\t\t\tXATTR_LINUX_HFS_PREFIX,\n+\t\t\t\t\tXATTR_LINUX_HFS_PREFIX_LEN);\n+\tif (res \u003c 0)\n+\t\treturn res;\n+\n+\tif (res != sizeof(value))\n+\t\treturn -EIO;\n+\n+\t*ts = (time64_t)be64_to_cpu(value);\n+\treturn 0;\n+}\n+\n+/*\n+ * Store the timestamp @ts into the internal xattr @name of catalog\n+ * record @cnid. The caller must hold the catalog tree lock and it is\n+ * caller's responsibility to set HFSPLUS_XATTR_EXISTS flag in\n+ * the catalog record.\n+ */\n+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,\n+\t\t\t\tconst char *name, time64_t ts)\n+{\n+\tstruct super_block *sb = inode-\u003ei_sb;\n+\tchar xattr_name[XATTR_LINUX_HFS_NAME_MAX];\n+\t__be64 value = cpu_to_be64(ts);\n+\tint err;\n+\n+\thfs_dbg(\"ino %llu, cnid %u, name %s, ts %lld\\n\",\n+\t\tinode-\u003ei_ino, cnid, name, ts);\n+\n+\tif (!HFSPLUS_SB(sb)-\u003eattr_tree) {\n+\t\terr = hfsplus_create_attributes_file(sb);\n+\t\tif (unlikely(err))\n+\t\t\treturn err;\n+\n+\t\tif (!HFSPLUS_SB(sb)-\u003eattr_tree)\n+\t\t\treturn -EIO;\n+\t}\n+\n+\tif (snprintf(xattr_name, sizeof(xattr_name), \"%s%s\",\n+\t\t     XATTR_LINUX_HFS_PREFIX, name) \u003e= sizeof(xattr_name))\n+\t\treturn -ENAMETOOLONG;\n+\n+\treturn hfsplus_set_attr_cnid(inode, cnid, xattr_name,\n+\t\t\t\t     \u0026value, sizeof(value));\n+}\n+\n+/*\n+ * Remove the internal xattr @name of @inode. The caller must hold\n+ * the catalog tree lock and it is caller's responsibility to update\n+ * HFSPLUS_XATTR_EXISTS flag in the catalog record.\n+ */\n+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name)\n+{\n+\tchar xattr_name[XATTR_LINUX_HFS_NAME_MAX];\n+\tint err;\n+\n+\thfs_dbg(\"ino %llu, name %s\\n\", inode-\u003ei_ino, name);\n+\n+\tif (!HFSPLUS_SB(inode-\u003ei_sb)-\u003eattr_tree)\n+\t\treturn 0;\n+\n+\tif (snprintf(xattr_name, sizeof(xattr_name), \"%s%s\",\n+\t\t     XATTR_LINUX_HFS_PREFIX, name) \u003e= sizeof(xattr_name))\n+\t\treturn -ENAMETOOLONG;\n+\n+\terr = hfsplus_delete_attr(inode, xattr_name);\n+\tif (err == -ENOENT || err == -ENODATA)\n+\t\terr = 0;\n+\n+\treturn err;\n }\n \n static inline int can_list(const char *xattr_name)\n@@ -726,6 +874,10 @@ static inline int can_list(const char *xattr_name)\n \tif (!xattr_name)\n \t\treturn 0;\n \n+\t/* Internal xattrs of the driver are hidden */\n+\tif (is_linux_hfs_internal_xattr(xattr_name))\n+\t\treturn 0;\n+\n \treturn strncmp(xattr_name, XATTR_TRUSTED_PREFIX,\n \t\t\tXATTR_TRUSTED_PREFIX_LEN) ||\n \t\t\t\tcapable(CAP_SYS_ADMIN);\n@@ -998,6 +1150,10 @@ static int hfsplus_osx_getxattr(const struct xattr_handler *handler,\n \tif (is_known_namespace(name))\n \t\treturn -EOPNOTSUPP;\n \n+\t/* Internal xattrs of the driver are hidden */\n+\tif (is_linux_hfs_internal_xattr(name))\n+\t\treturn -EOPNOTSUPP;\n+\n \t/*\n \t * osx is the namespace we use to indicate an unprefixed\n \t * attribute on the filesystem (like the ones that OS X\n@@ -1020,6 +1176,10 @@ static int hfsplus_osx_setxattr(const struct xattr_handler *handler,\n \tif (is_known_namespace(name))\n \t\treturn -EOPNOTSUPP;\n \n+\t/* Internal xattrs of the driver cannot be changed by user */\n+\tif (is_linux_hfs_internal_xattr(name))\n+\t\treturn -EOPNOTSUPP;\n+\n \t/*\n \t * osx is the namespace we use to indicate an unprefixed\n \t * attribute on the filesystem (like the ones that OS X\ndiff --git a/fs/hfsplus/xattr.h b/fs/hfsplus/xattr.h\nindex 15cc55e414100..8f6283f85f133 100644\n--- a/fs/hfsplus/xattr.h\n+++ b/fs/hfsplus/xattr.h\n@@ -12,6 +12,24 @@\n \n #include \u003clinux/xattr.h\u003e\n \n+/*\n+ * Internal xattrs of Linux HFS+ driver. Timestamps beyond\n+ * HFS_MAX_TIMESTAMP_SECS (February 2040) cannot be represented by\n+ * 32-bit on-disk fields. Such timestamp is stored as the maximal value\n+ * (HFSPLUS_EXT_TIMESTAMP_MARK) in the catalog record and the real value\n+ * is kept in the xattr as big-endian 64-bit seconds since\n+ * 00:00 GMT, Jan. 1, 1970.\n+ */\n+#define XATTR_LINUX_HFS_PREFIX \"linux_hfs.\"\n+#define XATTR_LINUX_HFS_PREFIX_LEN \\\n+\t(sizeof(XATTR_LINUX_HFS_PREFIX) - 1)\n+#define XATTR_LINUX_HFS_NAME_MAX\t64\n+#define XATTR_LINUX_CREATE_DATE_NAME \"create_date_u64\"\n+#define XATTR_LINUX_CONTENT_MOD_DATE_NAME \"content_mod_date_u64\"\n+#define XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME \"attribute_mod_date_u64\"\n+#define XATTR_LINUX_ACCESS_DATE_NAME \"access_date_u64\"\n+#define XATTR_LINUX_BACKUP_DATE_NAME \"backup_date_u64\"\n+\n extern const struct xattr_handler hfsplus_xattr_osx_handler;\n extern const struct xattr_handler hfsplus_xattr_user_handler;\n extern const struct xattr_handler hfsplus_xattr_trusted_handler;\n@@ -23,16 +41,18 @@ int __hfsplus_setxattr(struct inode *inode, const char *name,\n \t\t\tconst void *value, size_t size, int flags);\n \n int hfsplus_setxattr(struct inode *inode, const char *name,\n-\t\t\t\t   const void *value, size_t size, int flags,\n-\t\t\t\t   const char *prefix, size_t prefixlen);\n-\n-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n-\t\t\t   void *value, size_t size);\n-\n+\t\t     const void *value, size_t size, int flags,\n+\t\t     const char *prefix, size_t prefixlen);\n ssize_t hfsplus_getxattr(struct inode *inode, const char *name,\n \t\t\t void *value, size_t size,\n \t\t\t const char *prefix, size_t prefixlen);\n \n+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,\n+\t\t\t\tconst char *name, time64_t *ts);\n+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,\n+\t\t\t\tconst char *name, time64_t ts);\n+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name);\n+\n ssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size);\n \n int hfsplus_init_security(struct inode *inode, struct inode *dir,\ndiff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h\nindex d6a615e74b26e..d4e91dea23628 100644\n--- a/include/linux/hfs_common.h\n+++ b/include/linux/hfs_common.h\n@@ -667,4 +667,17 @@ typedef union {\n \tstruct hfsplus_attr_key attr;\n } __packed hfsplus_btree_key;\n \n+/*\n+ * There are two time systems.  Both are based on seconds since\n+ * a particular time/date.\n+ *\tUnix:\tsigned little-endian since 00:00 GMT, Jan. 1, 1970\n+ *\tmac:\tunsigned big-endian since 00:00 GMT, Jan. 1, 1904\n+ */\n+#define HFS_UTC_OFFSET 2082844800U\n+\n+/* January 1, 1904, 00:00:00 UTC */\n+#define HFS_MIN_TIMESTAMP_SECS\t\t-2082844800LL\n+/* February 6, 2040, 06:28:15 UTC */\n+#define HFS_MAX_TIMESTAMP_SECS\t\t2212122495LL\n+\n #endif /* _HFS_COMMON_H_ */\n"
}

2/1 2026/10/10 00:10 agent patch-evaluator
1m Model:
core

Args:
null

Results:
{
  "EnableConfigs": [
    "HFSPLUS_FS"
  ],
  "FocusSymbols": [
    "hfsplus_read_timestamp",
    "hfsplus_get_timestamp_xattr",
    "hfsplus_set_timestamp_xattr",
    "hfsplus_remove_timestamp_xattr",
    "hfsplus_set_attr_cnid",
    "hfsplus_cat_write_inode",
    "hfsplus_cat_read_inode",
    "hfsplus_create_cat"
  ],
  "Reasoning": "The patch implements an extension in the HFS+ filesystem to support timestamps beyond February 2040 by storing 64-bit timestamps in internal extended attributes ('linux_hfs.*'). This adds new logic for catalog record creation, inode read/write operations, and btree attribute management. The code is fully reachable via standard filesystem syscalls (mount, creat, utimes/futimens, statx) on loopback HFS+ images in virtualized environments, making it a good candidate for fuzzing to catch btree inconsistencies, locking issues, or edge cases in timestamp conversion and xattr handling.",
  "WorthFuzzing": true
}

Instruction:
You are an expert Linux kernel maintainer and security engineer.
Your job is to review a provided patch series and evaluate whether it warrants fuzzing with syzkaller.

IMPORTANT: The changes have ALREADY been applied and committed as the HEAD commit in
your workspace. Do NOT rely on internal assumptions. You must actively use your code access
tools to inspect the actual source code, callers, and surrounding context.

================================================================================
1. CORE TRIAGE PHILOSOPHY
================================================================================
The goal of patch fuzzing is to discover crashes, regressions, exposed latent bugs,
and newly triggered assertions introduced by the patch series.

- REACHABILITY IS THE PRIMARY GATE:
  Fuzzing can only discover bugs in code that can actually execute in standard virtualized
  environments (GCE or QEMU, utilizing software-emulated devices like USB gadgets, netdev, tun/tap).
  If the modified code is structurally unreachable (see Section 2), it MUST NOT be fuzzed,
  regardless of whether it adds assertions or complex logic.

- DO NOT BLINDLY TRUST "NO FUNCTIONAL CHANGE" (NFCI) OR "REFACTORING" CLAIMS:
  Patch authors routinely label changes as "cleanups", "refactorings", or state
  "No functional change intended". Do NOT take these claims at face value.
  Code refactorings that rearrange logic, introduce helper functions, or alter state management
  in core subsystems frequently introduce subtle semantic shifts or uncover latent kernel bugs.
  If reachable executable code is modified or refactored, it MUST be fuzzed.

- NEW OR MODIFIED ASSERTIONS IN REACHABLE CODE MUST BE FUZZED:
  When a patch introduces or modifies runtime checks or assertions (e.g., WARN_ON*, VM_WARN_ON*,
  BUG_ON*, lockdep_assert*) in reachable code paths, it enforces new or stricter invariants.
  Even if the author believes the invariant always holds, fuzzing is essential to verify whether
  an unusual sequence of operations can violate it.

================================================================================
2. WHEN TO RETURN WorthFuzzing=false (NEGATIVE CRITERIA)
================================================================================
Return WorthFuzzing=false ONLY IF all modified code falls strictly into one or more of these categories:

- Non-kernel and non-executable changes:
  * Modifications to Documentation/, comments, or spelling fixes.
  * User-space directories, self-tests, samples, or scripts (e.g., tools/, samples/, scripts/, usr/)
    that do not affect the compiled kernel image (vmlinux) or kernel modules.
  * Purely decorative logging (e.g., message strings in pr_err, printk, dev_info) or tracepoints
    that do not alter control flow or data structures.
  * Build system or Kconfig changes that do not alter compiled C logic.
- Structurally unreachable hardware:
  * Vendor-specific PCIe switches, SmartNICs, or GPU drivers (e.g., mlxsw, pds_core, qed,
    ionic, amdgpu) requiring physical ASIC/PCIe cards not emulated in standard QEMU.
- Unreachable execution paths:
  * Driver teardown callbacks (.remove, .shutdown, pci_unregister_driver) executed only during
    physical PCI hot-unplug or manual sysfs driver unbinding.
  * Code paths exclusive to architectures other than the target architecture.

================================================================================
3. WHEN TO RETURN WorthFuzzing=true (POSITIVE CRITERIA)
================================================================================
Return WorthFuzzing=true whenever the patch touches reachable executable code, including:
- Core Subsystems:
  * Any logic modifications in memory management (mm/), synchronization/locking (kernel/locking/),
    BPF, scheduler, core networking, VFS, or syscall handling.
- Refactorings and Code Cleanups:
  * Any restructuring of reachable data structures, helper abstractions, or algorithm flows.
- Runtime Assertions and Defensive Checks:
  * Any introduction or alteration of assertions (WARN_ON*, VM_WARN_ON*, BUG_ON*, etc.) in reachable paths.
- Reachable Drivers and Protocols:
  * Drivers accessible via virtual buses (virtio, USB gadget, loopback, netlink, binder, sockets, etc.).

================================================================================
4. EXTRACTING FocusSymbols (PREVENTING DILUTION)
================================================================================
When WorthFuzzing=true, you must extract specific kernel functions into FocusSymbols to guide the fuzzer:

- AVOID UBIQUITOUS LIFECYCLE HOT-PATHS:
  Do NOT list generic, ubiquitous functions called by almost every program in the corpus
  (including, but not limited to: general memory allocators and deallocators, page fault
  and trap handlers, or core synchronization primitives; this is not an exhaustive list).
  Listing ubiquitous functions causes the fuzzer to classify thousands of unrelated tests as "focused",
  which severely dilutes fuzzing effort away from the actual changes.

- TARGET SPECIFIC FEATURE LOGIC AND ENTRYPOINTS:
  List functions that specifically implement the logic being added or altered, or direct API entrypoints
  for the subsystem feature under review.

- HANDLING STATIC INLINE FUNCTIONS IN HEADERS (.h):
  Compiler-inlined static functions (such as static inlines in mm/*.h or include/linux/*.h) lack
  distinct symbol addresses in vmlinux and cannot be targeted directly by symbol coverage filters.
  If the changes are primarily in static inline helpers, identify non-static, feature-specific caller
  functions in .c files that exercise them (avoiding ubiquitous lifecycle wrappers).

================================================================================
5. IDENTIFYING EnableConfigs
================================================================================
Identify any specific CONFIG_ options required to properly compile and reach the modified code:
- Inspect Kconfig files and #ifdef guards; do not make assumptions.
- Check "depends on" lines in Kconfig to include any non-standard parent subsystem configs needed.
- Strip any 'CONFIG_' prefix (e.g., return "NET_IPV4" instead of "CONFIG_NET_IPV4").
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 9a5a9dae0ad59d6f64ca490471bca3aebb1151b4
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Oct 10 00:10:33 2026 +0000

    syz-cluster: applied patch under review

diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h
index e250f87a5e336..659b0ba733d49 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/Makefile b/fs/hfsplus/Makefile
index f2a9ae697e811..94a262ae91776 100644
--- a/fs/hfsplus/Makefile
+++ b/fs/hfsplus/Makefile
@@ -10,4 +10,4 @@ hfsplus-objs := super.o options.o inode.o ioctl.o extents.o catalog.o dir.o btre
 		attributes.o xattr.o xattr_user.o xattr_security.o xattr_trusted.o
 
 # KUnit tests
-obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o
+obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o time_test.o
diff --git a/fs/hfsplus/attributes.c b/fs/hfsplus/attributes.c
index 7c2e589d45538..51b18d780f2c0 100644
--- a/fs/hfsplus/attributes.c
+++ b/fs/hfsplus/attributes.c
@@ -206,7 +206,8 @@ int hfsplus_attr_exists(struct inode *inode, const char *name)
 }
 
 static
-int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
+int hfsplus_create_attr_nolock(struct inode *inode, u32 cnid,
+				const char *name,
 				const void *value, size_t size,
 				struct hfs_find_data *fd,
 				hfsplus_attr_entry *entry_ptr)
@@ -215,12 +216,12 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
 	int entry_size;
 	int err;
 
-	hfs_dbg("name %s, ino %llu\n",
-		name ? name : NULL, inode->i_ino);
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
 
 	if (name) {
 		err = hfsplus_attr_build_key(sb, fd->search_key,
-						inode->i_ino, name);
+						cnid, name);
 		if (err)
 			return err;
 	} else
@@ -229,7 +230,7 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
 	/* Mac OS X supports only inline data attributes. */
 	entry_size = hfsplus_attr_build_record(entry_ptr,
 					HFSPLUS_ATTR_INLINE_DATA,
-					inode->i_ino,
+					cnid,
 					value, size);
 	if (entry_size == HFSPLUS_INVALID_ATTR_RECORD) {
 		if (size > HFSPLUS_MAX_INLINE_DATA_SIZE)
@@ -289,8 +290,8 @@ int hfsplus_create_attr(struct inode *inode,
 	if (err)
 		goto failed_create_attr;
 
-	err = hfsplus_create_attr_nolock(inode, name, value, size,
-					 &fd, entry_ptr);
+	err = hfsplus_create_attr_nolock(inode, (u32)inode->i_ino, name,
+					 value, size, &fd, entry_ptr);
 	if (err)
 		goto failed_create_attr;
 
@@ -347,18 +348,19 @@ static int __hfsplus_delete_attr(struct inode *inode, u32 cnid,
 }
 
 static
-int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,
+int hfsplus_delete_attr_nolock(struct inode *inode, u32 cnid,
+				const char *name,
 				struct hfs_find_data *fd)
 {
 	struct super_block *sb = inode->i_sb;
 	int err;
 
-	hfs_dbg("name %s, ino %llu\n",
-		name ? name : NULL, inode->i_ino);
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
 
 	if (name) {
 		err = hfsplus_attr_build_key(sb, fd->search_key,
-						inode->i_ino, name);
+						cnid, name);
 		if (err)
 			return err;
 	} else {
@@ -373,7 +375,7 @@ int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,
 	} else if (err)
 		return err;
 
-	err = __hfsplus_delete_attr(inode, inode->i_ino, fd);
+	err = __hfsplus_delete_attr(inode, cnid, fd);
 	if (err)
 		return err;
 
@@ -403,7 +405,7 @@ int hfsplus_delete_attr(struct inode *inode, const char *name)
 	if (err)
 		goto out;
 
-	err = hfsplus_delete_attr_nolock(inode, name, &fd);
+	err = hfsplus_delete_attr_nolock(inode, (u32)inode->i_ino, name, &fd);
 	if (err)
 		goto out;
 
@@ -481,12 +483,12 @@ int hfsplus_replace_attr(struct inode *inode,
 	if (err)
 		goto failed_replace_attr;
 
-	err = hfsplus_delete_attr_nolock(inode, name, &fd);
+	err = hfsplus_delete_attr_nolock(inode, (u32)inode->i_ino, name, &fd);
 	if (err)
 		goto failed_replace_attr;
 
-	err = hfsplus_create_attr_nolock(inode, name, value, size,
-					 &fd, entry_ptr);
+	err = hfsplus_create_attr_nolock(inode, (u32)inode->i_ino, name,
+					 value, size, &fd, entry_ptr);
 	if (err)
 		goto failed_replace_attr;
 
@@ -497,3 +499,54 @@ int hfsplus_replace_attr(struct inode *inode,
 	hfsplus_destroy_attr_entry(entry_ptr);
 	return err;
 }
+
+/*
+ * Create or replace the xattr @name of catalog record @cnid.
+ * The @inode is used only for access to the superblock and for
+ * marking the attributes tree as dirty. It is the caller's
+ * responsibility to set HFSPLUS_XATTR_EXISTS in the catalog record.
+ */
+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,
+			  const char *name,
+			  const void *value, size_t size)
+{
+	struct super_block *sb = inode->i_sb;
+	struct hfs_find_data fd;
+	hfsplus_attr_entry *entry_ptr;
+	int err = 0;
+
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
+
+	if (!HFSPLUS_SB(sb)->attr_tree) {
+		pr_err("attributes file doesn't exist\n");
+		return -EINVAL;
+	}
+
+	entry_ptr = hfsplus_alloc_attr_entry();
+	if (!entry_ptr)
+		return -ENOMEM;
+
+	err = hfs_find_init(HFSPLUS_SB(sb)->attr_tree, &fd);
+	if (err)
+		goto failed_init_set_attr;
+
+	/* Fail early and avoid ENOSPC during the btree operation */
+	err = hfs_bmap_reserve(fd.tree, fd.tree->depth + 1);
+	if (err)
+		goto failed_set_attr;
+
+	err = hfsplus_delete_attr_nolock(inode, cnid, name, &fd);
+	if (err && err != -ENOENT && err != -ENODATA)
+		goto failed_set_attr;
+
+	err = hfsplus_create_attr_nolock(inode, cnid, name,
+					 value, size, &fd, entry_ptr);
+
+failed_set_attr:
+	hfs_find_exit(&fd);
+
+failed_init_set_attr:
+	hfsplus_destroy_attr_entry(entry_ptr);
+	return err;
+}
diff --git a/fs/hfsplus/catalog.c b/fs/hfsplus/catalog.c
index fe11c4b6dd997..d710419db3591 100644
--- a/fs/hfsplus/catalog.c
+++ b/fs/hfsplus/catalog.c
@@ -12,6 +12,7 @@
 
 #include "hfsplus_fs.h"
 #include "hfsplus_raw.h"
+#include "xattr.h"
 
 int hfsplus_cat_case_cmp_key(const hfsplus_btree_key *k1,
 			     const hfsplus_btree_key *k2)
@@ -102,10 +103,63 @@ void hfsplus_cat_set_perms(struct inode *inode, struct hfsplus_perm *perms)
 		perms->dev = 0;
 }
 
+static void hfsplus_copy_timestamps2folder(struct hfsplus_timestamps *timestamps,
+					    struct hfsplus_cat_folder *folder)
+{
+	folder->create_date = timestamps->create_date;
+	folder->content_mod_date = timestamps->content_mod_date;
+	folder->attribute_mod_date = timestamps->attribute_mod_date;
+	folder->access_date = timestamps->access_date;
+}
+
+static void hfsplus_copy_timestamps2file(struct hfsplus_timestamps *timestamps,
+					  struct hfsplus_cat_file *file)
+{
+	file->create_date = timestamps->create_date;
+	file->content_mod_date = timestamps->content_mod_date;
+	file->attribute_mod_date = timestamps->attribute_mod_date;
+	file->access_date = timestamps->access_date;
+}
+
+/*
+ * Set creation date of new inode. The creation date after February 2040
+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK in the catalog record. The real
+ * value is saved into internal xattr by hfsplus_create_cat().
+ */
+static void hfsplus_set_create_date(struct inode *inode,
+				     struct hfsplus_timestamps *timestamps)
+{
+	time64_t ut = ktime_get_real_seconds();
+	__be32 create_date = __hfsp_ut2mt(ut);
+
+	HFSPLUS_I(inode)->birthdate = ut;
+	HFSPLUS_I(inode)->create_date = create_date;
+
+	timestamps->create_date = create_date;
+	timestamps->content_mod_date = create_date;
+	timestamps->attribute_mod_date = create_date;
+	timestamps->access_date = create_date;
+}
+
+/*
+ * Real creation date of new catalog record. The hard link record
+ * inherits the creation date of the hidden directory.
+ */
+static time64_t hfsplus_cat_record_birthdate(u32 cnid, struct inode *inode)
+{
+	struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb);
+
+	if (!S_ISDIR(inode->i_mode) && cnid != inode->i_ino)
+		return HFSPLUS_I(sbi->hidden_dir)->birthdate;
+
+	return HFSPLUS_I(inode)->birthdate;
+}
+
 static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
-		u32 cnid, struct inode *inode)
+				    u32 cnid, struct inode *inode)
 {
 	struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb);
+	struct hfsplus_timestamps timestamps = {0};
 
 	if (S_ISDIR(inode->i_mode)) {
 		struct hfsplus_cat_folder *folder;
@@ -116,11 +170,12 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 		if (test_bit(HFSPLUS_SB_HFSX, &sbi->flags))
 			folder->flags |= cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT);
 		folder->id = cpu_to_be32(inode->i_ino);
-		HFSPLUS_I(inode)->create_date =
-			folder->create_date =
-			folder->content_mod_date =
-			folder->attribute_mod_date =
-			folder->access_date = hfsp_now2mt();
+
+		hfsplus_set_create_date(inode, &timestamps);
+		hfsplus_copy_timestamps2folder(&timestamps, folder);
+		if (hfsp_ut_needs_xattr(HFSPLUS_I(inode)->birthdate))
+			folder->flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+
 		hfsplus_cat_set_perms(inode, &folder->permissions);
 		if (inode == sbi->hidden_dir)
 			/* invisible and namelocked */
@@ -134,11 +189,23 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 		file->type = cpu_to_be16(HFSPLUS_FILE);
 		file->flags = cpu_to_be16(HFSPLUS_FILE_THREAD_EXISTS);
 		file->id = cpu_to_be32(cnid);
-		HFSPLUS_I(inode)->create_date =
-			file->create_date =
-			file->content_mod_date =
-			file->attribute_mod_date =
-			file->access_date = hfsp_now2mt();
+
+		if (cnid == inode->i_ino) {
+			hfsplus_set_create_date(inode, &timestamps);
+		} else {
+			/*
+			 * Hard link record: don't change creation date
+			 * of the linked inode.
+			 */
+			__be32 now = __hfsp_ut2mt(ktime_get_real_seconds());
+
+			timestamps.create_date = now;
+			timestamps.content_mod_date = now;
+			timestamps.attribute_mod_date = now;
+			timestamps.access_date = now;
+		}
+		hfsplus_copy_timestamps2file(&timestamps, file);
+
 		if (cnid == inode->i_ino) {
 			hfsplus_cat_set_perms(inode, &file->permissions);
 			if (S_ISLNK(inode->i_mode)) {
@@ -169,6 +236,10 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 			file->permissions.dev =
 				cpu_to_be32(HFSPLUS_I(inode)->linkid);
 		}
+
+		if (hfsp_ut_needs_xattr(hfsplus_cat_record_birthdate(cnid, inode)))
+			file->flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+
 		return sizeof(*file);
 	}
 }
@@ -256,6 +327,7 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	struct super_block *sb = dir->i_sb;
 	struct hfs_find_data fd;
 	hfsplus_cat_entry entry;
+	time64_t birthdate;
 	int entry_size;
 	int err;
 
@@ -309,6 +381,22 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	if (err)
 		goto err1;
 
+	birthdate = hfsplus_cat_record_birthdate(cnid, inode);
+	if (hfsp_ut_needs_xattr(birthdate)) {
+		/*
+		 * Failure is not critical: the creation date
+		 * will be read as HFS_MAX_TIMESTAMP_SECS.
+		 */
+		err = hfsplus_set_timestamp_xattr(inode, cnid,
+						  XATTR_LINUX_CREATE_DATE_NAME,
+						  birthdate);
+		if (err) {
+			pr_warn("fail to save creation date: cnid %u, err %d\n",
+				cnid, err);
+			err = 0;
+		}
+	}
+
 	dir->i_size++;
 	if (S_ISDIR(inode->i_mode))
 		hfsplus_subfolders_inc(dir);
diff --git a/fs/hfsplus/dir.c b/fs/hfsplus/dir.c
index 51fcba2e6d403..af0520ebced70 100644
--- a/fs/hfsplus/dir.c
+++ b/fs/hfsplus/dir.c
@@ -26,6 +26,43 @@ static inline void hfsplus_instantiate(struct dentry *dentry,
 	d_instantiate(dentry, inode);
 }
 
+/*
+ * Check that creation date of catalog record @cnid is identical to
+ * creation date of @inode. If the creation date is after February 2040,
+ * then the real value is compared by means of internal xattr.
+ */
+static inline bool is_create_date_identical(struct inode *inode,
+					    u32 cnid, __be32 create_date)
+{
+	time64_t timestamp;
+	bool has_xattr;
+	int err;
+
+	if (create_date != HFSPLUS_I(inode)->create_date)
+		return false;
+
+	if (!hfsp_mt_is_ext_timestamp(create_date))
+		return true;
+
+	err = hfsplus_read_timestamp(inode->i_sb, cnid,
+				     XATTR_LINUX_CREATE_DATE_NAME,
+				     create_date, &timestamp, &has_xattr);
+	if (err) {
+		pr_err("timestamp extraction failure: cnid %u, err %d\n",
+			cnid, err);
+		return false;
+	}
+
+	/*
+	 * The record without xattr (for example, created by Mac OS X)
+	 * cannot be distinguished by means of 64-bit timestamp.
+	 */
+	if (!has_xattr)
+		return true;
+
+	return timestamp == HFSPLUS_I(inode)->birthdate;
+}
+
 /* Find the entry inside dir named dentry->d_name */
 static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,
 				     unsigned int flags)
@@ -78,12 +115,12 @@ static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,
 				entry.file.user_info.fdCreator ==
 				cpu_to_be32(HFSP_HFSPLUS_CREATOR) &&
 				HFSPLUS_SB(sb)->hidden_dir &&
-				(entry.file.create_date ==
-					HFSPLUS_I(HFSPLUS_SB(sb)->hidden_dir)->
-						create_date ||
-				entry.file.create_date ==
-					HFSPLUS_I(d_inode(sb->s_root))->
-						create_date)) {
+				(is_create_date_identical(
+					HFSPLUS_SB(sb)->hidden_dir,
+					cnid, entry.file.create_date) ||
+				is_create_date_identical(
+					d_inode(sb->s_root),
+					cnid, entry.file.create_date))) {
 			struct qstr str;
 			char name[32];
 
diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h
index 1e5b58e6a13fc..637bba65568ec 100644
--- a/fs/hfsplus/hfsplus_fs.h
+++ b/fs/hfsplus/hfsplus_fs.h
@@ -175,6 +175,13 @@ static inline struct hfsplus_sb_info *HFSPLUS_SB(struct super_block *sb)
 	return sb->s_fs_info;
 }
 
+struct hfsplus_timestamps {
+	__be32 create_date;
+	__be32 content_mod_date;
+	__be32 attribute_mod_date;
+	__be32 access_date;
+	__be32 backup_date;
+};
 
 struct hfsplus_inode_info {
 	atomic_t opencnt;
@@ -196,7 +203,8 @@ struct hfsplus_inode_info {
 	 * Immutable data.
 	 */
 	struct inode *rsrc_inode;
-	__be32 create_date;
+	__be32 create_date;	/* on-disk (maybe clamped) creation date */
+	time64_t birthdate;	/* real creation date (seconds since 1970) */
 
 	/*
 	 * Protected by sbi->vh_mutex.
@@ -227,6 +235,9 @@ struct hfsplus_inode_info {
 #define HFSPLUS_I_EXT_DIRTY	2	/* has changes in the extent tree */
 #define HFSPLUS_I_ALLOC_DIRTY	3	/* has changes in the allocation file */
 #define HFSPLUS_I_ATTR_DIRTY	4	/* has changes in the attributes tree */
+#define HFSPLUS_I_ATIME_XATTR	5	/* access date is stored in xattr */
+#define HFSPLUS_I_MTIME_XATTR	6	/* content mod date is stored in xattr */
+#define HFSPLUS_I_CTIME_XATTR	7	/* attribute mod date is stored in xattr */
 
 #define HFSPLUS_IS_RSRC(inode) \
 	test_bit(HFSPLUS_I_RSRC, &HFSPLUS_I(inode)->flags)
@@ -351,6 +362,9 @@ int hfsplus_delete_all_attrs(struct inode *dir, u32 cnid);
 int hfsplus_replace_attr(struct inode *inode,
 			 const char *name,
 			 const void *value, size_t size);
+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,
+			  const char *name,
+			  const void *value, size_t size);
 
 /* bitmap.c */
 int hfsplus_block_allocate(struct super_block *sb, u32 size, u32 offset,
@@ -467,6 +481,9 @@ int hfsplus_file_fsync(struct file *file, loff_t start, loff_t end,
 int hfsplus_fileattr_get(struct dentry *dentry, struct file_kattr *fa);
 int hfsplus_fileattr_set(struct mnt_idmap *idmap,
 			 struct dentry *dentry, struct file_kattr *fa);
+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,
+			   const char *name, __be32 mt,
+			   time64_t *ut, bool *has_xattr);
 
 /* ioctl.c */
 long hfsplus_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
@@ -532,23 +549,42 @@ 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.
+ * The on-disk 32-bit timestamps cover the range from
+ * HFS_MIN_TIMESTAMP_SECS (Jan. 1, 1904) till HFS_MAX_TIMESTAMP_SECS
+ * (Feb. 6, 2040). The timestamps out of this range are clamped.
+ * The on-disk value HFSPLUS_EXT_TIMESTAMP_MARK (maximal value) means
+ * that real timestamp could be stored in the internal xattr of
+ * the catalog record. It is Linux specific extension of
+ * HFS+ on-disk layout. It makes possible to support timestamps
+ * till HFSPLUS_MAX_TIMESTAMP_SECS.
  */
-#define HFSPLUS_UTC_OFFSET 2082844800U
+#define HFSPLUS_EXT_TIMESTAMP_MARK	cpu_to_be32(U32_MAX)
+#define HFSPLUS_MAX_TIMESTAMP_SECS	TIME64_MAX
 
 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 = clamp_t(time64_t, ut,
+		     HFS_MIN_TIMESTAMP_SECS, HFS_MAX_TIMESTAMP_SECS);
+	ut += HFS_UTC_OFFSET;
+
+	return cpu_to_be32(lower_32_bits(ut));
+}
+
+static inline bool hfsp_ut_needs_xattr(time64_t ut)
+{
+	return ut >= HFS_MAX_TIMESTAMP_SECS;
+}
+
+static inline bool hfsp_mt_is_ext_timestamp(__be32 mt)
+{
+	return mt == HFSPLUS_EXT_TIMESTAMP_MARK;
 }
 
 static inline enum hfsplus_btree_mutex_classes
diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c
index 2ce6de574fa6e..0df58dfda5a3e 100644
--- a/fs/hfsplus/inode.c
+++ b/fs/hfsplus/inode.c
@@ -18,6 +18,7 @@
 #include <linux/cred.h>
 #include <linux/uio.h>
 #include <linux/fileattr.h>
+#include <kunit/visibility.h>
 
 #include "hfsplus_fs.h"
 #include "hfsplus_raw.h"
@@ -344,7 +345,8 @@ int hfsplus_getattr(struct mnt_idmap *idmap, const struct path *path,
 
 	if (request_mask & STATX_BTIME) {
 		stat->result_mask |= STATX_BTIME;
-		stat->btime = hfsp_mt2ut(hip->create_date);
+		stat->btime.tv_sec = hip->birthdate;
+		stat->btime.tv_nsec = 0;
 	}
 
 	if (inode->i_flags & S_APPEND)
@@ -598,9 +600,139 @@ void hfsplus_inode_write_fork(struct inode *inode,
 	fork->total_blocks = cpu_to_be32(HFSPLUS_I(inode)->alloc_blocks);
 }
 
+/*
+ * hfsplus_read_timestamp - convert on-disk timestamp into 1970-base one
+ * @sb: superblock
+ * @cnid: catalog record ID
+ * @name: name of internal xattr that can keep the real timestamp
+ * @mt: on-disk timestamp
+ * @ut: pointer on converted timestamp [out]
+ * @has_xattr: pointer on flag that xattr exists [out]
+ *
+ * If on-disk timestamp is equal to HFSPLUS_EXT_TIMESTAMP_MARK, then
+ * the real timestamp (after February 2040) could be stored in
+ * the internal xattr. If the xattr is absent (for example, the record
+ * has been created by Mac OS X), then HFS_MAX_TIMESTAMP_SECS is used.
+ */
+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,
+			   const char *name, __be32 mt,
+			   time64_t *ut, bool *has_xattr)
+{
+	time64_t timestamp;
+	int err;
+
+	*ut = __hfsp_mt2ut(mt);
+	*has_xattr = false;
+
+	if (!hfsp_mt_is_ext_timestamp(mt))
+		return 0;
+
+	err = hfsplus_get_timestamp_xattr(sb, cnid, name, &timestamp);
+	if (err == -ENODATA || err == -EOPNOTSUPP) {
+		/* timestamp is really equal to HFS_MAX_TIMESTAMP_SECS */
+		return 0;
+	} else if (unlikely(err)) {
+		pr_warn("fail to extract timestamp: cnid %u, name %s, err %d\n",
+			cnid, name, err);
+		return err;
+	}
+
+	if (timestamp < HFS_MAX_TIMESTAMP_SECS) {
+		pr_warn("invalid timestamp in xattr: cnid %u, name %s, timestamp %lld\n",
+			cnid, name, timestamp);
+		/* keep xattr to be deleted or overwritten */
+		*has_xattr = true;
+	} else {
+		*ut = timestamp;
+		*has_xattr = true;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(hfsplus_read_timestamp);
+
+static int hfsplus_inode_read_timestamps(struct inode *inode,
+					 struct hfsplus_timestamps *timestamps)
+{
+	struct super_block *sb = inode->i_sb;
+	struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
+	u32 cnid = inode->i_ino;
+	struct timespec64 ts = {0};
+	bool has_xattr;
+	int res = 0;
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_ACCESS_DATE_NAME,
+				     timestamps->access_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_atime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_ATIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+				     timestamps->content_mod_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_mtime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_MTIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+				     timestamps->attribute_mod_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_ctime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_CTIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_CREATE_DATE_NAME,
+				     timestamps->create_date,
+				     &hip->birthdate, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	hip->create_date = timestamps->create_date;
+
+finish_read_timestamps:
+	return res;
+}
+
+static inline void
+hfsplus_copy_folder2timestamps(struct hfsplus_cat_folder *folder,
+				struct hfsplus_timestamps *timestamps)
+{
+	timestamps->create_date = folder->create_date;
+	timestamps->content_mod_date = folder->content_mod_date;
+	timestamps->attribute_mod_date = folder->attribute_mod_date;
+	timestamps->access_date = folder->access_date;
+	timestamps->backup_date = folder->backup_date;
+}
+
+static inline void
+hfsplus_copy_file2timestamps(struct hfsplus_cat_file *file,
+			     struct hfsplus_timestamps *timestamps)
+{
+	timestamps->create_date = file->create_date;
+	timestamps->content_mod_date = file->content_mod_date;
+	timestamps->attribute_mod_date = file->attribute_mod_date;
+	timestamps->access_date = file->access_date;
+	timestamps->backup_date = file->backup_date;
+}
+
 int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 {
 	hfsplus_cat_entry entry;
+	struct hfsplus_timestamps timestamps = {0};
 	int res = 0;
 	u16 type;
 
@@ -622,12 +754,14 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 			goto out;
 		set_nlink(inode, 1);
 		inode->i_size = 2 + be32_to_cpu(folder->valence);
-		inode_set_atime_to_ts(inode, hfsp_mt2ut(folder->access_date));
-		inode_set_mtime_to_ts(inode,
-				      hfsp_mt2ut(folder->content_mod_date));
-		inode_set_ctime_to_ts(inode,
-				      hfsp_mt2ut(folder->attribute_mod_date));
-		HFSPLUS_I(inode)->create_date = folder->create_date;
+
+		hfsplus_copy_folder2timestamps(folder, &timestamps);
+		res = hfsplus_inode_read_timestamps(inode, &timestamps);
+		if (res) {
+			res = -EIO;
+			goto out;
+		}
+
 		HFSPLUS_I(inode)->fs_blocks = 0;
 		if (folder->flags & cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {
 			HFSPLUS_I(inode)->subfolders =
@@ -668,12 +802,13 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 			init_special_inode(inode, inode->i_mode,
 					   be32_to_cpu(file->permissions.dev));
 		}
-		inode_set_atime_to_ts(inode, hfsp_mt2ut(file->access_date));
-		inode_set_mtime_to_ts(inode,
-				      hfsp_mt2ut(file->content_mod_date));
-		inode_set_ctime_to_ts(inode,
-				      hfsp_mt2ut(file->attribute_mod_date));
-		HFSPLUS_I(inode)->create_date = file->create_date;
+
+		hfsplus_copy_file2timestamps(file, &timestamps);
+		res = hfsplus_inode_read_timestamps(inode, &timestamps);
+		if (res) {
+			res = -EIO;
+			goto out;
+		}
 	} else {
 		pr_err("bad catalog entry used to create inode\n");
 		res = -EIO;
@@ -682,12 +817,126 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 	return res;
 }
 
+struct hfsplus_xattr_changes {
+	bool added;
+	bool removed;
+};
+
+/*
+ * Convert @ts into on-disk timestamp. The timestamp after February 2040
+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK with the real value in
+ * the internal xattr @name. The stale xattr is removed if timestamp
+ * becomes representable by on-disk field.
+ */
+static int hfsplus_inode_save_timestamp(struct inode *inode,
+					const char *name, int xattr_bit,
+					struct timespec64 ts, __be32 *date,
+					struct hfsplus_xattr_changes *xattr)
+{
+	struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
+	time64_t stored;
+	int err;
+
+	*date = __hfsp_ut2mt(ts.tv_sec);
+
+	if (hfsp_ut_needs_xattr(ts.tv_sec)) {
+		/* don't rewrite xattr if nothing has been changed */
+		if (test_bit(xattr_bit, &hip->flags)) {
+			err = hfsplus_get_timestamp_xattr(inode->i_sb,
+							  inode->i_ino,
+							  name,
+							  &stored);
+			if (!err && stored == ts.tv_sec)
+				return 0;
+		}
+
+		err = hfsplus_set_timestamp_xattr(inode, inode->i_ino,
+						  name, ts.tv_sec);
+		if (err) {
+			clear_bit(xattr_bit, &hip->flags);
+			return err;
+		}
+
+		set_bit(xattr_bit, &hip->flags);
+		xattr->added = true;
+	} else if (test_bit(xattr_bit, &hip->flags)) {
+		err = hfsplus_remove_timestamp_xattr(inode, name);
+		if (err)
+			return err;
+
+		clear_bit(xattr_bit, &hip->flags);
+		xattr->removed = true;
+	}
+
+	return 0;
+}
+
+static void hfsplus_inode_save_timestamps(struct inode *inode,
+					  struct hfsplus_timestamps *timestamps,
+					  struct hfsplus_xattr_changes *changes)
+{
+	int res;
+
+	/*
+	 * Failure of xattr operation is not critical. The timestamp
+	 * is clamped by HFS_MAX_TIMESTAMP_SECS in such case.
+	 * But the rest of the catalog record must be saved anyway.
+	 */
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_ACCESS_DATE_NAME,
+					   HFSPLUS_I_ATIME_XATTR,
+					   inode_get_atime(inode),
+					   &timestamps->access_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save access date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+					   HFSPLUS_I_MTIME_XATTR,
+					   inode_get_mtime(inode),
+					   &timestamps->content_mod_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save content mod date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+					   HFSPLUS_I_CTIME_XATTR,
+					   inode_get_ctime(inode),
+					   &timestamps->attribute_mod_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save attribute mod date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+}
+
+static inline __be16
+hfsplus_update_xattr_exists_flag(struct inode *inode, __be16 flags,
+				 struct hfsplus_xattr_changes *xattr)
+{
+	if (xattr->added)
+		flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+	else if (xattr->removed && !hfsplus_attr_exists(inode, NULL))
+		flags &= cpu_to_be16(~HFSPLUS_XATTR_EXISTS);
+
+	return flags;
+}
+
 int hfsplus_cat_write_inode(struct inode *inode)
 {
 	struct inode *main_inode = inode;
 	struct hfs_btree *tree = HFSPLUS_SB(inode->i_sb)->cat_tree;
 	struct hfs_find_data fd;
 	hfsplus_cat_entry entry;
+	struct hfsplus_timestamps timestamps = {0};
+	struct hfsplus_xattr_changes changes = {0};
 	int res = 0;
 
 	hfs_dbg("inode->i_ino %llu\n", inode->i_ino);
@@ -718,9 +967,15 @@ int hfsplus_cat_write_inode(struct inode *inode)
 					sizeof(struct hfsplus_cat_folder));
 		/* simple node checks? */
 		hfsplus_cat_set_perms(inode, &folder->permissions);
-		folder->access_date = hfsp_ut2mt(inode_get_atime(inode));
-		folder->content_mod_date = hfsp_ut2mt(inode_get_mtime(inode));
-		folder->attribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));
+
+		hfsplus_inode_save_timestamps(inode, &timestamps, &changes);
+		folder->access_date = timestamps.access_date;
+		folder->content_mod_date = timestamps.content_mod_date;
+		folder->attribute_mod_date = timestamps.attribute_mod_date;
+		folder->flags = hfsplus_update_xattr_exists_flag(inode,
+								 folder->flags,
+								 &changes);
+
 		folder->valence = cpu_to_be32(inode->i_size - 2);
 		if (folder->flags & cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {
 			folder->subfolders =
@@ -753,9 +1008,15 @@ int hfsplus_cat_write_inode(struct inode *inode)
 			file->flags |= cpu_to_be16(HFSPLUS_FILE_LOCKED);
 		else
 			file->flags &= cpu_to_be16(~HFSPLUS_FILE_LOCKED);
-		file->access_date = hfsp_ut2mt(inode_get_atime(inode));
-		file->content_mod_date = hfsp_ut2mt(inode_get_mtime(inode));
-		file->attribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));
+
+		hfsplus_inode_save_timestamps(inode, &timestamps, &changes);
+		file->access_date = timestamps.access_date;
+		file->content_mod_date = timestamps.content_mod_date;
+		file->attribute_mod_date = timestamps.attribute_mod_date;
+		file->flags = hfsplus_update_xattr_exists_flag(inode,
+							       file->flags,
+							       &changes);
+
 		hfs_bnode_write(fd.bnode, &entry, fd.entryoffset,
 					 sizeof(struct hfsplus_cat_file));
 	}
diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c
index ff7d6b3336a66..b10b53f6f8f9e 100644
--- a/fs/hfsplus/super.c
+++ b/fs/hfsplus/super.c
@@ -86,6 +86,7 @@ struct inode *hfsplus_iget(struct super_block *sb, unsigned long ino)
 	mutex_init(&HFSPLUS_I(inode)->extents_lock);
 	HFSPLUS_I(inode)->rsrc_inode = NULL;
 	HFSPLUS_I(inode)->create_date = 0;
+	HFSPLUS_I(inode)->birthdate = 0;
 	HFSPLUS_I(inode)->linkid = 0;
 	HFSPLUS_I(inode)->flags = 0;
 	HFSPLUS_I(inode)->fs_blocks = 0;
@@ -511,6 +512,13 @@ 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;
+	/*
+	 * Timestamps after HFS_MAX_TIMESTAMP_SECS are stored in xattrs.
+	 */
+	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/fs/hfsplus/time_test.c b/fs/hfsplus/time_test.c
new file mode 100644
index 0000000000000..4b3312dc0048b
--- /dev/null
+++ b/fs/hfsplus/time_test.c
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for HFS+ date/time conversion
+ *
+ * Copyright (C) 2026 Viacheslav Dubeyko <slava@dubeyko.com>
+ */
+
+#include <kunit/test.h>
+#include "hfsplus_fs.h"
+#include "xattr.h"
+
+/* January 1, 1970, 00:00:00 UTC */
+#define TEST_UNIX_EPOCH_SECS		0LL
+/* December 31, 1969, 23:59:59 UTC */
+#define TEST_BEFORE_UNIX_EPOCH_SECS	-1LL
+/* January 1, 2000, 00:00:00 UTC */
+#define TEST_Y2000_SECS			946684800LL
+/* January 19, 2038, 03:14:08 UTC (signed 32-bit overflow) */
+#define TEST_Y2038_SECS			2147483648LL
+/* January 1, 2026, 00:00:00 UTC */
+#define TEST_Y2026_SECS			1767225600LL
+/* January 1, 2100, 00:00:00 UTC */
+#define TEST_Y2100_SECS			4102444800LL
+/* January 1, 1900, 00:00:00 UTC */
+#define TEST_Y1900_SECS			-2208988800LL
+
+static u32 mt_raw(__be32 mt)
+{
+	return be32_to_cpu(mt);
+}
+
+/* Test the time range constants */
+static void hfsplus_time_constants_test(struct kunit *test)
+{
+	/* 32-bit unsigned on-disk field covers exactly U32_MAX seconds */
+	KUNIT_EXPECT_EQ(test, (s64)U32_MAX,
+			HFS_MAX_TIMESTAMP_SECS - HFS_MIN_TIMESTAMP_SECS);
+	KUNIT_EXPECT_EQ(test, -(s64)HFS_UTC_OFFSET, HFS_MIN_TIMESTAMP_SECS);
+
+	/* xattrs extend the supported range beyond February 2040 */
+	KUNIT_EXPECT_GT(test, (s64)HFSPLUS_MAX_TIMESTAMP_SECS,
+			(s64)HFS_MAX_TIMESTAMP_SECS);
+
+	/* the extended timestamp mark is the maximal on-disk value */
+	KUNIT_EXPECT_EQ(test, U32_MAX, mt_raw(HFSPLUS_EXT_TIMESTAMP_MARK));
+}
+
+/* Test conversion of on-disk (1904-based) timestamp into 1970-based one */
+static void hfsplus_mt2ut_test(struct kunit *test)
+{
+	KUNIT_EXPECT_EQ(test, HFS_MIN_TIMESTAMP_SECS,
+			__hfsp_mt2ut(cpu_to_be32(0)));
+	KUNIT_EXPECT_EQ(test, TEST_UNIX_EPOCH_SECS,
+			__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET)));
+	KUNIT_EXPECT_EQ(test, TEST_BEFORE_UNIX_EPOCH_SECS,
+			__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET - 1)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2000_SECS,
+			__hfsp_mt2ut(cpu_to_be32(3029529600U)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2026_SECS,
+			__hfsp_mt2ut(cpu_to_be32(3850070400U)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2038_SECS,
+			__hfsp_mt2ut(cpu_to_be32(4230328448U)));
+	KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,
+			__hfsp_mt2ut(cpu_to_be32(U32_MAX)));
+	KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,
+			__hfsp_mt2ut(HFSPLUS_EXT_TIMESTAMP_MARK));
+}
+
+/* Test conversion of 1970-based timestamp into on-disk (1904-based) one */
+static void hfsplus_ut2mt_test(struct kunit *test)
+{
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS)));
+	KUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET,
+			mt_raw(__hfsp_ut2mt(TEST_UNIX_EPOCH_SECS)));
+	KUNIT_EXPECT_EQ(test, 3029529600U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2000_SECS)));
+	KUNIT_EXPECT_EQ(test, 3850070400U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2026_SECS)));
+	KUNIT_EXPECT_EQ(test, 4230328448U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2038_SECS)));
+	KUNIT_EXPECT_EQ(test, U32_MAX - 1,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));
+}
+
+/*
+ * Test timestamps before January 1, 1970 (xfstests generic/258).
+ * The old logic added HFS_UTC_OFFSET to lower 32 bits of negative
+ * value and produced completely wrong result.
+ */
+static void hfsplus_ut2mt_before_1970_test(struct kunit *test)
+{
+	time64_t ut;
+
+	KUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET - 1,
+			mt_raw(__hfsp_ut2mt(TEST_BEFORE_UNIX_EPOCH_SECS)));
+
+	/* January 1, 1960, 00:00:00 UTC */
+	ut = -315619200LL;
+	KUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));
+
+	/* January 1, 1904, 00:00:01 UTC */
+	ut = HFS_MIN_TIMESTAMP_SECS + 1;
+	KUNIT_EXPECT_EQ(test, 1U, mt_raw(__hfsp_ut2mt(ut)));
+	KUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));
+}
+
+/* Test that out-of-range timestamps are clamped instead of wrapped */
+static void hfsplus_ut2mt_clamp_test(struct kunit *test)
+{
+	/* below January 1, 1904 */
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS - 1)));
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(TEST_Y1900_SECS)));
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(TIME64_MIN)));
+
+	/* after February 6, 2040 */
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS + 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(TEST_Y2100_SECS)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFSPLUS_MAX_TIMESTAMP_SECS)));
+
+	/* lower 32 bits of these values would wrap into small dates */
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS +
+					    (s64)U32_MAX + 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(1LL << 32)));
+}
+
+/* Test that representable timestamps survive the round trip */
+static void hfsplus_time_roundtrip_test(struct kunit *test)
+{
+	static const time64_t values[] = {
+		HFS_MIN_TIMESTAMP_SECS,
+		HFS_MIN_TIMESTAMP_SECS + 1,
+		TEST_BEFORE_UNIX_EPOCH_SECS,
+		TEST_UNIX_EPOCH_SECS,
+		1,
+		TEST_Y2000_SECS,
+		TEST_Y2026_SECS,
+		(s64)S32_MAX,
+		TEST_Y2038_SECS,
+		HFS_MAX_TIMESTAMP_SECS - 1,
+		HFS_MAX_TIMESTAMP_SECS,
+	};
+	time64_t ut;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(values); i++) {
+		KUNIT_EXPECT_EQ_MSG(test, values[i],
+				    __hfsp_mt2ut(__hfsp_ut2mt(values[i])),
+				    "round trip failed: index %d", i);
+	}
+
+	/* sweep the whole range with a large prime step */
+	for (ut = HFS_MIN_TIMESTAMP_SECS; ut <= HFS_MAX_TIMESTAMP_SECS;
+	     ut += 999983) {
+		if (__hfsp_mt2ut(__hfsp_ut2mt(ut)) != ut) {
+			KUNIT_FAIL(test, "round trip failed: ut %lld", ut);
+			break;
+		}
+	}
+
+	/* every on-disk value maps back to itself */
+	for (i = 0; i < 32; i++) {
+		u32 raw = (u32)BIT_ULL(i);
+
+		KUNIT_EXPECT_EQ(test, raw,
+				mt_raw(__hfsp_ut2mt(__hfsp_mt2ut(cpu_to_be32(raw)))));
+	}
+}
+
+/* Test the decision to keep timestamp in xattr */
+static void hfsplus_ut_needs_xattr_test(struct kunit *test)
+{
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(HFS_MIN_TIMESTAMP_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_BEFORE_UNIX_EPOCH_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_UNIX_EPOCH_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2026_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2038_SECS));
+	KUNIT_EXPECT_FALSE(test,
+			   hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS - 1));
+
+	KUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS));
+	KUNIT_EXPECT_TRUE(test,
+			  hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS + 1));
+	KUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(TEST_Y2100_SECS));
+	KUNIT_EXPECT_TRUE(test,
+			  hfsp_ut_needs_xattr(HFSPLUS_MAX_TIMESTAMP_SECS));
+}
+
+/* Test detection of the extended timestamp mark */
+static void hfsplus_mt_is_ext_timestamp_test(struct kunit *test)
+{
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(HFSPLUS_EXT_TIMESTAMP_MARK));
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX)));
+
+	KUNIT_EXPECT_FALSE(test, hfsp_mt_is_ext_timestamp(cpu_to_be32(0)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX - 1)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(HFS_UTC_OFFSET)));
+
+	/* every timestamp that needs xattr is stored as the mark */
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(TEST_Y2100_SECS)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));
+}
+
+static struct super_block *alloc_mock_sb(struct kunit *test)
+{
+	struct super_block *sb;
+	struct hfsplus_sb_info *sbi;
+
+	sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sb);
+
+	sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);
+
+	/* no attributes tree: xattr lookup returns -EOPNOTSUPP */
+	sbi->attr_tree = NULL;
+	sb->s_fs_info = sbi;
+
+	return sb;
+}
+
+/*
+ * Test reading of the on-disk timestamp that doesn't need xattr.
+ * The superblock must not be touched in such case.
+ */
+static void hfsplus_read_timestamp_regular_test(struct kunit *test)
+{
+	static const time64_t values[] = {
+		HFS_MIN_TIMESTAMP_SECS,
+		TEST_BEFORE_UNIX_EPOCH_SECS,
+		TEST_UNIX_EPOCH_SECS,
+		TEST_Y2026_SECS,
+		HFS_MAX_TIMESTAMP_SECS - 1,
+	};
+	time64_t ut;
+	bool has_xattr;
+	int err;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(values); i++) {
+		ut = 0;
+		has_xattr = true;
+
+		err = hfsplus_read_timestamp(NULL, HFSPLUS_FIRSTUSER_CNID,
+					     XATTR_LINUX_ACCESS_DATE_NAME,
+					     __hfsp_ut2mt(values[i]),
+					     &ut, &has_xattr);
+		KUNIT_EXPECT_EQ(test, 0, err);
+		KUNIT_EXPECT_EQ(test, values[i], ut);
+		KUNIT_EXPECT_FALSE(test, has_xattr);
+	}
+}
+
+/*
+ * Test reading of the extended timestamp mark without xattr
+ * (for example, the record has been created by Mac OS X or
+ * the volume has no attributes tree). The timestamp must be
+ * HFS_MAX_TIMESTAMP_SECS without any error.
+ */
+static void hfsplus_read_timestamp_mark_no_xattr_test(struct kunit *test)
+{
+	struct super_block *sb = alloc_mock_sb(test);
+	static const char * const names[] = {
+		XATTR_LINUX_CREATE_DATE_NAME,
+		XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+		XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+		XATTR_LINUX_ACCESS_DATE_NAME,
+	};
+	time64_t ut;
+	bool has_xattr;
+	int err;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(names); i++) {
+		ut = 0;
+		has_xattr = true;
+
+		err = hfsplus_read_timestamp(sb, HFSPLUS_FIRSTUSER_CNID,
+					     names[i],
+					     HFSPLUS_EXT_TIMESTAMP_MARK,
+					     &ut, &has_xattr);
+		KUNIT_EXPECT_EQ(test, 0, err);
+		KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS, ut);
+		KUNIT_EXPECT_FALSE(test, has_xattr);
+	}
+}
+
+/* Test that names of internal xattrs fit the name buffer */
+static void hfsplus_timestamp_xattr_names_test(struct kunit *test)
+{
+	static const char * const names[] = {
+		XATTR_LINUX_CREATE_DATE_NAME,
+		XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+		XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+		XATTR_LINUX_ACCESS_DATE_NAME,
+		XATTR_LINUX_BACKUP_DATE_NAME,
+	};
+	int i;
+
+	KUNIT_EXPECT_EQ(test, strlen(XATTR_LINUX_HFS_PREFIX),
+			(size_t)XATTR_LINUX_HFS_PREFIX_LEN);
+
+	for (i = 0; i < ARRAY_SIZE(names); i++) {
+		size_t len = XATTR_LINUX_HFS_PREFIX_LEN + strlen(names[i]);
+
+		KUNIT_EXPECT_LT_MSG(test, len, (size_t)XATTR_LINUX_HFS_NAME_MAX,
+				    "name %s is too long", names[i]);
+		KUNIT_EXPECT_LE(test, len, (size_t)HFSPLUS_ATTR_MAX_STRLEN);
+	}
+}
+
+static struct kunit_case hfsplus_time_test_cases[] = {
+	KUNIT_CASE(hfsplus_time_constants_test),
+	KUNIT_CASE(hfsplus_mt2ut_test),
+	KUNIT_CASE(hfsplus_ut2mt_test),
+	KUNIT_CASE(hfsplus_ut2mt_before_1970_test),
+	KUNIT_CASE(hfsplus_ut2mt_clamp_test),
+	KUNIT_CASE(hfsplus_time_roundtrip_test),
+	KUNIT_CASE(hfsplus_ut_needs_xattr_test),
+	KUNIT_CASE(hfsplus_mt_is_ext_timestamp_test),
+	KUNIT_CASE(hfsplus_read_timestamp_regular_test),
+	KUNIT_CASE(hfsplus_read_timestamp_mark_no_xattr_test),
+	KUNIT_CASE(hfsplus_timestamp_xattr_names_test),
+	{}
+};
+
+static struct kunit_suite hfsplus_time_test_suite = {
+	.name = "hfsplus_time",
+	.test_cases = hfsplus_time_test_cases,
+};
+
+kunit_test_suite(hfsplus_time_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for HFS+ date/time conversion");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
diff --git a/fs/hfsplus/xattr.c b/fs/hfsplus/xattr.c
index 21a1c196c71f2..d3b1734496168 100644
--- a/fs/hfsplus/xattr.c
+++ b/fs/hfsplus/xattr.c
@@ -50,6 +50,12 @@ static bool is_known_namespace(const char *name)
 	return true;
 }
 
+static bool is_linux_hfs_internal_xattr(const char *name)
+{
+	return !strncmp(name, XATTR_LINUX_HFS_PREFIX,
+			XATTR_LINUX_HFS_PREFIX_LEN);
+}
+
 static u32 hfsplus_init_header_node(struct inode *attr_file,
 					u32 clump_size,
 					char *buf, u16 node_size)
@@ -319,6 +325,19 @@ static int hfsplus_create_attributes_file(struct super_block *sb)
 
 	hfsplus_mark_inode_dirty(attr_file, HFSPLUS_I_ATTR_DIRTY);
 
+	/*
+	 * Store the fork of AttributesFile into the volume header right now.
+	 * The AttributesFile can be created by hfsplus_cat_write_inode()
+	 * (timestamps after 2040 are stored in xattrs) in the middle of
+	 * sync operation. Then, the dirty AttributesFile's inode can be
+	 * skipped by writeback and the volume header will be committed
+	 * without AttributesFile. As a result, the AttributesFile will be
+	 * not opened during the next mount.
+	 */
+	hfsplus_inode_write_fork(attr_file, &sbi->s_vhdr->attr_file);
+	set_bit(HFSPLUS_SB_WRITEBACKUP, &sbi->flags);
+	hfsplus_mark_mdb_dirty(sb);
+
 	sbi->attr_tree = hfs_btree_open(sb, HFSPLUS_ATTR_CNID);
 	if (!sbi->attr_tree)
 		pr_err("failed to load attributes file\n");
@@ -339,8 +358,7 @@ static int hfsplus_create_attributes_file(struct super_block *sb)
 	return err;
 }
 
-static inline
-bool is_xattr_operation_supported(struct inode *inode)
+static inline bool is_xattr_operation_supported(struct inode *inode)
 {
 	if (HFSPLUS_IS_RSRC(inode))
 		return false;
@@ -609,8 +627,9 @@ static ssize_t hfsplus_getxattr_finder_info(struct inode *inode,
 	return res;
 }
 
-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
-			 void *value, size_t size)
+static ssize_t __hfsplus_getxattr_nocheck(struct super_block *sb,
+					  u32 cnid, const char *name,
+					  void *value, size_t size)
 {
 	struct hfs_find_data fd;
 	hfsplus_attr_entry *entry;
@@ -619,13 +638,7 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 	u16 record_length = 0;
 	ssize_t res;
 
-	if (!is_xattr_operation_supported(inode))
-		return -EOPNOTSUPP;
-
-	if (!strcmp_xattr_finder_info(name))
-		return hfsplus_getxattr_finder_info(inode, value, size);
-
-	if (!HFSPLUS_SB(inode->i_sb)->attr_tree)
+	if (!HFSPLUS_SB(sb)->attr_tree)
 		return -EOPNOTSUPP;
 
 	entry = hfsplus_alloc_attr_entry();
@@ -634,18 +647,18 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 		return -ENOMEM;
 	}
 
-	res = hfs_find_init(HFSPLUS_SB(inode->i_sb)->attr_tree, &fd);
+	res = hfs_find_init(HFSPLUS_SB(sb)->attr_tree, &fd);
 	if (res) {
 		pr_err("can't init xattr find struct\n");
 		goto failed_getxattr_init;
 	}
 
-	res = hfsplus_find_attr(inode->i_sb, inode->i_ino, name, &fd);
+	res = hfsplus_find_attr(sb, cnid, name, &fd);
 	if (res) {
 		if (res == -ENOENT || res == -ENODATA)
 			res = -ENODATA;
 		else
-			pr_err("xattr search failed\n");
+			pr_err("xattr searching failed\n");
 		goto out;
 	}
 
@@ -693,6 +706,38 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 	return res;
 }
 
+static inline ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
+					 void *value, size_t size)
+{
+	if (!is_xattr_operation_supported(inode))
+		return -EOPNOTSUPP;
+
+	if (!strcmp_xattr_finder_info(name))
+		return hfsplus_getxattr_finder_info(inode, value, size);
+
+	return __hfsplus_getxattr_nocheck(inode->i_sb, (u32)inode->i_ino,
+					  name, value, size);
+}
+
+static inline char *hfsplus_build_xattr_name(const char *name,
+					     const char *prefix,
+					     size_t prefixlen)
+{
+	char *xattr_name;
+	size_t len = NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1;
+
+	xattr_name = kzalloc(len, GFP_KERNEL);
+	if (!xattr_name)
+		return ERR_PTR(-ENOMEM);
+
+	if (snprintf(xattr_name, len, "%s%s", prefix, name) >= len) {
+		kfree(xattr_name);
+		return ERR_PTR(-ENAMETOOLONG);
+	}
+
+	return xattr_name;
+}
+
 ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 			 void *value, size_t size,
 			 const char *prefix, size_t prefixlen)
@@ -704,13 +749,9 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 		inode->i_ino, name ? name : NULL,
 		prefix ? prefix : NULL);
 
-	xattr_name = kmalloc(NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1,
-			     GFP_KERNEL);
-	if (!xattr_name)
-		return -ENOMEM;
-
-	strcpy(xattr_name, prefix);
-	strcpy(xattr_name + prefixlen, name);
+	xattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);
+	if (IS_ERR(xattr_name))
+		return PTR_ERR(xattr_name);
 
 	res = __hfsplus_getxattr(inode, xattr_name, value, size);
 	kfree(xattr_name);
@@ -718,7 +759,114 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 	hfs_dbg("finished: res %d\n", res);
 
 	return res;
+}
+
+static ssize_t hfsplus_getxattr_nocheck(struct super_block *sb, u32 cnid,
+					const char *name,
+					void *value, size_t size,
+					const char *prefix, size_t prefixlen)
+{
+	int res;
+	char *xattr_name;
 
+	hfs_dbg("cnid %u, name %s, prefix %s\n",
+		cnid, name ? name : NULL,
+		prefix ? prefix : NULL);
+
+	xattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);
+	if (IS_ERR(xattr_name))
+		return PTR_ERR(xattr_name);
+
+	res = __hfsplus_getxattr_nocheck(sb, cnid, xattr_name, value, size);
+	kfree(xattr_name);
+
+	hfs_dbg("finished: res %d\n", res);
+
+	return res;
+}
+
+/*
+ * Get the timestamp stored in the internal xattr @name
+ * of catalog record @cnid. The caller can hold the catalog tree lock.
+ */
+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,
+				const char *name, time64_t *ts)
+{
+	__be64 value;
+	ssize_t res;
+
+	res = hfsplus_getxattr_nocheck(sb, cnid, name,
+					&value, sizeof(value),
+					XATTR_LINUX_HFS_PREFIX,
+					XATTR_LINUX_HFS_PREFIX_LEN);
+	if (res < 0)
+		return res;
+
+	if (res != sizeof(value))
+		return -EIO;
+
+	*ts = (time64_t)be64_to_cpu(value);
+	return 0;
+}
+
+/*
+ * Store the timestamp @ts into the internal xattr @name of catalog
+ * record @cnid. The caller must hold the catalog tree lock and it is
+ * caller's responsibility to set HFSPLUS_XATTR_EXISTS flag in
+ * the catalog record.
+ */
+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,
+				const char *name, time64_t ts)
+{
+	struct super_block *sb = inode->i_sb;
+	char xattr_name[XATTR_LINUX_HFS_NAME_MAX];
+	__be64 value = cpu_to_be64(ts);
+	int err;
+
+	hfs_dbg("ino %llu, cnid %u, name %s, ts %lld\n",
+		inode->i_ino, cnid, name, ts);
+
+	if (!HFSPLUS_SB(sb)->attr_tree) {
+		err = hfsplus_create_attributes_file(sb);
+		if (unlikely(err))
+			return err;
+
+		if (!HFSPLUS_SB(sb)->attr_tree)
+			return -EIO;
+	}
+
+	if (snprintf(xattr_name, sizeof(xattr_name), "%s%s",
+		     XATTR_LINUX_HFS_PREFIX, name) >= sizeof(xattr_name))
+		return -ENAMETOOLONG;
+
+	return hfsplus_set_attr_cnid(inode, cnid, xattr_name,
+				     &value, sizeof(value));
+}
+
+/*
+ * Remove the internal xattr @name of @inode. The caller must hold
+ * the catalog tree lock and it is caller's responsibility to update
+ * HFSPLUS_XATTR_EXISTS flag in the catalog record.
+ */
+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name)
+{
+	char xattr_name[XATTR_LINUX_HFS_NAME_MAX];
+	int err;
+
+	hfs_dbg("ino %llu, name %s\n", inode->i_ino, name);
+
+	if (!HFSPLUS_SB(inode->i_sb)->attr_tree)
+		return 0;
+
+	if (snprintf(xattr_name, sizeof(xattr_name), "%s%s",
+		     XATTR_LINUX_HFS_PREFIX, name) >= sizeof(xattr_name))
+		return -ENAMETOOLONG;
+
+	err = hfsplus_delete_attr(inode, xattr_name);
+	if (err == -ENOENT || err == -ENODATA)
+		err = 0;
+
+	return err;
 }
 
 static inline int can_list(const char *xattr_name)
@@ -726,6 +874,10 @@ static inline int can_list(const char *xattr_name)
 	if (!xattr_name)
 		return 0;
 
+	/* Internal xattrs of the driver are hidden */
+	if (is_linux_hfs_internal_xattr(xattr_name))
+		return 0;
+
 	return strncmp(xattr_name, XATTR_TRUSTED_PREFIX,
 			XATTR_TRUSTED_PREFIX_LEN) ||
 				capable(CAP_SYS_ADMIN);
@@ -998,6 +1150,10 @@ static int hfsplus_osx_getxattr(const struct xattr_handler *handler,
 	if (is_known_namespace(name))
 		return -EOPNOTSUPP;
 
+	/* Internal xattrs of the driver are hidden */
+	if (is_linux_hfs_internal_xattr(name))
+		return -EOPNOTSUPP;
+
 	/*
 	 * osx is the namespace we use to indicate an unprefixed
 	 * attribute on the filesystem (like the ones that OS X
@@ -1020,6 +1176,10 @@ static int hfsplus_osx_setxattr(const struct xattr_handler *handler,
 	if (is_known_namespace(name))
 		return -EOPNOTSUPP;
 
+	/* Internal xattrs of the driver cannot be changed by user */
+	if (is_linux_hfs_internal_xattr(name))
+		return -EOPNOTSUPP;
+
 	/*
 	 * osx is the namespace we use to indicate an unprefixed
 	 * attribute on the filesystem (like the ones that OS X
diff --git a/fs/hfsplus/xattr.h b/fs/hfsplus/xattr.h
index 15cc55e414100..8f6283f85f133 100644
--- a/fs/hfsplus/xattr.h
+++ b/fs/hfsplus/xattr.h
@@ -12,6 +12,24 @@
 
 #include <linux/xattr.h>
 
+/*
+ * Internal xattrs of Linux HFS+ driver. Timestamps beyond
+ * HFS_MAX_TIMESTAMP_SECS (February 2040) cannot be represented by
+ * 32-bit on-disk fields. Such timestamp is stored as the maximal value
+ * (HFSPLUS_EXT_TIMESTAMP_MARK) in the catalog record and the real value
+ * is kept in the xattr as big-endian 64-bit seconds since
+ * 00:00 GMT, Jan. 1, 1970.
+ */
+#define XATTR_LINUX_HFS_PREFIX "linux_hfs."
+#define XATTR_LINUX_HFS_PREFIX_LEN \
+	(sizeof(XATTR_LINUX_HFS_PREFIX) - 1)
+#define XATTR_LINUX_HFS_NAME_MAX	64
+#define XATTR_LINUX_CREATE_DATE_NAME "create_date_u64"
+#define XATTR_LINUX_CONTENT_MOD_DATE_NAME "content_mod_date_u64"
+#define XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME "attribute_mod_date_u64"
+#define XATTR_LINUX_ACCESS_DATE_NAME "access_date_u64"
+#define XATTR_LINUX_BACKUP_DATE_NAME "backup_date_u64"
+
 extern const struct xattr_handler hfsplus_xattr_osx_handler;
 extern const struct xattr_handler hfsplus_xattr_user_handler;
 extern const struct xattr_handler hfsplus_xattr_trusted_handler;
@@ -23,16 +41,18 @@ int __hfsplus_setxattr(struct inode *inode, const char *name,
 			const void *value, size_t size, int flags);
 
 int hfsplus_setxattr(struct inode *inode, const char *name,
-				   const void *value, size_t size, int flags,
-				   const char *prefix, size_t prefixlen);
-
-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
-			   void *value, size_t size);
-
+		     const void *value, size_t size, int flags,
+		     const char *prefix, size_t prefixlen);
 ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 			 void *value, size_t size,
 			 const char *prefix, size_t prefixlen);
 
+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,
+				const char *name, time64_t *ts);
+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,
+				const char *name, time64_t ts);
+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name);
+
 ssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size);
 
 int hfsplus_init_security(struct inode *inode, struct inode *dir,
diff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h
index d6a615e74b26e..d4e91dea23628 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_ */

3/2 2026/10/10 00:10 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 25806
							output: 35
							thoughts: 921
						

Thoughts:
**Analyzing the HFS+ Timestamp Patch for Fuzzing Potential**

Okay, let's break this down. My initial assessment is focused on whether this patch introduces new attack surface suitable for fuzzing, and if so, how to best target it.

First, I need to understand what's changed. The diff touches a significant number of files: `hfs_fs.h`, `Makefile`, `attributes.c`, `catalog.c`, `dir.c`, `hfsplus_fs.h`, `inode.c`, `super.c`, `time_test.c`, `xattr.c`, `xattr.h`, and `hfs_common.h`. That's a decent breadth of code involved, immediately raising my interest.

The core functionality seems to be extending HFS+ timestamp support beyond February 2040 by storing 64-bit timestamps in internal extended attributes (`linux_hfs.*`). This is a critical point; changes to timestamp handling are a notorious source of bugs.

Diving deeper into the changes, `xattr.c` is the most active area. Specifically:

*   The addition of functions to check if an xattr is internal (`is_linux_hfs_internal_xattr()`).
*   Modifications to how xattrs are handled during creation and retrieval (`hfsplus_create_attributes_file`, `__hfsplus_getxattr`).
*   The implementation of specific functions for getting, setting, and removing these timestamp xattrs (`hfsplus_get_timestamp_xattr`, `hfsplus_set_timestamp_xattr`, `hfsplus_remove_timestamp_xattr`).
*   Filtering internal xattrs in various xattr operations.

The other files show related changes: `attributes.c` has been refactored to support new uses of cnid, which appears to link inodes with xattrs. `catalog.c` is updated to handle creation dates and xattrs for timestamps, which is critical for inode and filesystem integrity. `dir.c` is updated to handle xattrs in date comparisons. `inode.c` contains the primary logic for reading, writing, and saving these timestamps, including fallback mechanisms to handle old systems. `super.c` is updated to adjust the time metadata for the filesystem. The new KUnit tests in `time_test.c` are a good sign of proactive bug detection, but don't provide any guarantees.

Now, the crucial question: Is HFS+ accessible for fuzzing? Thankfully, based on my understanding of syzkaller and virtualized environments, yes, it appears to be. I can mount HFS+ filesystems using loop devices, image mounts, `syz_mount_image`, or the standard `mount` syscall. This is a very good starting point. I need to confirm that `HFSPLUS_FS` is enabled in the configuration; that will be the first step.

Next, I need to identify what the best focus symbols would be. This is where experience kicks in; I should target the new xattr functions (`hfsplus_get_timestamp_xattr`, `hfsplus_set_timestamp_xattr`, `hfsplus_remove_timestamp_xattr`) and the functions that read and write timestamps (`hfsplus_read_timestamp`, `hfsplus_inode_save_timestamps`, `hfsplus_cat_write_inode`). Also, I'll pay close attention to any code paths that handle the interaction of standard timestamps and the new extended timestamps and the fallback code. Any code that deals with changes, like date creation in `catalog.c` and date comparisons in `dir.c`, is always a good target.



4/2 2026/10/10 00:10 tool grepper
0m Args:
{
  "Expression": "config HFSPLUS_FS",
  "PathPrefixes": [
    "fs/hfsplus/Kconfig"
  ]
}

Results:
{
  "Output": "fs/hfsplus/Kconfig-1-# SPDX-License-Identifier: GPL-2.0-only\nfs/hfsplus/Kconfig:2:config HFSPLUS_FS\nfs/hfsplus/Kconfig-3-\ttristate \"Apple Extended HFS file system support\"\n"
}

5/2 2026/10/10 00:10 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 27631
							output: 34
							thoughts: 17
						

Thoughts:
Okay, let's dive into this. Here's my take on examining that Kconfig file:

**Analyzing the HFS+ Kconfig**

Alright, time to get my hands dirty and dissect the `fs/hfsplus/Kconfig` file. I need to understand what configuration options are available for the HFS+ filesystem within the kernel. This file is the key to understanding the build-time configuration parameters, the dependencies between them, and how they shape the HFS+ implementation. I'll be looking for things like:

*   **Feature Flags**: What specific HFS+ features can be enabled or disabled? Is there support for extended attributes, journaling, case-sensitive names, or other nuances? I'll want to see how these are controlled.
*   **Dependencies**: What other kernel components or options does the HFS+ driver rely on? For example, does it depend on specific block device drivers, crypto modules, or other filesystem-related code? Knowing the dependencies helps to understand potential build conflicts and compatibility issues.
*   **Default Values**: What are the default settings for the HFS+ options? Are they reasonable, or do they need adjustment for specific use cases or architectures?
*   **Help Text**: The help text within the Kconfig file should offer valuable insights into each option's purpose, potential risks, and implications. I'll pay close attention to this to gain a deeper understanding of the configuration choices.
*   **Architecture Specific Settings**: Does the Kconfig expose any settings that are dependent on the target system architecture? If so, I'll need to know which settings affect which architectures, for example, 32-bit versus 64-bit platforms.

I'm essentially trying to build a mental map of the configuration space for the HFS+ filesystem. This knowledge will be crucial for debugging any issues, optimizing the driver for specific environments, and understanding how different configurations impact performance and behavior. Once I've thoroughly examined this file, I'll have a much clearer picture of how HFS+ fits into the kernel ecosystem and how to effectively manage it.



6/2 2026/10/10 00:10 tool read-file
0m Args:
{
  "File": "fs/hfsplus/Kconfig",
  "FirstLine": 1,
  "LineCount": 40
}

Results:
{
  "Contents": "   1:\t# SPDX-License-Identifier: GPL-2.0-only\n   2:\tconfig HFSPLUS_FS\n   3:\t\ttristate \"Apple Extended HFS file system support\"\n   4:\t\tdepends on BLOCK\n   5:\t\tselect BUFFER_HEAD\n   6:\t\tselect NLS\n   7:\t\tselect NLS_UTF8\n   8:\t\tselect LEGACY_DIRECT_IO\n   9:\t\thelp\n  10:\t\t  If you say Y here, you will be able to mount extended format\n  11:\t\t  Macintosh-formatted hard drive partitions with full read-write access.\n  12:\t\n  13:\t\t  This file system is often called HFS+ and was introduced with\n  14:\t\t  MacOS 8. It includes all Mac specific filesystem data such as\n  15:\t\t  data forks and creator codes, but it also has several UNIX\n  16:\t\t  style features such as file ownership and permissions.\n  17:\t\n  18:\tconfig HFSPLUS_KUNIT_TEST\n  19:\t\ttristate \"KUnit tests for HFS+ filesystem\" if !KUNIT_ALL_TESTS\n  20:\t\tdepends on HFSPLUS_FS \u0026\u0026 KUNIT\n  21:\t\tdefault KUNIT_ALL_TESTS\n  22:\t\thelp\n  23:\t\t  This builds KUnit tests for the HFS+ filesystem.\n  24:\t\n  25:\t\t  KUnit tests run during boot and output the results to the debug\n  26:\t\t  log in TAP format (https://testanything.org/). Only useful for\n  27:\t\t  kernel devs running KUnit test harness and are not for inclusion\n  28:\t\t  into a production build.\n  29:\t\n  30:\t\t  For more information on KUnit and unit tests in general please\n  31:\t\t  refer to the KUnit documentation in Documentation/dev-tools/kunit/.\n"
}

7/2 2026/10/10 00:10 llm patch-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 28591
							output: 258
							thoughts: 2989
						

Thoughts:
**HFS+ Filesystem Fuzzing Analysis**

Okay, let's break this down. HFS+ is a standard disk filesystem (`HFSPLUS_FS`) and it's easily reachable in QEMU and standard fuzzing setups. This patch introduces significant changes related to timestamp handling *beyond* the year 2040. Specifically, when timestamps exceed `HFS_MAX_TIMESTAMP_SECS`, the code now stores them as internal xattrs on catalog records, which is a big shift. This involves new or modified logic in various crucial areas: inode operations, catalog management, file operations, xattr management, and timestamp updates, and btree operations, which screams potential for issues. I'm seeing changes across a significant portion of the driver.

The core of the change lies in how timestamps are *written* to disk (using xattrs), *read* from disk (reading the new xattrs), and interacting with btree structures. There's a lot of interaction with `hfsplus_set_timestamp_xattr`, `hfsplus_create_attributes_file`, `hfsplus_set_attr_cnid`, and these functions are all touchpoints. There's potential for deadlocks, btree corruption, memory leaks, integer overflows, and a whole host of errors. Given the scope and complexity of this logic, the conclusion is obvious: `WorthFuzzing = true`.

For configuration, it's straightforward. `HFSPLUS_FS` is the primary and only required configuration. The kernel *does* need `BLOCK`, but that's a baseline requirement. No other dependencies are strictly required. So, `EnableConfigs` will be `["HFSPLUS_FS"]`.

Now, the important part: identifying the `FocusSymbols`. We're going to list the functions that implement the *new* logic or directly handle the changes. We want specific entry points, not ubiquitous functions. The crucial ones are:

-   `hfsplus_read_timestamp`
-   `hfsplus_get_timestamp_xattr`
-   `hfsplus_set_timestamp_xattr`
-   `hfsplus_remove_timestamp_xattr`
-   `hfsplus_set_attr_cnid`
-   `hfsplus_cat_write_inode`
-   `hfsplus_cat_read_inode`
-   `hfsplus_create_cat`

These are all *directly* involved in the timestamp extension logic or the underlying btree and xattr operations that support it. These are all non-static, or have non-static functions that call them. They are all entrypoints to the functionality being modified.

A sanity check of all instructions, and rationale, yields the following:
*   `set-results` tool called once.
*   `WorthFuzzing`: true
*   `Reasoning`: Concise explanation provided above
*   `EnableConfigs`: ["HFSPLUS_FS"]
*   `FocusSymbols`:  the targeted function symbols are listed above.



8/2 2026/10/10 00:11 tool set-results
0m Args:
{
  "EnableConfigs": [
    "HFSPLUS_FS"
  ],
  "FocusSymbols": [
    "hfsplus_read_timestamp",
    "hfsplus_get_timestamp_xattr",
    "hfsplus_set_timestamp_xattr",
    "hfsplus_remove_timestamp_xattr",
    "hfsplus_set_attr_cnid",
    "hfsplus_cat_write_inode",
    "hfsplus_cat_read_inode",
    "hfsplus_create_cat"
  ],
  "Reasoning": "The patch implements an extension in the HFS+ filesystem to support timestamps beyond February 2040 by storing 64-bit timestamps in internal extended attributes ('linux_hfs.*'). This adds new logic for catalog record creation, inode read/write operations, and btree attribute management. The code is fully reachable via standard filesystem syscalls (mount, creat, utimes/futimens, statx) on loopback HFS+ images in virtualized environments, making it a good candidate for fuzzing to catch btree inconsistencies, locking issues, or edge cases in timestamp conversion and xattr handling.",
  "WorthFuzzing": true
}

Results:
{
  "EnableConfigs": [
    "HFSPLUS_FS"
  ],
  "FocusSymbols": [
    "hfsplus_read_timestamp",
    "hfsplus_get_timestamp_xattr",
    "hfsplus_set_timestamp_xattr",
    "hfsplus_remove_timestamp_xattr",
    "hfsplus_set_attr_cnid",
    "hfsplus_cat_write_inode",
    "hfsplus_cat_read_inode",
    "hfsplus_create_cat"
  ],
  "Reasoning": "The patch implements an extension in the HFS+ filesystem to support timestamps beyond February 2040 by storing 64-bit timestamps in internal extended attributes ('linux_hfs.*'). This adds new logic for catalog record creation, inode read/write operations, and btree attribute management. The code is fully reachable via standard filesystem syscalls (mount, creat, utimes/futimens, statx) on loopback HFS+ images in virtualized environments, making it a good candidate for fuzzing to catch btree inconsistencies, locking issues, or edge cases in timestamp conversion and xattr handling.",
  "WorthFuzzing": true
}

9/1 2026/10/10 00:11 action If
2m Args:
{
  "WorthFuzzing": true
}

Results:
null

10/2 2026/10/10 00:11 agent kmsan-evaluator
2m Model:
core

Args:
null

Results:
{
  "KMSANReasoning": "The patch series adds support for 64-bit timestamps in the HFS+ filesystem by encoding timestamps beyond February 2040 (HFS_MAX_TIMESTAMP_SECS) in internal extended attributes prefixed with \"linux_hfs.\", along with corresponding KUnit tests.\n\nEvaluating against KMSAN vs KASAN:\n1. Stack and Heap Initialization: All new local stack structures (`struct hfsplus_timestamps`, `struct timespec64`, `struct hfsplus_xattr_changes`) are zero-initialized with `= {0}` at declaration. Inode structure fields such as `birthdate` are explicitly initialized to 0 in `hfsplus_iget()`. Dynamic name allocations use `kzalloc()`.\n2. Info-leak risks to user space: Internal timestamp xattrs are explicitly filtered out from user space visibility in `can_list()`, `hfsplus_osx_getxattr()`, and `hfsplus_osx_setxattr()`. In `hfsplus_getattr()`, both `stat-\u003ebtime.tv_sec` and `stat-\u003ebtime.tv_nsec` are explicitly assigned without leaving padding or fields uninitialized.\n3. Read safety: Output pointers in `hfsplus_read_timestamp()` are initialized on entry, and `hfsplus_get_timestamp_xattr()` validates that the xattr payload size matches `sizeof(value)` before interpreting the data.\n4. Applicability of other sanitizers: Any potential defects related to corrupted on-disk btree metadata, buffer bounds, or lock contention are detected by standard KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory reads or info-leak risks are exposed, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

Instruction:
You are an expert Linux kernel security engineer specializing in kernel memory
error detectors (KASAN and KMSAN). Your job is to review the provided patch series and
determine if the code changes justify spawning a dedicated KMSAN (KernelMemorySanitizer)
fuzzing session in addition to standard KASAN fuzzing.

CRITICAL DISTINCTION BETWEEN KASAN AND KMSAN:
- Standard KASAN kernel builds (upstream-apparmor-kasan.config) already enable
  a comprehensive suite of debugging tools and sanitizers, including KASAN
  (out-of-bounds accesses, use-after-free, double free, invalid free), LOCKDEP
  (locking bugs and deadlocks), UB-sanitizers, and memory corruption checks.
- KMSAN (KernelMemorySanitizer) detects reads of UNINITIALIZED memory (stack, heap,
  or page allocations) and kernel-to-user memory info-leaks.

Rule: THERE IS NO SENSE IN RUNNING A KMSAN SESSION IF A BUG CAN BE CAUGHT BY KASAN,
LOCKDEP, OR OTHER STANDARD BUG DETECTORS.
A dedicated KMSAN fuzzing session incurs significant resource costs. You must ONLY
set NeedsKMSAN=true if the code changes introduce or expose UNINITIALIZED MEMORY risks
that are detected ONLY by KMSAN.

Look holistically at the patch series and surrounding code. Even if no direct
uninitialized field accesses or new buffer allocations are added in the diff itself,
a patch may alter control flow, bounds checking, or data length calculations in ways
that change how the rest of the code operates on existing buffers (e.g. allowing
uninitialized stack/heap memory to be read, copied to user space, or used in control
flow). Do not hesitate to use your code access tools to inspect the surrounding code,
called functions, and callers.

Set NeedsKMSAN=true ONLY IF the patch introduces or modifies:
1. Kernel structures sent to user space (via copy_to_user, put_user, netlink skb
   attributes, ioctl output arguments, socket options, or BPF buffers) where fields
   or structure padding might not be fully initialized/zeroed.
2. Conditional logic or branching that depends on potentially uninitialized variables
   or struct fields.
3. Allocation or initialization of complex data structures where uninitialized fields
   could be read later in reachable code paths.
4. Bounds checks, lengths, or logic in a way that allows surrounding code to access
   uninitialized bytes of existing buffers.

Set NeedsKMSAN=false IF:
- The code changes primarily risk out-of-bounds access, array overflows, NULL pointer
  dereferences, locking deadlocks, or use-after-free bugs (these are already caught
  by KASAN, LOCKDEP, or standard bug detectors).
- All stack/heap structures touched or introduced by the patch are fully zeroed
  or initialized (e.g. using = {0}, memset, kzalloc) before being read or copied.
- The patch does not introduce any risk of uninitialized memory usage or info-leaks.

Use your code access tools to inspect the surrounding code if necessary, then provide
detailed KMSANReasoning contrasting KASAN vs KMSAN applicability for this patch.
Prefer calling several tools at the same time to save round-trips.


Use set-results tool to provide results of the analysis.
It must be called exactly once before the final reply.
Ignore results of this tool.

Prompt:
Target architecture: amd64

For your convenience, here is the diff of the changes:
commit 9a5a9dae0ad59d6f64ca490471bca3aebb1151b4
Author: syz-cluster <triage@syzkaller.com>
Date:   Sat Oct 10 00:10:33 2026 +0000

    syz-cluster: applied patch under review

diff --git a/fs/hfs/hfs_fs.h b/fs/hfs/hfs_fs.h
index e250f87a5e336..659b0ba733d49 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/Makefile b/fs/hfsplus/Makefile
index f2a9ae697e811..94a262ae91776 100644
--- a/fs/hfsplus/Makefile
+++ b/fs/hfsplus/Makefile
@@ -10,4 +10,4 @@ hfsplus-objs := super.o options.o inode.o ioctl.o extents.o catalog.o dir.o btre
 		attributes.o xattr.o xattr_user.o xattr_security.o xattr_trusted.o
 
 # KUnit tests
-obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o
+obj-$(CONFIG_HFSPLUS_KUNIT_TEST) += unicode_test.o time_test.o
diff --git a/fs/hfsplus/attributes.c b/fs/hfsplus/attributes.c
index 7c2e589d45538..51b18d780f2c0 100644
--- a/fs/hfsplus/attributes.c
+++ b/fs/hfsplus/attributes.c
@@ -206,7 +206,8 @@ int hfsplus_attr_exists(struct inode *inode, const char *name)
 }
 
 static
-int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
+int hfsplus_create_attr_nolock(struct inode *inode, u32 cnid,
+				const char *name,
 				const void *value, size_t size,
 				struct hfs_find_data *fd,
 				hfsplus_attr_entry *entry_ptr)
@@ -215,12 +216,12 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
 	int entry_size;
 	int err;
 
-	hfs_dbg("name %s, ino %llu\n",
-		name ? name : NULL, inode->i_ino);
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
 
 	if (name) {
 		err = hfsplus_attr_build_key(sb, fd->search_key,
-						inode->i_ino, name);
+						cnid, name);
 		if (err)
 			return err;
 	} else
@@ -229,7 +230,7 @@ int hfsplus_create_attr_nolock(struct inode *inode, const char *name,
 	/* Mac OS X supports only inline data attributes. */
 	entry_size = hfsplus_attr_build_record(entry_ptr,
 					HFSPLUS_ATTR_INLINE_DATA,
-					inode->i_ino,
+					cnid,
 					value, size);
 	if (entry_size == HFSPLUS_INVALID_ATTR_RECORD) {
 		if (size > HFSPLUS_MAX_INLINE_DATA_SIZE)
@@ -289,8 +290,8 @@ int hfsplus_create_attr(struct inode *inode,
 	if (err)
 		goto failed_create_attr;
 
-	err = hfsplus_create_attr_nolock(inode, name, value, size,
-					 &fd, entry_ptr);
+	err = hfsplus_create_attr_nolock(inode, (u32)inode->i_ino, name,
+					 value, size, &fd, entry_ptr);
 	if (err)
 		goto failed_create_attr;
 
@@ -347,18 +348,19 @@ static int __hfsplus_delete_attr(struct inode *inode, u32 cnid,
 }
 
 static
-int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,
+int hfsplus_delete_attr_nolock(struct inode *inode, u32 cnid,
+				const char *name,
 				struct hfs_find_data *fd)
 {
 	struct super_block *sb = inode->i_sb;
 	int err;
 
-	hfs_dbg("name %s, ino %llu\n",
-		name ? name : NULL, inode->i_ino);
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
 
 	if (name) {
 		err = hfsplus_attr_build_key(sb, fd->search_key,
-						inode->i_ino, name);
+						cnid, name);
 		if (err)
 			return err;
 	} else {
@@ -373,7 +375,7 @@ int hfsplus_delete_attr_nolock(struct inode *inode, const char *name,
 	} else if (err)
 		return err;
 
-	err = __hfsplus_delete_attr(inode, inode->i_ino, fd);
+	err = __hfsplus_delete_attr(inode, cnid, fd);
 	if (err)
 		return err;
 
@@ -403,7 +405,7 @@ int hfsplus_delete_attr(struct inode *inode, const char *name)
 	if (err)
 		goto out;
 
-	err = hfsplus_delete_attr_nolock(inode, name, &fd);
+	err = hfsplus_delete_attr_nolock(inode, (u32)inode->i_ino, name, &fd);
 	if (err)
 		goto out;
 
@@ -481,12 +483,12 @@ int hfsplus_replace_attr(struct inode *inode,
 	if (err)
 		goto failed_replace_attr;
 
-	err = hfsplus_delete_attr_nolock(inode, name, &fd);
+	err = hfsplus_delete_attr_nolock(inode, (u32)inode->i_ino, name, &fd);
 	if (err)
 		goto failed_replace_attr;
 
-	err = hfsplus_create_attr_nolock(inode, name, value, size,
-					 &fd, entry_ptr);
+	err = hfsplus_create_attr_nolock(inode, (u32)inode->i_ino, name,
+					 value, size, &fd, entry_ptr);
 	if (err)
 		goto failed_replace_attr;
 
@@ -497,3 +499,54 @@ int hfsplus_replace_attr(struct inode *inode,
 	hfsplus_destroy_attr_entry(entry_ptr);
 	return err;
 }
+
+/*
+ * Create or replace the xattr @name of catalog record @cnid.
+ * The @inode is used only for access to the superblock and for
+ * marking the attributes tree as dirty. It is the caller's
+ * responsibility to set HFSPLUS_XATTR_EXISTS in the catalog record.
+ */
+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,
+			  const char *name,
+			  const void *value, size_t size)
+{
+	struct super_block *sb = inode->i_sb;
+	struct hfs_find_data fd;
+	hfsplus_attr_entry *entry_ptr;
+	int err = 0;
+
+	hfs_dbg("name %s, ino %llu, cnid %u\n",
+		name ? name : NULL, inode->i_ino, cnid);
+
+	if (!HFSPLUS_SB(sb)->attr_tree) {
+		pr_err("attributes file doesn't exist\n");
+		return -EINVAL;
+	}
+
+	entry_ptr = hfsplus_alloc_attr_entry();
+	if (!entry_ptr)
+		return -ENOMEM;
+
+	err = hfs_find_init(HFSPLUS_SB(sb)->attr_tree, &fd);
+	if (err)
+		goto failed_init_set_attr;
+
+	/* Fail early and avoid ENOSPC during the btree operation */
+	err = hfs_bmap_reserve(fd.tree, fd.tree->depth + 1);
+	if (err)
+		goto failed_set_attr;
+
+	err = hfsplus_delete_attr_nolock(inode, cnid, name, &fd);
+	if (err && err != -ENOENT && err != -ENODATA)
+		goto failed_set_attr;
+
+	err = hfsplus_create_attr_nolock(inode, cnid, name,
+					 value, size, &fd, entry_ptr);
+
+failed_set_attr:
+	hfs_find_exit(&fd);
+
+failed_init_set_attr:
+	hfsplus_destroy_attr_entry(entry_ptr);
+	return err;
+}
diff --git a/fs/hfsplus/catalog.c b/fs/hfsplus/catalog.c
index fe11c4b6dd997..d710419db3591 100644
--- a/fs/hfsplus/catalog.c
+++ b/fs/hfsplus/catalog.c
@@ -12,6 +12,7 @@
 
 #include "hfsplus_fs.h"
 #include "hfsplus_raw.h"
+#include "xattr.h"
 
 int hfsplus_cat_case_cmp_key(const hfsplus_btree_key *k1,
 			     const hfsplus_btree_key *k2)
@@ -102,10 +103,63 @@ void hfsplus_cat_set_perms(struct inode *inode, struct hfsplus_perm *perms)
 		perms->dev = 0;
 }
 
+static void hfsplus_copy_timestamps2folder(struct hfsplus_timestamps *timestamps,
+					    struct hfsplus_cat_folder *folder)
+{
+	folder->create_date = timestamps->create_date;
+	folder->content_mod_date = timestamps->content_mod_date;
+	folder->attribute_mod_date = timestamps->attribute_mod_date;
+	folder->access_date = timestamps->access_date;
+}
+
+static void hfsplus_copy_timestamps2file(struct hfsplus_timestamps *timestamps,
+					  struct hfsplus_cat_file *file)
+{
+	file->create_date = timestamps->create_date;
+	file->content_mod_date = timestamps->content_mod_date;
+	file->attribute_mod_date = timestamps->attribute_mod_date;
+	file->access_date = timestamps->access_date;
+}
+
+/*
+ * Set creation date of new inode. The creation date after February 2040
+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK in the catalog record. The real
+ * value is saved into internal xattr by hfsplus_create_cat().
+ */
+static void hfsplus_set_create_date(struct inode *inode,
+				     struct hfsplus_timestamps *timestamps)
+{
+	time64_t ut = ktime_get_real_seconds();
+	__be32 create_date = __hfsp_ut2mt(ut);
+
+	HFSPLUS_I(inode)->birthdate = ut;
+	HFSPLUS_I(inode)->create_date = create_date;
+
+	timestamps->create_date = create_date;
+	timestamps->content_mod_date = create_date;
+	timestamps->attribute_mod_date = create_date;
+	timestamps->access_date = create_date;
+}
+
+/*
+ * Real creation date of new catalog record. The hard link record
+ * inherits the creation date of the hidden directory.
+ */
+static time64_t hfsplus_cat_record_birthdate(u32 cnid, struct inode *inode)
+{
+	struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb);
+
+	if (!S_ISDIR(inode->i_mode) && cnid != inode->i_ino)
+		return HFSPLUS_I(sbi->hidden_dir)->birthdate;
+
+	return HFSPLUS_I(inode)->birthdate;
+}
+
 static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
-		u32 cnid, struct inode *inode)
+				    u32 cnid, struct inode *inode)
 {
 	struct hfsplus_sb_info *sbi = HFSPLUS_SB(inode->i_sb);
+	struct hfsplus_timestamps timestamps = {0};
 
 	if (S_ISDIR(inode->i_mode)) {
 		struct hfsplus_cat_folder *folder;
@@ -116,11 +170,12 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 		if (test_bit(HFSPLUS_SB_HFSX, &sbi->flags))
 			folder->flags |= cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT);
 		folder->id = cpu_to_be32(inode->i_ino);
-		HFSPLUS_I(inode)->create_date =
-			folder->create_date =
-			folder->content_mod_date =
-			folder->attribute_mod_date =
-			folder->access_date = hfsp_now2mt();
+
+		hfsplus_set_create_date(inode, &timestamps);
+		hfsplus_copy_timestamps2folder(&timestamps, folder);
+		if (hfsp_ut_needs_xattr(HFSPLUS_I(inode)->birthdate))
+			folder->flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+
 		hfsplus_cat_set_perms(inode, &folder->permissions);
 		if (inode == sbi->hidden_dir)
 			/* invisible and namelocked */
@@ -134,11 +189,23 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 		file->type = cpu_to_be16(HFSPLUS_FILE);
 		file->flags = cpu_to_be16(HFSPLUS_FILE_THREAD_EXISTS);
 		file->id = cpu_to_be32(cnid);
-		HFSPLUS_I(inode)->create_date =
-			file->create_date =
-			file->content_mod_date =
-			file->attribute_mod_date =
-			file->access_date = hfsp_now2mt();
+
+		if (cnid == inode->i_ino) {
+			hfsplus_set_create_date(inode, &timestamps);
+		} else {
+			/*
+			 * Hard link record: don't change creation date
+			 * of the linked inode.
+			 */
+			__be32 now = __hfsp_ut2mt(ktime_get_real_seconds());
+
+			timestamps.create_date = now;
+			timestamps.content_mod_date = now;
+			timestamps.attribute_mod_date = now;
+			timestamps.access_date = now;
+		}
+		hfsplus_copy_timestamps2file(&timestamps, file);
+
 		if (cnid == inode->i_ino) {
 			hfsplus_cat_set_perms(inode, &file->permissions);
 			if (S_ISLNK(inode->i_mode)) {
@@ -169,6 +236,10 @@ static int hfsplus_cat_build_record(hfsplus_cat_entry *entry,
 			file->permissions.dev =
 				cpu_to_be32(HFSPLUS_I(inode)->linkid);
 		}
+
+		if (hfsp_ut_needs_xattr(hfsplus_cat_record_birthdate(cnid, inode)))
+			file->flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+
 		return sizeof(*file);
 	}
 }
@@ -256,6 +327,7 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	struct super_block *sb = dir->i_sb;
 	struct hfs_find_data fd;
 	hfsplus_cat_entry entry;
+	time64_t birthdate;
 	int entry_size;
 	int err;
 
@@ -309,6 +381,22 @@ int hfsplus_create_cat(u32 cnid, struct inode *dir,
 	if (err)
 		goto err1;
 
+	birthdate = hfsplus_cat_record_birthdate(cnid, inode);
+	if (hfsp_ut_needs_xattr(birthdate)) {
+		/*
+		 * Failure is not critical: the creation date
+		 * will be read as HFS_MAX_TIMESTAMP_SECS.
+		 */
+		err = hfsplus_set_timestamp_xattr(inode, cnid,
+						  XATTR_LINUX_CREATE_DATE_NAME,
+						  birthdate);
+		if (err) {
+			pr_warn("fail to save creation date: cnid %u, err %d\n",
+				cnid, err);
+			err = 0;
+		}
+	}
+
 	dir->i_size++;
 	if (S_ISDIR(inode->i_mode))
 		hfsplus_subfolders_inc(dir);
diff --git a/fs/hfsplus/dir.c b/fs/hfsplus/dir.c
index 51fcba2e6d403..af0520ebced70 100644
--- a/fs/hfsplus/dir.c
+++ b/fs/hfsplus/dir.c
@@ -26,6 +26,43 @@ static inline void hfsplus_instantiate(struct dentry *dentry,
 	d_instantiate(dentry, inode);
 }
 
+/*
+ * Check that creation date of catalog record @cnid is identical to
+ * creation date of @inode. If the creation date is after February 2040,
+ * then the real value is compared by means of internal xattr.
+ */
+static inline bool is_create_date_identical(struct inode *inode,
+					    u32 cnid, __be32 create_date)
+{
+	time64_t timestamp;
+	bool has_xattr;
+	int err;
+
+	if (create_date != HFSPLUS_I(inode)->create_date)
+		return false;
+
+	if (!hfsp_mt_is_ext_timestamp(create_date))
+		return true;
+
+	err = hfsplus_read_timestamp(inode->i_sb, cnid,
+				     XATTR_LINUX_CREATE_DATE_NAME,
+				     create_date, &timestamp, &has_xattr);
+	if (err) {
+		pr_err("timestamp extraction failure: cnid %u, err %d\n",
+			cnid, err);
+		return false;
+	}
+
+	/*
+	 * The record without xattr (for example, created by Mac OS X)
+	 * cannot be distinguished by means of 64-bit timestamp.
+	 */
+	if (!has_xattr)
+		return true;
+
+	return timestamp == HFSPLUS_I(inode)->birthdate;
+}
+
 /* Find the entry inside dir named dentry->d_name */
 static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,
 				     unsigned int flags)
@@ -78,12 +115,12 @@ static struct dentry *hfsplus_lookup(struct inode *dir, struct dentry *dentry,
 				entry.file.user_info.fdCreator ==
 				cpu_to_be32(HFSP_HFSPLUS_CREATOR) &&
 				HFSPLUS_SB(sb)->hidden_dir &&
-				(entry.file.create_date ==
-					HFSPLUS_I(HFSPLUS_SB(sb)->hidden_dir)->
-						create_date ||
-				entry.file.create_date ==
-					HFSPLUS_I(d_inode(sb->s_root))->
-						create_date)) {
+				(is_create_date_identical(
+					HFSPLUS_SB(sb)->hidden_dir,
+					cnid, entry.file.create_date) ||
+				is_create_date_identical(
+					d_inode(sb->s_root),
+					cnid, entry.file.create_date))) {
 			struct qstr str;
 			char name[32];
 
diff --git a/fs/hfsplus/hfsplus_fs.h b/fs/hfsplus/hfsplus_fs.h
index 1e5b58e6a13fc..637bba65568ec 100644
--- a/fs/hfsplus/hfsplus_fs.h
+++ b/fs/hfsplus/hfsplus_fs.h
@@ -175,6 +175,13 @@ static inline struct hfsplus_sb_info *HFSPLUS_SB(struct super_block *sb)
 	return sb->s_fs_info;
 }
 
+struct hfsplus_timestamps {
+	__be32 create_date;
+	__be32 content_mod_date;
+	__be32 attribute_mod_date;
+	__be32 access_date;
+	__be32 backup_date;
+};
 
 struct hfsplus_inode_info {
 	atomic_t opencnt;
@@ -196,7 +203,8 @@ struct hfsplus_inode_info {
 	 * Immutable data.
 	 */
 	struct inode *rsrc_inode;
-	__be32 create_date;
+	__be32 create_date;	/* on-disk (maybe clamped) creation date */
+	time64_t birthdate;	/* real creation date (seconds since 1970) */
 
 	/*
 	 * Protected by sbi->vh_mutex.
@@ -227,6 +235,9 @@ struct hfsplus_inode_info {
 #define HFSPLUS_I_EXT_DIRTY	2	/* has changes in the extent tree */
 #define HFSPLUS_I_ALLOC_DIRTY	3	/* has changes in the allocation file */
 #define HFSPLUS_I_ATTR_DIRTY	4	/* has changes in the attributes tree */
+#define HFSPLUS_I_ATIME_XATTR	5	/* access date is stored in xattr */
+#define HFSPLUS_I_MTIME_XATTR	6	/* content mod date is stored in xattr */
+#define HFSPLUS_I_CTIME_XATTR	7	/* attribute mod date is stored in xattr */
 
 #define HFSPLUS_IS_RSRC(inode) \
 	test_bit(HFSPLUS_I_RSRC, &HFSPLUS_I(inode)->flags)
@@ -351,6 +362,9 @@ int hfsplus_delete_all_attrs(struct inode *dir, u32 cnid);
 int hfsplus_replace_attr(struct inode *inode,
 			 const char *name,
 			 const void *value, size_t size);
+int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,
+			  const char *name,
+			  const void *value, size_t size);
 
 /* bitmap.c */
 int hfsplus_block_allocate(struct super_block *sb, u32 size, u32 offset,
@@ -467,6 +481,9 @@ int hfsplus_file_fsync(struct file *file, loff_t start, loff_t end,
 int hfsplus_fileattr_get(struct dentry *dentry, struct file_kattr *fa);
 int hfsplus_fileattr_set(struct mnt_idmap *idmap,
 			 struct dentry *dentry, struct file_kattr *fa);
+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,
+			   const char *name, __be32 mt,
+			   time64_t *ut, bool *has_xattr);
 
 /* ioctl.c */
 long hfsplus_ioctl(struct file *filp, unsigned int cmd, unsigned long arg);
@@ -532,23 +549,42 @@ 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.
+ * The on-disk 32-bit timestamps cover the range from
+ * HFS_MIN_TIMESTAMP_SECS (Jan. 1, 1904) till HFS_MAX_TIMESTAMP_SECS
+ * (Feb. 6, 2040). The timestamps out of this range are clamped.
+ * The on-disk value HFSPLUS_EXT_TIMESTAMP_MARK (maximal value) means
+ * that real timestamp could be stored in the internal xattr of
+ * the catalog record. It is Linux specific extension of
+ * HFS+ on-disk layout. It makes possible to support timestamps
+ * till HFSPLUS_MAX_TIMESTAMP_SECS.
  */
-#define HFSPLUS_UTC_OFFSET 2082844800U
+#define HFSPLUS_EXT_TIMESTAMP_MARK	cpu_to_be32(U32_MAX)
+#define HFSPLUS_MAX_TIMESTAMP_SECS	TIME64_MAX
 
 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 = clamp_t(time64_t, ut,
+		     HFS_MIN_TIMESTAMP_SECS, HFS_MAX_TIMESTAMP_SECS);
+	ut += HFS_UTC_OFFSET;
+
+	return cpu_to_be32(lower_32_bits(ut));
+}
+
+static inline bool hfsp_ut_needs_xattr(time64_t ut)
+{
+	return ut >= HFS_MAX_TIMESTAMP_SECS;
+}
+
+static inline bool hfsp_mt_is_ext_timestamp(__be32 mt)
+{
+	return mt == HFSPLUS_EXT_TIMESTAMP_MARK;
 }
 
 static inline enum hfsplus_btree_mutex_classes
diff --git a/fs/hfsplus/inode.c b/fs/hfsplus/inode.c
index 2ce6de574fa6e..0df58dfda5a3e 100644
--- a/fs/hfsplus/inode.c
+++ b/fs/hfsplus/inode.c
@@ -18,6 +18,7 @@
 #include <linux/cred.h>
 #include <linux/uio.h>
 #include <linux/fileattr.h>
+#include <kunit/visibility.h>
 
 #include "hfsplus_fs.h"
 #include "hfsplus_raw.h"
@@ -344,7 +345,8 @@ int hfsplus_getattr(struct mnt_idmap *idmap, const struct path *path,
 
 	if (request_mask & STATX_BTIME) {
 		stat->result_mask |= STATX_BTIME;
-		stat->btime = hfsp_mt2ut(hip->create_date);
+		stat->btime.tv_sec = hip->birthdate;
+		stat->btime.tv_nsec = 0;
 	}
 
 	if (inode->i_flags & S_APPEND)
@@ -598,9 +600,139 @@ void hfsplus_inode_write_fork(struct inode *inode,
 	fork->total_blocks = cpu_to_be32(HFSPLUS_I(inode)->alloc_blocks);
 }
 
+/*
+ * hfsplus_read_timestamp - convert on-disk timestamp into 1970-base one
+ * @sb: superblock
+ * @cnid: catalog record ID
+ * @name: name of internal xattr that can keep the real timestamp
+ * @mt: on-disk timestamp
+ * @ut: pointer on converted timestamp [out]
+ * @has_xattr: pointer on flag that xattr exists [out]
+ *
+ * If on-disk timestamp is equal to HFSPLUS_EXT_TIMESTAMP_MARK, then
+ * the real timestamp (after February 2040) could be stored in
+ * the internal xattr. If the xattr is absent (for example, the record
+ * has been created by Mac OS X), then HFS_MAX_TIMESTAMP_SECS is used.
+ */
+int hfsplus_read_timestamp(struct super_block *sb, u32 cnid,
+			   const char *name, __be32 mt,
+			   time64_t *ut, bool *has_xattr)
+{
+	time64_t timestamp;
+	int err;
+
+	*ut = __hfsp_mt2ut(mt);
+	*has_xattr = false;
+
+	if (!hfsp_mt_is_ext_timestamp(mt))
+		return 0;
+
+	err = hfsplus_get_timestamp_xattr(sb, cnid, name, &timestamp);
+	if (err == -ENODATA || err == -EOPNOTSUPP) {
+		/* timestamp is really equal to HFS_MAX_TIMESTAMP_SECS */
+		return 0;
+	} else if (unlikely(err)) {
+		pr_warn("fail to extract timestamp: cnid %u, name %s, err %d\n",
+			cnid, name, err);
+		return err;
+	}
+
+	if (timestamp < HFS_MAX_TIMESTAMP_SECS) {
+		pr_warn("invalid timestamp in xattr: cnid %u, name %s, timestamp %lld\n",
+			cnid, name, timestamp);
+		/* keep xattr to be deleted or overwritten */
+		*has_xattr = true;
+	} else {
+		*ut = timestamp;
+		*has_xattr = true;
+	}
+
+	return 0;
+}
+EXPORT_SYMBOL_IF_KUNIT(hfsplus_read_timestamp);
+
+static int hfsplus_inode_read_timestamps(struct inode *inode,
+					 struct hfsplus_timestamps *timestamps)
+{
+	struct super_block *sb = inode->i_sb;
+	struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
+	u32 cnid = inode->i_ino;
+	struct timespec64 ts = {0};
+	bool has_xattr;
+	int res = 0;
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_ACCESS_DATE_NAME,
+				     timestamps->access_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_atime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_ATIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+				     timestamps->content_mod_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_mtime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_MTIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+				     timestamps->attribute_mod_date,
+				     &ts.tv_sec, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	inode_set_ctime_to_ts(inode, ts);
+	if (has_xattr)
+		set_bit(HFSPLUS_I_CTIME_XATTR, &hip->flags);
+
+	res = hfsplus_read_timestamp(sb, cnid,
+				     XATTR_LINUX_CREATE_DATE_NAME,
+				     timestamps->create_date,
+				     &hip->birthdate, &has_xattr);
+	if (res)
+		goto finish_read_timestamps;
+
+	hip->create_date = timestamps->create_date;
+
+finish_read_timestamps:
+	return res;
+}
+
+static inline void
+hfsplus_copy_folder2timestamps(struct hfsplus_cat_folder *folder,
+				struct hfsplus_timestamps *timestamps)
+{
+	timestamps->create_date = folder->create_date;
+	timestamps->content_mod_date = folder->content_mod_date;
+	timestamps->attribute_mod_date = folder->attribute_mod_date;
+	timestamps->access_date = folder->access_date;
+	timestamps->backup_date = folder->backup_date;
+}
+
+static inline void
+hfsplus_copy_file2timestamps(struct hfsplus_cat_file *file,
+			     struct hfsplus_timestamps *timestamps)
+{
+	timestamps->create_date = file->create_date;
+	timestamps->content_mod_date = file->content_mod_date;
+	timestamps->attribute_mod_date = file->attribute_mod_date;
+	timestamps->access_date = file->access_date;
+	timestamps->backup_date = file->backup_date;
+}
+
 int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 {
 	hfsplus_cat_entry entry;
+	struct hfsplus_timestamps timestamps = {0};
 	int res = 0;
 	u16 type;
 
@@ -622,12 +754,14 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 			goto out;
 		set_nlink(inode, 1);
 		inode->i_size = 2 + be32_to_cpu(folder->valence);
-		inode_set_atime_to_ts(inode, hfsp_mt2ut(folder->access_date));
-		inode_set_mtime_to_ts(inode,
-				      hfsp_mt2ut(folder->content_mod_date));
-		inode_set_ctime_to_ts(inode,
-				      hfsp_mt2ut(folder->attribute_mod_date));
-		HFSPLUS_I(inode)->create_date = folder->create_date;
+
+		hfsplus_copy_folder2timestamps(folder, &timestamps);
+		res = hfsplus_inode_read_timestamps(inode, &timestamps);
+		if (res) {
+			res = -EIO;
+			goto out;
+		}
+
 		HFSPLUS_I(inode)->fs_blocks = 0;
 		if (folder->flags & cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {
 			HFSPLUS_I(inode)->subfolders =
@@ -668,12 +802,13 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 			init_special_inode(inode, inode->i_mode,
 					   be32_to_cpu(file->permissions.dev));
 		}
-		inode_set_atime_to_ts(inode, hfsp_mt2ut(file->access_date));
-		inode_set_mtime_to_ts(inode,
-				      hfsp_mt2ut(file->content_mod_date));
-		inode_set_ctime_to_ts(inode,
-				      hfsp_mt2ut(file->attribute_mod_date));
-		HFSPLUS_I(inode)->create_date = file->create_date;
+
+		hfsplus_copy_file2timestamps(file, &timestamps);
+		res = hfsplus_inode_read_timestamps(inode, &timestamps);
+		if (res) {
+			res = -EIO;
+			goto out;
+		}
 	} else {
 		pr_err("bad catalog entry used to create inode\n");
 		res = -EIO;
@@ -682,12 +817,126 @@ int hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)
 	return res;
 }
 
+struct hfsplus_xattr_changes {
+	bool added;
+	bool removed;
+};
+
+/*
+ * Convert @ts into on-disk timestamp. The timestamp after February 2040
+ * is stored as HFSPLUS_EXT_TIMESTAMP_MARK with the real value in
+ * the internal xattr @name. The stale xattr is removed if timestamp
+ * becomes representable by on-disk field.
+ */
+static int hfsplus_inode_save_timestamp(struct inode *inode,
+					const char *name, int xattr_bit,
+					struct timespec64 ts, __be32 *date,
+					struct hfsplus_xattr_changes *xattr)
+{
+	struct hfsplus_inode_info *hip = HFSPLUS_I(inode);
+	time64_t stored;
+	int err;
+
+	*date = __hfsp_ut2mt(ts.tv_sec);
+
+	if (hfsp_ut_needs_xattr(ts.tv_sec)) {
+		/* don't rewrite xattr if nothing has been changed */
+		if (test_bit(xattr_bit, &hip->flags)) {
+			err = hfsplus_get_timestamp_xattr(inode->i_sb,
+							  inode->i_ino,
+							  name,
+							  &stored);
+			if (!err && stored == ts.tv_sec)
+				return 0;
+		}
+
+		err = hfsplus_set_timestamp_xattr(inode, inode->i_ino,
+						  name, ts.tv_sec);
+		if (err) {
+			clear_bit(xattr_bit, &hip->flags);
+			return err;
+		}
+
+		set_bit(xattr_bit, &hip->flags);
+		xattr->added = true;
+	} else if (test_bit(xattr_bit, &hip->flags)) {
+		err = hfsplus_remove_timestamp_xattr(inode, name);
+		if (err)
+			return err;
+
+		clear_bit(xattr_bit, &hip->flags);
+		xattr->removed = true;
+	}
+
+	return 0;
+}
+
+static void hfsplus_inode_save_timestamps(struct inode *inode,
+					  struct hfsplus_timestamps *timestamps,
+					  struct hfsplus_xattr_changes *changes)
+{
+	int res;
+
+	/*
+	 * Failure of xattr operation is not critical. The timestamp
+	 * is clamped by HFS_MAX_TIMESTAMP_SECS in such case.
+	 * But the rest of the catalog record must be saved anyway.
+	 */
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_ACCESS_DATE_NAME,
+					   HFSPLUS_I_ATIME_XATTR,
+					   inode_get_atime(inode),
+					   &timestamps->access_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save access date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+					   HFSPLUS_I_MTIME_XATTR,
+					   inode_get_mtime(inode),
+					   &timestamps->content_mod_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save content mod date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+
+	res = hfsplus_inode_save_timestamp(inode,
+					   XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+					   HFSPLUS_I_CTIME_XATTR,
+					   inode_get_ctime(inode),
+					   &timestamps->attribute_mod_date,
+					   changes);
+	if (res) {
+		pr_warn_ratelimited("fail to save attribute mod date: ino %llu, err %d\n",
+				    inode->i_ino, res);
+	}
+}
+
+static inline __be16
+hfsplus_update_xattr_exists_flag(struct inode *inode, __be16 flags,
+				 struct hfsplus_xattr_changes *xattr)
+{
+	if (xattr->added)
+		flags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);
+	else if (xattr->removed && !hfsplus_attr_exists(inode, NULL))
+		flags &= cpu_to_be16(~HFSPLUS_XATTR_EXISTS);
+
+	return flags;
+}
+
 int hfsplus_cat_write_inode(struct inode *inode)
 {
 	struct inode *main_inode = inode;
 	struct hfs_btree *tree = HFSPLUS_SB(inode->i_sb)->cat_tree;
 	struct hfs_find_data fd;
 	hfsplus_cat_entry entry;
+	struct hfsplus_timestamps timestamps = {0};
+	struct hfsplus_xattr_changes changes = {0};
 	int res = 0;
 
 	hfs_dbg("inode->i_ino %llu\n", inode->i_ino);
@@ -718,9 +967,15 @@ int hfsplus_cat_write_inode(struct inode *inode)
 					sizeof(struct hfsplus_cat_folder));
 		/* simple node checks? */
 		hfsplus_cat_set_perms(inode, &folder->permissions);
-		folder->access_date = hfsp_ut2mt(inode_get_atime(inode));
-		folder->content_mod_date = hfsp_ut2mt(inode_get_mtime(inode));
-		folder->attribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));
+
+		hfsplus_inode_save_timestamps(inode, &timestamps, &changes);
+		folder->access_date = timestamps.access_date;
+		folder->content_mod_date = timestamps.content_mod_date;
+		folder->attribute_mod_date = timestamps.attribute_mod_date;
+		folder->flags = hfsplus_update_xattr_exists_flag(inode,
+								 folder->flags,
+								 &changes);
+
 		folder->valence = cpu_to_be32(inode->i_size - 2);
 		if (folder->flags & cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {
 			folder->subfolders =
@@ -753,9 +1008,15 @@ int hfsplus_cat_write_inode(struct inode *inode)
 			file->flags |= cpu_to_be16(HFSPLUS_FILE_LOCKED);
 		else
 			file->flags &= cpu_to_be16(~HFSPLUS_FILE_LOCKED);
-		file->access_date = hfsp_ut2mt(inode_get_atime(inode));
-		file->content_mod_date = hfsp_ut2mt(inode_get_mtime(inode));
-		file->attribute_mod_date = hfsp_ut2mt(inode_get_ctime(inode));
+
+		hfsplus_inode_save_timestamps(inode, &timestamps, &changes);
+		file->access_date = timestamps.access_date;
+		file->content_mod_date = timestamps.content_mod_date;
+		file->attribute_mod_date = timestamps.attribute_mod_date;
+		file->flags = hfsplus_update_xattr_exists_flag(inode,
+							       file->flags,
+							       &changes);
+
 		hfs_bnode_write(fd.bnode, &entry, fd.entryoffset,
 					 sizeof(struct hfsplus_cat_file));
 	}
diff --git a/fs/hfsplus/super.c b/fs/hfsplus/super.c
index ff7d6b3336a66..b10b53f6f8f9e 100644
--- a/fs/hfsplus/super.c
+++ b/fs/hfsplus/super.c
@@ -86,6 +86,7 @@ struct inode *hfsplus_iget(struct super_block *sb, unsigned long ino)
 	mutex_init(&HFSPLUS_I(inode)->extents_lock);
 	HFSPLUS_I(inode)->rsrc_inode = NULL;
 	HFSPLUS_I(inode)->create_date = 0;
+	HFSPLUS_I(inode)->birthdate = 0;
 	HFSPLUS_I(inode)->linkid = 0;
 	HFSPLUS_I(inode)->flags = 0;
 	HFSPLUS_I(inode)->fs_blocks = 0;
@@ -511,6 +512,13 @@ 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;
+	/*
+	 * Timestamps after HFS_MAX_TIMESTAMP_SECS are stored in xattrs.
+	 */
+	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/fs/hfsplus/time_test.c b/fs/hfsplus/time_test.c
new file mode 100644
index 0000000000000..4b3312dc0048b
--- /dev/null
+++ b/fs/hfsplus/time_test.c
@@ -0,0 +1,354 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * KUnit tests for HFS+ date/time conversion
+ *
+ * Copyright (C) 2026 Viacheslav Dubeyko <slava@dubeyko.com>
+ */
+
+#include <kunit/test.h>
+#include "hfsplus_fs.h"
+#include "xattr.h"
+
+/* January 1, 1970, 00:00:00 UTC */
+#define TEST_UNIX_EPOCH_SECS		0LL
+/* December 31, 1969, 23:59:59 UTC */
+#define TEST_BEFORE_UNIX_EPOCH_SECS	-1LL
+/* January 1, 2000, 00:00:00 UTC */
+#define TEST_Y2000_SECS			946684800LL
+/* January 19, 2038, 03:14:08 UTC (signed 32-bit overflow) */
+#define TEST_Y2038_SECS			2147483648LL
+/* January 1, 2026, 00:00:00 UTC */
+#define TEST_Y2026_SECS			1767225600LL
+/* January 1, 2100, 00:00:00 UTC */
+#define TEST_Y2100_SECS			4102444800LL
+/* January 1, 1900, 00:00:00 UTC */
+#define TEST_Y1900_SECS			-2208988800LL
+
+static u32 mt_raw(__be32 mt)
+{
+	return be32_to_cpu(mt);
+}
+
+/* Test the time range constants */
+static void hfsplus_time_constants_test(struct kunit *test)
+{
+	/* 32-bit unsigned on-disk field covers exactly U32_MAX seconds */
+	KUNIT_EXPECT_EQ(test, (s64)U32_MAX,
+			HFS_MAX_TIMESTAMP_SECS - HFS_MIN_TIMESTAMP_SECS);
+	KUNIT_EXPECT_EQ(test, -(s64)HFS_UTC_OFFSET, HFS_MIN_TIMESTAMP_SECS);
+
+	/* xattrs extend the supported range beyond February 2040 */
+	KUNIT_EXPECT_GT(test, (s64)HFSPLUS_MAX_TIMESTAMP_SECS,
+			(s64)HFS_MAX_TIMESTAMP_SECS);
+
+	/* the extended timestamp mark is the maximal on-disk value */
+	KUNIT_EXPECT_EQ(test, U32_MAX, mt_raw(HFSPLUS_EXT_TIMESTAMP_MARK));
+}
+
+/* Test conversion of on-disk (1904-based) timestamp into 1970-based one */
+static void hfsplus_mt2ut_test(struct kunit *test)
+{
+	KUNIT_EXPECT_EQ(test, HFS_MIN_TIMESTAMP_SECS,
+			__hfsp_mt2ut(cpu_to_be32(0)));
+	KUNIT_EXPECT_EQ(test, TEST_UNIX_EPOCH_SECS,
+			__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET)));
+	KUNIT_EXPECT_EQ(test, TEST_BEFORE_UNIX_EPOCH_SECS,
+			__hfsp_mt2ut(cpu_to_be32(HFS_UTC_OFFSET - 1)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2000_SECS,
+			__hfsp_mt2ut(cpu_to_be32(3029529600U)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2026_SECS,
+			__hfsp_mt2ut(cpu_to_be32(3850070400U)));
+	KUNIT_EXPECT_EQ(test, TEST_Y2038_SECS,
+			__hfsp_mt2ut(cpu_to_be32(4230328448U)));
+	KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,
+			__hfsp_mt2ut(cpu_to_be32(U32_MAX)));
+	KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS,
+			__hfsp_mt2ut(HFSPLUS_EXT_TIMESTAMP_MARK));
+}
+
+/* Test conversion of 1970-based timestamp into on-disk (1904-based) one */
+static void hfsplus_ut2mt_test(struct kunit *test)
+{
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS)));
+	KUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET,
+			mt_raw(__hfsp_ut2mt(TEST_UNIX_EPOCH_SECS)));
+	KUNIT_EXPECT_EQ(test, 3029529600U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2000_SECS)));
+	KUNIT_EXPECT_EQ(test, 3850070400U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2026_SECS)));
+	KUNIT_EXPECT_EQ(test, 4230328448U,
+			mt_raw(__hfsp_ut2mt(TEST_Y2038_SECS)));
+	KUNIT_EXPECT_EQ(test, U32_MAX - 1,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));
+}
+
+/*
+ * Test timestamps before January 1, 1970 (xfstests generic/258).
+ * The old logic added HFS_UTC_OFFSET to lower 32 bits of negative
+ * value and produced completely wrong result.
+ */
+static void hfsplus_ut2mt_before_1970_test(struct kunit *test)
+{
+	time64_t ut;
+
+	KUNIT_EXPECT_EQ(test, HFS_UTC_OFFSET - 1,
+			mt_raw(__hfsp_ut2mt(TEST_BEFORE_UNIX_EPOCH_SECS)));
+
+	/* January 1, 1960, 00:00:00 UTC */
+	ut = -315619200LL;
+	KUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));
+
+	/* January 1, 1904, 00:00:01 UTC */
+	ut = HFS_MIN_TIMESTAMP_SECS + 1;
+	KUNIT_EXPECT_EQ(test, 1U, mt_raw(__hfsp_ut2mt(ut)));
+	KUNIT_EXPECT_EQ(test, ut, __hfsp_mt2ut(__hfsp_ut2mt(ut)));
+}
+
+/* Test that out-of-range timestamps are clamped instead of wrapped */
+static void hfsplus_ut2mt_clamp_test(struct kunit *test)
+{
+	/* below January 1, 1904 */
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(HFS_MIN_TIMESTAMP_SECS - 1)));
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(TEST_Y1900_SECS)));
+	KUNIT_EXPECT_EQ(test, 0U,
+			mt_raw(__hfsp_ut2mt(TIME64_MIN)));
+
+	/* after February 6, 2040 */
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS + 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(TEST_Y2100_SECS)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFSPLUS_MAX_TIMESTAMP_SECS)));
+
+	/* lower 32 bits of these values would wrap into small dates */
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS +
+					    (s64)U32_MAX + 1)));
+	KUNIT_EXPECT_EQ(test, U32_MAX,
+			mt_raw(__hfsp_ut2mt(1LL << 32)));
+}
+
+/* Test that representable timestamps survive the round trip */
+static void hfsplus_time_roundtrip_test(struct kunit *test)
+{
+	static const time64_t values[] = {
+		HFS_MIN_TIMESTAMP_SECS,
+		HFS_MIN_TIMESTAMP_SECS + 1,
+		TEST_BEFORE_UNIX_EPOCH_SECS,
+		TEST_UNIX_EPOCH_SECS,
+		1,
+		TEST_Y2000_SECS,
+		TEST_Y2026_SECS,
+		(s64)S32_MAX,
+		TEST_Y2038_SECS,
+		HFS_MAX_TIMESTAMP_SECS - 1,
+		HFS_MAX_TIMESTAMP_SECS,
+	};
+	time64_t ut;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(values); i++) {
+		KUNIT_EXPECT_EQ_MSG(test, values[i],
+				    __hfsp_mt2ut(__hfsp_ut2mt(values[i])),
+				    "round trip failed: index %d", i);
+	}
+
+	/* sweep the whole range with a large prime step */
+	for (ut = HFS_MIN_TIMESTAMP_SECS; ut <= HFS_MAX_TIMESTAMP_SECS;
+	     ut += 999983) {
+		if (__hfsp_mt2ut(__hfsp_ut2mt(ut)) != ut) {
+			KUNIT_FAIL(test, "round trip failed: ut %lld", ut);
+			break;
+		}
+	}
+
+	/* every on-disk value maps back to itself */
+	for (i = 0; i < 32; i++) {
+		u32 raw = (u32)BIT_ULL(i);
+
+		KUNIT_EXPECT_EQ(test, raw,
+				mt_raw(__hfsp_ut2mt(__hfsp_mt2ut(cpu_to_be32(raw)))));
+	}
+}
+
+/* Test the decision to keep timestamp in xattr */
+static void hfsplus_ut_needs_xattr_test(struct kunit *test)
+{
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(HFS_MIN_TIMESTAMP_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_BEFORE_UNIX_EPOCH_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_UNIX_EPOCH_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2026_SECS));
+	KUNIT_EXPECT_FALSE(test, hfsp_ut_needs_xattr(TEST_Y2038_SECS));
+	KUNIT_EXPECT_FALSE(test,
+			   hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS - 1));
+
+	KUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS));
+	KUNIT_EXPECT_TRUE(test,
+			  hfsp_ut_needs_xattr(HFS_MAX_TIMESTAMP_SECS + 1));
+	KUNIT_EXPECT_TRUE(test, hfsp_ut_needs_xattr(TEST_Y2100_SECS));
+	KUNIT_EXPECT_TRUE(test,
+			  hfsp_ut_needs_xattr(HFSPLUS_MAX_TIMESTAMP_SECS));
+}
+
+/* Test detection of the extended timestamp mark */
+static void hfsplus_mt_is_ext_timestamp_test(struct kunit *test)
+{
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(HFSPLUS_EXT_TIMESTAMP_MARK));
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX)));
+
+	KUNIT_EXPECT_FALSE(test, hfsp_mt_is_ext_timestamp(cpu_to_be32(0)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(U32_MAX - 1)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(cpu_to_be32(HFS_UTC_OFFSET)));
+
+	/* every timestamp that needs xattr is stored as the mark */
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS)));
+	KUNIT_EXPECT_TRUE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(TEST_Y2100_SECS)));
+	KUNIT_EXPECT_FALSE(test,
+		hfsp_mt_is_ext_timestamp(__hfsp_ut2mt(HFS_MAX_TIMESTAMP_SECS - 1)));
+}
+
+static struct super_block *alloc_mock_sb(struct kunit *test)
+{
+	struct super_block *sb;
+	struct hfsplus_sb_info *sbi;
+
+	sb = kunit_kzalloc(test, sizeof(*sb), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sb);
+
+	sbi = kunit_kzalloc(test, sizeof(*sbi), GFP_KERNEL);
+	KUNIT_ASSERT_NOT_ERR_OR_NULL(test, sbi);
+
+	/* no attributes tree: xattr lookup returns -EOPNOTSUPP */
+	sbi->attr_tree = NULL;
+	sb->s_fs_info = sbi;
+
+	return sb;
+}
+
+/*
+ * Test reading of the on-disk timestamp that doesn't need xattr.
+ * The superblock must not be touched in such case.
+ */
+static void hfsplus_read_timestamp_regular_test(struct kunit *test)
+{
+	static const time64_t values[] = {
+		HFS_MIN_TIMESTAMP_SECS,
+		TEST_BEFORE_UNIX_EPOCH_SECS,
+		TEST_UNIX_EPOCH_SECS,
+		TEST_Y2026_SECS,
+		HFS_MAX_TIMESTAMP_SECS - 1,
+	};
+	time64_t ut;
+	bool has_xattr;
+	int err;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(values); i++) {
+		ut = 0;
+		has_xattr = true;
+
+		err = hfsplus_read_timestamp(NULL, HFSPLUS_FIRSTUSER_CNID,
+					     XATTR_LINUX_ACCESS_DATE_NAME,
+					     __hfsp_ut2mt(values[i]),
+					     &ut, &has_xattr);
+		KUNIT_EXPECT_EQ(test, 0, err);
+		KUNIT_EXPECT_EQ(test, values[i], ut);
+		KUNIT_EXPECT_FALSE(test, has_xattr);
+	}
+}
+
+/*
+ * Test reading of the extended timestamp mark without xattr
+ * (for example, the record has been created by Mac OS X or
+ * the volume has no attributes tree). The timestamp must be
+ * HFS_MAX_TIMESTAMP_SECS without any error.
+ */
+static void hfsplus_read_timestamp_mark_no_xattr_test(struct kunit *test)
+{
+	struct super_block *sb = alloc_mock_sb(test);
+	static const char * const names[] = {
+		XATTR_LINUX_CREATE_DATE_NAME,
+		XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+		XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+		XATTR_LINUX_ACCESS_DATE_NAME,
+	};
+	time64_t ut;
+	bool has_xattr;
+	int err;
+	int i;
+
+	for (i = 0; i < ARRAY_SIZE(names); i++) {
+		ut = 0;
+		has_xattr = true;
+
+		err = hfsplus_read_timestamp(sb, HFSPLUS_FIRSTUSER_CNID,
+					     names[i],
+					     HFSPLUS_EXT_TIMESTAMP_MARK,
+					     &ut, &has_xattr);
+		KUNIT_EXPECT_EQ(test, 0, err);
+		KUNIT_EXPECT_EQ(test, HFS_MAX_TIMESTAMP_SECS, ut);
+		KUNIT_EXPECT_FALSE(test, has_xattr);
+	}
+}
+
+/* Test that names of internal xattrs fit the name buffer */
+static void hfsplus_timestamp_xattr_names_test(struct kunit *test)
+{
+	static const char * const names[] = {
+		XATTR_LINUX_CREATE_DATE_NAME,
+		XATTR_LINUX_CONTENT_MOD_DATE_NAME,
+		XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME,
+		XATTR_LINUX_ACCESS_DATE_NAME,
+		XATTR_LINUX_BACKUP_DATE_NAME,
+	};
+	int i;
+
+	KUNIT_EXPECT_EQ(test, strlen(XATTR_LINUX_HFS_PREFIX),
+			(size_t)XATTR_LINUX_HFS_PREFIX_LEN);
+
+	for (i = 0; i < ARRAY_SIZE(names); i++) {
+		size_t len = XATTR_LINUX_HFS_PREFIX_LEN + strlen(names[i]);
+
+		KUNIT_EXPECT_LT_MSG(test, len, (size_t)XATTR_LINUX_HFS_NAME_MAX,
+				    "name %s is too long", names[i]);
+		KUNIT_EXPECT_LE(test, len, (size_t)HFSPLUS_ATTR_MAX_STRLEN);
+	}
+}
+
+static struct kunit_case hfsplus_time_test_cases[] = {
+	KUNIT_CASE(hfsplus_time_constants_test),
+	KUNIT_CASE(hfsplus_mt2ut_test),
+	KUNIT_CASE(hfsplus_ut2mt_test),
+	KUNIT_CASE(hfsplus_ut2mt_before_1970_test),
+	KUNIT_CASE(hfsplus_ut2mt_clamp_test),
+	KUNIT_CASE(hfsplus_time_roundtrip_test),
+	KUNIT_CASE(hfsplus_ut_needs_xattr_test),
+	KUNIT_CASE(hfsplus_mt_is_ext_timestamp_test),
+	KUNIT_CASE(hfsplus_read_timestamp_regular_test),
+	KUNIT_CASE(hfsplus_read_timestamp_mark_no_xattr_test),
+	KUNIT_CASE(hfsplus_timestamp_xattr_names_test),
+	{}
+};
+
+static struct kunit_suite hfsplus_time_test_suite = {
+	.name = "hfsplus_time",
+	.test_cases = hfsplus_time_test_cases,
+};
+
+kunit_test_suite(hfsplus_time_test_suite);
+
+MODULE_DESCRIPTION("KUnit tests for HFS+ date/time conversion");
+MODULE_LICENSE("GPL");
+MODULE_IMPORT_NS("EXPORTED_FOR_KUNIT_TESTING");
diff --git a/fs/hfsplus/xattr.c b/fs/hfsplus/xattr.c
index 21a1c196c71f2..d3b1734496168 100644
--- a/fs/hfsplus/xattr.c
+++ b/fs/hfsplus/xattr.c
@@ -50,6 +50,12 @@ static bool is_known_namespace(const char *name)
 	return true;
 }
 
+static bool is_linux_hfs_internal_xattr(const char *name)
+{
+	return !strncmp(name, XATTR_LINUX_HFS_PREFIX,
+			XATTR_LINUX_HFS_PREFIX_LEN);
+}
+
 static u32 hfsplus_init_header_node(struct inode *attr_file,
 					u32 clump_size,
 					char *buf, u16 node_size)
@@ -319,6 +325,19 @@ static int hfsplus_create_attributes_file(struct super_block *sb)
 
 	hfsplus_mark_inode_dirty(attr_file, HFSPLUS_I_ATTR_DIRTY);
 
+	/*
+	 * Store the fork of AttributesFile into the volume header right now.
+	 * The AttributesFile can be created by hfsplus_cat_write_inode()
+	 * (timestamps after 2040 are stored in xattrs) in the middle of
+	 * sync operation. Then, the dirty AttributesFile's inode can be
+	 * skipped by writeback and the volume header will be committed
+	 * without AttributesFile. As a result, the AttributesFile will be
+	 * not opened during the next mount.
+	 */
+	hfsplus_inode_write_fork(attr_file, &sbi->s_vhdr->attr_file);
+	set_bit(HFSPLUS_SB_WRITEBACKUP, &sbi->flags);
+	hfsplus_mark_mdb_dirty(sb);
+
 	sbi->attr_tree = hfs_btree_open(sb, HFSPLUS_ATTR_CNID);
 	if (!sbi->attr_tree)
 		pr_err("failed to load attributes file\n");
@@ -339,8 +358,7 @@ static int hfsplus_create_attributes_file(struct super_block *sb)
 	return err;
 }
 
-static inline
-bool is_xattr_operation_supported(struct inode *inode)
+static inline bool is_xattr_operation_supported(struct inode *inode)
 {
 	if (HFSPLUS_IS_RSRC(inode))
 		return false;
@@ -609,8 +627,9 @@ static ssize_t hfsplus_getxattr_finder_info(struct inode *inode,
 	return res;
 }
 
-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
-			 void *value, size_t size)
+static ssize_t __hfsplus_getxattr_nocheck(struct super_block *sb,
+					  u32 cnid, const char *name,
+					  void *value, size_t size)
 {
 	struct hfs_find_data fd;
 	hfsplus_attr_entry *entry;
@@ -619,13 +638,7 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 	u16 record_length = 0;
 	ssize_t res;
 
-	if (!is_xattr_operation_supported(inode))
-		return -EOPNOTSUPP;
-
-	if (!strcmp_xattr_finder_info(name))
-		return hfsplus_getxattr_finder_info(inode, value, size);
-
-	if (!HFSPLUS_SB(inode->i_sb)->attr_tree)
+	if (!HFSPLUS_SB(sb)->attr_tree)
 		return -EOPNOTSUPP;
 
 	entry = hfsplus_alloc_attr_entry();
@@ -634,18 +647,18 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 		return -ENOMEM;
 	}
 
-	res = hfs_find_init(HFSPLUS_SB(inode->i_sb)->attr_tree, &fd);
+	res = hfs_find_init(HFSPLUS_SB(sb)->attr_tree, &fd);
 	if (res) {
 		pr_err("can't init xattr find struct\n");
 		goto failed_getxattr_init;
 	}
 
-	res = hfsplus_find_attr(inode->i_sb, inode->i_ino, name, &fd);
+	res = hfsplus_find_attr(sb, cnid, name, &fd);
 	if (res) {
 		if (res == -ENOENT || res == -ENODATA)
 			res = -ENODATA;
 		else
-			pr_err("xattr search failed\n");
+			pr_err("xattr searching failed\n");
 		goto out;
 	}
 
@@ -693,6 +706,38 @@ ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
 	return res;
 }
 
+static inline ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
+					 void *value, size_t size)
+{
+	if (!is_xattr_operation_supported(inode))
+		return -EOPNOTSUPP;
+
+	if (!strcmp_xattr_finder_info(name))
+		return hfsplus_getxattr_finder_info(inode, value, size);
+
+	return __hfsplus_getxattr_nocheck(inode->i_sb, (u32)inode->i_ino,
+					  name, value, size);
+}
+
+static inline char *hfsplus_build_xattr_name(const char *name,
+					     const char *prefix,
+					     size_t prefixlen)
+{
+	char *xattr_name;
+	size_t len = NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1;
+
+	xattr_name = kzalloc(len, GFP_KERNEL);
+	if (!xattr_name)
+		return ERR_PTR(-ENOMEM);
+
+	if (snprintf(xattr_name, len, "%s%s", prefix, name) >= len) {
+		kfree(xattr_name);
+		return ERR_PTR(-ENAMETOOLONG);
+	}
+
+	return xattr_name;
+}
+
 ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 			 void *value, size_t size,
 			 const char *prefix, size_t prefixlen)
@@ -704,13 +749,9 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 		inode->i_ino, name ? name : NULL,
 		prefix ? prefix : NULL);
 
-	xattr_name = kmalloc(NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN + 1,
-			     GFP_KERNEL);
-	if (!xattr_name)
-		return -ENOMEM;
-
-	strcpy(xattr_name, prefix);
-	strcpy(xattr_name + prefixlen, name);
+	xattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);
+	if (IS_ERR(xattr_name))
+		return PTR_ERR(xattr_name);
 
 	res = __hfsplus_getxattr(inode, xattr_name, value, size);
 	kfree(xattr_name);
@@ -718,7 +759,114 @@ ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 	hfs_dbg("finished: res %d\n", res);
 
 	return res;
+}
+
+static ssize_t hfsplus_getxattr_nocheck(struct super_block *sb, u32 cnid,
+					const char *name,
+					void *value, size_t size,
+					const char *prefix, size_t prefixlen)
+{
+	int res;
+	char *xattr_name;
 
+	hfs_dbg("cnid %u, name %s, prefix %s\n",
+		cnid, name ? name : NULL,
+		prefix ? prefix : NULL);
+
+	xattr_name = hfsplus_build_xattr_name(name, prefix, prefixlen);
+	if (IS_ERR(xattr_name))
+		return PTR_ERR(xattr_name);
+
+	res = __hfsplus_getxattr_nocheck(sb, cnid, xattr_name, value, size);
+	kfree(xattr_name);
+
+	hfs_dbg("finished: res %d\n", res);
+
+	return res;
+}
+
+/*
+ * Get the timestamp stored in the internal xattr @name
+ * of catalog record @cnid. The caller can hold the catalog tree lock.
+ */
+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,
+				const char *name, time64_t *ts)
+{
+	__be64 value;
+	ssize_t res;
+
+	res = hfsplus_getxattr_nocheck(sb, cnid, name,
+					&value, sizeof(value),
+					XATTR_LINUX_HFS_PREFIX,
+					XATTR_LINUX_HFS_PREFIX_LEN);
+	if (res < 0)
+		return res;
+
+	if (res != sizeof(value))
+		return -EIO;
+
+	*ts = (time64_t)be64_to_cpu(value);
+	return 0;
+}
+
+/*
+ * Store the timestamp @ts into the internal xattr @name of catalog
+ * record @cnid. The caller must hold the catalog tree lock and it is
+ * caller's responsibility to set HFSPLUS_XATTR_EXISTS flag in
+ * the catalog record.
+ */
+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,
+				const char *name, time64_t ts)
+{
+	struct super_block *sb = inode->i_sb;
+	char xattr_name[XATTR_LINUX_HFS_NAME_MAX];
+	__be64 value = cpu_to_be64(ts);
+	int err;
+
+	hfs_dbg("ino %llu, cnid %u, name %s, ts %lld\n",
+		inode->i_ino, cnid, name, ts);
+
+	if (!HFSPLUS_SB(sb)->attr_tree) {
+		err = hfsplus_create_attributes_file(sb);
+		if (unlikely(err))
+			return err;
+
+		if (!HFSPLUS_SB(sb)->attr_tree)
+			return -EIO;
+	}
+
+	if (snprintf(xattr_name, sizeof(xattr_name), "%s%s",
+		     XATTR_LINUX_HFS_PREFIX, name) >= sizeof(xattr_name))
+		return -ENAMETOOLONG;
+
+	return hfsplus_set_attr_cnid(inode, cnid, xattr_name,
+				     &value, sizeof(value));
+}
+
+/*
+ * Remove the internal xattr @name of @inode. The caller must hold
+ * the catalog tree lock and it is caller's responsibility to update
+ * HFSPLUS_XATTR_EXISTS flag in the catalog record.
+ */
+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name)
+{
+	char xattr_name[XATTR_LINUX_HFS_NAME_MAX];
+	int err;
+
+	hfs_dbg("ino %llu, name %s\n", inode->i_ino, name);
+
+	if (!HFSPLUS_SB(inode->i_sb)->attr_tree)
+		return 0;
+
+	if (snprintf(xattr_name, sizeof(xattr_name), "%s%s",
+		     XATTR_LINUX_HFS_PREFIX, name) >= sizeof(xattr_name))
+		return -ENAMETOOLONG;
+
+	err = hfsplus_delete_attr(inode, xattr_name);
+	if (err == -ENOENT || err == -ENODATA)
+		err = 0;
+
+	return err;
 }
 
 static inline int can_list(const char *xattr_name)
@@ -726,6 +874,10 @@ static inline int can_list(const char *xattr_name)
 	if (!xattr_name)
 		return 0;
 
+	/* Internal xattrs of the driver are hidden */
+	if (is_linux_hfs_internal_xattr(xattr_name))
+		return 0;
+
 	return strncmp(xattr_name, XATTR_TRUSTED_PREFIX,
 			XATTR_TRUSTED_PREFIX_LEN) ||
 				capable(CAP_SYS_ADMIN);
@@ -998,6 +1150,10 @@ static int hfsplus_osx_getxattr(const struct xattr_handler *handler,
 	if (is_known_namespace(name))
 		return -EOPNOTSUPP;
 
+	/* Internal xattrs of the driver are hidden */
+	if (is_linux_hfs_internal_xattr(name))
+		return -EOPNOTSUPP;
+
 	/*
 	 * osx is the namespace we use to indicate an unprefixed
 	 * attribute on the filesystem (like the ones that OS X
@@ -1020,6 +1176,10 @@ static int hfsplus_osx_setxattr(const struct xattr_handler *handler,
 	if (is_known_namespace(name))
 		return -EOPNOTSUPP;
 
+	/* Internal xattrs of the driver cannot be changed by user */
+	if (is_linux_hfs_internal_xattr(name))
+		return -EOPNOTSUPP;
+
 	/*
 	 * osx is the namespace we use to indicate an unprefixed
 	 * attribute on the filesystem (like the ones that OS X
diff --git a/fs/hfsplus/xattr.h b/fs/hfsplus/xattr.h
index 15cc55e414100..8f6283f85f133 100644
--- a/fs/hfsplus/xattr.h
+++ b/fs/hfsplus/xattr.h
@@ -12,6 +12,24 @@
 
 #include <linux/xattr.h>
 
+/*
+ * Internal xattrs of Linux HFS+ driver. Timestamps beyond
+ * HFS_MAX_TIMESTAMP_SECS (February 2040) cannot be represented by
+ * 32-bit on-disk fields. Such timestamp is stored as the maximal value
+ * (HFSPLUS_EXT_TIMESTAMP_MARK) in the catalog record and the real value
+ * is kept in the xattr as big-endian 64-bit seconds since
+ * 00:00 GMT, Jan. 1, 1970.
+ */
+#define XATTR_LINUX_HFS_PREFIX "linux_hfs."
+#define XATTR_LINUX_HFS_PREFIX_LEN \
+	(sizeof(XATTR_LINUX_HFS_PREFIX) - 1)
+#define XATTR_LINUX_HFS_NAME_MAX	64
+#define XATTR_LINUX_CREATE_DATE_NAME "create_date_u64"
+#define XATTR_LINUX_CONTENT_MOD_DATE_NAME "content_mod_date_u64"
+#define XATTR_LINUX_ATTRIBUTE_MOD_DATE_NAME "attribute_mod_date_u64"
+#define XATTR_LINUX_ACCESS_DATE_NAME "access_date_u64"
+#define XATTR_LINUX_BACKUP_DATE_NAME "backup_date_u64"
+
 extern const struct xattr_handler hfsplus_xattr_osx_handler;
 extern const struct xattr_handler hfsplus_xattr_user_handler;
 extern const struct xattr_handler hfsplus_xattr_trusted_handler;
@@ -23,16 +41,18 @@ int __hfsplus_setxattr(struct inode *inode, const char *name,
 			const void *value, size_t size, int flags);
 
 int hfsplus_setxattr(struct inode *inode, const char *name,
-				   const void *value, size_t size, int flags,
-				   const char *prefix, size_t prefixlen);
-
-ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,
-			   void *value, size_t size);
-
+		     const void *value, size_t size, int flags,
+		     const char *prefix, size_t prefixlen);
 ssize_t hfsplus_getxattr(struct inode *inode, const char *name,
 			 void *value, size_t size,
 			 const char *prefix, size_t prefixlen);
 
+int hfsplus_get_timestamp_xattr(struct super_block *sb, u32 cnid,
+				const char *name, time64_t *ts);
+int hfsplus_set_timestamp_xattr(struct inode *inode, u32 cnid,
+				const char *name, time64_t ts);
+int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name);
+
 ssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size);
 
 int hfsplus_init_security(struct inode *inode, struct inode *dir,
diff --git a/include/linux/hfs_common.h b/include/linux/hfs_common.h
index d6a615e74b26e..d4e91dea23628 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_ */

11/3 2026/10/10 00:11 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 25018
							output: 38
							thoughts: 1983
						

Thoughts:
**KMSAN Needs Analysis: HFS+ 64-bit Timestamp Support**

Okay, let's break this down. We need to decide if this HFS+ 64-bit timestamp support patch requires a dedicated KMSAN fuzzing session. The guiding principle is clear: if KASAN, LOCKDEP, or standard bug detectors would catch any issues, then we *don't* need KMSAN. KMSAN is for catching uninitialized memory reads and kernel-to-user info leaks that other tools might miss.

Looking at the patch, it's about extending timestamp support in HFS+ beyond February 2040 by storing the 64-bit timestamps as extended attributes (xattrs) prefixed with `linux_hfs.`. The patch primarily touches files in the `fs/hfsplus` directory.

The key areas I'm focused on are where data is *transferred* between kernel space and user space or where *potentially uninitialized* memory could be accessed.

Specifically, the patch introduces several new functions related to handling these extended timestamps, like `hfsplus_read_timestamp`, `hfsplus_get_timestamp_xattr`, and `hfsplus_set_attr_cnid`.  I see several functions such as `hfsplus_copy_timestamps2folder`, `hfsplus_copy_timestamps2file`, `hfsplus_set_create_date`, and `hfsplus_cat_build_record` which create and manipulate structures for timestamps. The key here is checking to see whether these timestamps are being initialized correctly.

Let's examine `hfsplus_read_timestamp` first. In this function, it calls `hfsplus_get_timestamp_xattr`. Within `hfsplus_get_timestamp_xattr`, there is a call to `hfsplus_getxattr_nocheck` with a call to `read-file` to check `__hfsplus_getxattr_nocheck`, to determine whether it actually fills `value` when writing into the memory pointed to by `value`. My concern is whether we *always* initialize the `value` variable fully when reading the xattr; an uninitialized read here could lead to a kernel-to-user information leak. From inspecting `hfsplus_read_timestamp` and `hfsplus_get_timestamp_xattr`, if an error occurs while reading the xattr (e.g., `-ENODATA`), the `timestamp` variable *appears* to be initialized correctly because `*ut = __hfsp_mt2ut(mt);` and `*has_xattr = false;` are set at the start, but let's check one more time if there is the potential for uninitialized values later.

With this in mind, let's keep going and determine if the patch introduces risks of uninitialized memory usage or info-leaks, or conditional logic that depends on potentially uninitialized variables or struct fields.

Given these considerations, I want to confirm whether there could be information leakage via uninitialized memory through any of the new structures or logic paths in `hfsplus_read_timestamp`, `hfsplus_get_timestamp_xattr`, or related functions that might lead to user-space leaks. Let me get a copy of the source code and run `read-file` one more time.

Okay, I'm going to run `read-file` on `fs/hfsplus/xattr.c` to look at `__hfsplus_getxattr_nocheck`, and I will have a very good idea whether we need to set `NeedsKMSAN=true` or not.



12/3 2026/10/10 00:11 tool read-file
0m Args:
{
  "File": "fs/hfsplus/xattr.c",
  "FirstLine": 630,
  "LineCount": 80
}

Results:
{
  "Contents": " 630:\tstatic ssize_t __hfsplus_getxattr_nocheck(struct super_block *sb,\n 631:\t\t\t\t\t\t  u32 cnid, const char *name,\n 632:\t\t\t\t\t\t  void *value, size_t size)\n 633:\t{\n 634:\t\tstruct hfs_find_data fd;\n 635:\t\thfsplus_attr_entry *entry;\n 636:\t\t__be32 xattr_record_type;\n 637:\t\tu32 record_type;\n 638:\t\tu16 record_length = 0;\n 639:\t\tssize_t res;\n 640:\t\n 641:\t\tif (!HFSPLUS_SB(sb)-\u003eattr_tree)\n 642:\t\t\treturn -EOPNOTSUPP;\n 643:\t\n 644:\t\tentry = hfsplus_alloc_attr_entry();\n 645:\t\tif (!entry) {\n 646:\t\t\tpr_err(\"can't allocate xattr entry\\n\");\n 647:\t\t\treturn -ENOMEM;\n 648:\t\t}\n 649:\t\n 650:\t\tres = hfs_find_init(HFSPLUS_SB(sb)-\u003eattr_tree, \u0026fd);\n 651:\t\tif (res) {\n 652:\t\t\tpr_err(\"can't init xattr find struct\\n\");\n 653:\t\t\tgoto failed_getxattr_init;\n 654:\t\t}\n 655:\t\n 656:\t\tres = hfsplus_find_attr(sb, cnid, name, \u0026fd);\n 657:\t\tif (res) {\n 658:\t\t\tif (res == -ENOENT || res == -ENODATA)\n 659:\t\t\t\tres = -ENODATA;\n 660:\t\t\telse\n 661:\t\t\t\tpr_err(\"xattr searching failed\\n\");\n 662:\t\t\tgoto out;\n 663:\t\t}\n 664:\t\n 665:\t\thfs_bnode_read(fd.bnode, \u0026xattr_record_type,\n 666:\t\t\t\tfd.entryoffset, sizeof(xattr_record_type));\n 667:\t\trecord_type = be32_to_cpu(xattr_record_type);\n 668:\t\tif (record_type == HFSPLUS_ATTR_INLINE_DATA) {\n 669:\t\t\trecord_length = hfs_bnode_read_u16(fd.bnode,\n 670:\t\t\t\t\tfd.entryoffset +\n 671:\t\t\t\t\toffsetof(struct hfsplus_attr_inline_data,\n 672:\t\t\t\t\tlength));\n 673:\t\t\tif (record_length \u003e HFSPLUS_MAX_INLINE_DATA_SIZE) {\n 674:\t\t\t\tpr_err(\"invalid xattr record size\\n\");\n 675:\t\t\t\tres = -EIO;\n 676:\t\t\t\tgoto out;\n 677:\t\t\t}\n 678:\t\t} else if (record_type == HFSPLUS_ATTR_FORK_DATA ||\n 679:\t\t\t\trecord_type == HFSPLUS_ATTR_EXTENTS) {\n 680:\t\t\tpr_err(\"only inline data xattr are supported\\n\");\n 681:\t\t\tres = -EOPNOTSUPP;\n 682:\t\t\tgoto out;\n 683:\t\t} else {\n 684:\t\t\tpr_err(\"invalid xattr record\\n\");\n 685:\t\t\tres = -EIO;\n 686:\t\t\tgoto out;\n 687:\t\t}\n 688:\t\n 689:\t\tif (size) {\n 690:\t\t\thfs_bnode_read(fd.bnode, entry, fd.entryoffset,\n 691:\t\t\t\t\toffsetof(struct hfsplus_attr_inline_data,\n 692:\t\t\t\t\t\traw_bytes) + record_length);\n 693:\t\t}\n 694:\t\n 695:\t\tif (size \u003e= record_length) {\n 696:\t\t\tmemcpy(value, entry-\u003einline_data.raw_bytes, record_length);\n 697:\t\t\tres = record_length;\n 698:\t\t} else\n 699:\t\t\tres = size ? -ERANGE : record_length;\n 700:\t\n 701:\tout:\n 702:\t\thfs_find_exit(\u0026fd);\n 703:\t\n 704:\tfailed_getxattr_init:\n 705:\t\thfsplus_destroy_attr_entry(entry);\n 706:\t\treturn res;\n 707:\t}\n 708:\t\n 709:\tstatic inline ssize_t __hfsplus_getxattr(struct inode *inode, const char *name,\n"
}

13/3 2026/10/10 00:11 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 29067
							output: 33
							thoughts: 1766
						

Thoughts:
**Analyzing Timestamp and Extended Attributes in HFS+**

Okay, let's break this down. I'm looking at the timestamp and extended attribute handling in this HFS+ code, specifically related to the birthdate and create date attributes.

First, I examined `hfsplus_get_timestamp_xattr`. I see that when `res == sizeof(value)`, the `value` buffer is fully populated, meaning the timestamp is definitely initialized. I also noted the error handling; if `res` doesn't match the expected size, it returns `-EIO`, which ensures a fallback strategy is in place.

Then, the focus shifted to `hfsplus_read_timestamp`. This is crucial. If `hfsp_mt_is_ext_timestamp(mt)` is false, it means we're dealing with a standard timestamp, and the code returns with `*ut` initialized from `mt` and `*has_xattr` set to `false`. If it *is* an extended timestamp, it calls `hfsplus_get_timestamp_xattr`. Importantly, I realized that if an error like `-ENODATA` or `-EOPNOTSUPP` occurs, the function still returns without issues, with the necessary parameters initialized. In other words, in all cases where `err == 0`, both `*ut` and `*has_xattr` are properly initialized. So, no unititialized reads here. The key point is that `*ut` is initialized either with the basic timestamp or with the result of a successful xattr read. That ensures there will always be a value, even if the xattr is missing.

I tracked the initialization of `hip->birthdate` through `hfsplus_iget`, `hfsplus_set_create_date`, and `hfsplus_inode_read_timestamps`. It starts at 0, can be set and read by other methods.

Then, there's `hfsplus_getattr`, where `stat->btime` is set.  I saw that `stat->btime.tv_nsec` is explicitly set to 0. And importantly, `stat` is already initialized through `generic_fillattr`. So there's no concern about reading uninitialized values here. This indicates a consistent and safe pattern, which is great.

Finally, I got to extended attribute visibility. It uses `is_linux_hfs_internal_xattr` to hide internal attributes. Specifically, `hfsplus_osx_getxattr`, `hfsplus_osx_setxattr` and `can_list`. This prevents user-level access to internal driver data. I need to make sure this doesn't leak anything from a `listxattr` call. I should grep or read `hfsplus_listxattr` now to ensure there are no subtle leaks.



14/3 2026/10/10 00:12 tool grepper
0m Args:
{
  "Expression": "hfsplus_listxattr",
  "PathPrefixes": [
    "fs/hfsplus"
  ]
}

Results:
{
  "Output": "fs/hfsplus/dir.c=657=const struct inode_operations hfsplus_dir_inode_operations = {\n--\nfs/hfsplus/dir.c-667-\t.getattr\t\t= hfsplus_getattr,\nfs/hfsplus/dir.c:668:\t.listxattr\t\t= hfsplus_listxattr,\nfs/hfsplus/dir.c-669-\t.fileattr_get\t\t= hfsplus_fileattr_get,\n--\nfs/hfsplus/inode.c=446=static const struct inode_operations hfsplus_file_inode_operations = {\n--\nfs/hfsplus/inode.c-448-\t.getattr\t= hfsplus_getattr,\nfs/hfsplus/inode.c:449:\t.listxattr\t= hfsplus_listxattr,\nfs/hfsplus/inode.c-450-\t.fileattr_get\t= hfsplus_fileattr_get,\n--\nfs/hfsplus/inode.c=454=static const struct inode_operations hfsplus_symlink_inode_operations = {\n--\nfs/hfsplus/inode.c-457-\t.getattr\t= hfsplus_getattr,\nfs/hfsplus/inode.c:458:\t.listxattr\t= hfsplus_listxattr,\nfs/hfsplus/inode.c-459-};\n--\nfs/hfsplus/inode.c=461=static const struct inode_operations hfsplus_special_inode_operations = {\n--\nfs/hfsplus/inode.c-463-\t.getattr\t= hfsplus_getattr,\nfs/hfsplus/inode.c:464:\t.listxattr\t= hfsplus_listxattr,\nfs/hfsplus/inode.c-465-};\n--\nfs/hfsplus/xattr.c=872=static inline int can_list(const char *xattr_name)\n--\nfs/hfsplus/xattr.c-885-\nfs/hfsplus/xattr.c:886:static ssize_t hfsplus_listxattr_finder_info(struct dentry *dentry,\nfs/hfsplus/xattr.c-887-\t\t\t\t\t\tchar *buffer, size_t size)\n--\nfs/hfsplus/xattr.c-953-\nfs/hfsplus/xattr.c:954:ssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size)\nfs/hfsplus/xattr.c-955-{\n--\nfs/hfsplus/xattr.c-969-\nfs/hfsplus/xattr.c:970:\tres = hfsplus_listxattr_finder_info(dentry, buffer, size);\nfs/hfsplus/xattr.c-971-\tif (res \u003c 0)\n--\nfs/hfsplus/xattr.h=54=int hfsplus_remove_timestamp_xattr(struct inode *inode, const char *name);\nfs/hfsplus/xattr.h-55-\nfs/hfsplus/xattr.h:56:ssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size);\nfs/hfsplus/xattr.h-57-\n"
}

15/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 32333
							output: 38
							thoughts: 32
						

Thoughts:
Okay, let's dive into this. Here's how I'd approach summarizing the prompt:

**Analyzing `hfsplus_listxattr` in `fs/hfsplus/xattr.c`**

Alright, so the task at hand is to understand `hfsplus_listxattr`. It's located around line 954 in `fs/hfsplus/xattr.c`, which gives me a good starting point. My initial thought process is this: given my expertise, I'm already mentally constructing the broader context of this function. I know I'm dealing with extended attributes within the HFS+ filesystem, specifically the listing of them. That immediately brings up questions about how HFS+ stores these attributes, the data structures involved, and the potential complexities of iterating through them. I'm going to start by reading the code itself to see how the implementation handles this challenge, as this will determine the key part of how this function works. I'll be looking for things like: how the function is called, what arguments it takes, and what it returns. I'll pay close attention to the data structures it manipulates, especially those related to the inode and the attribute list. I'll also be scanning for potential issues like error handling, concurrency, and any limitations in the number or size of attributes it can list. Finally, I'll be looking for any optimization strategies employed, or any limitations within the context of the larger filesystem. This examination will give me a clear understanding of the listing process and its place within the HFS+ ecosystem.



16/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/xattr.c",
  "FirstLine": 945,
  "LineCount": 55
}

Results:
{
  "Contents": " 945:\t\t\t}\n 946:\t\t}\n 947:\t\n 948:\tend_listxattr_finder_info:\n 949:\t\thfs_find_exit(\u0026fd);\n 950:\t\n 951:\t\treturn res;\n 952:\t}\n 953:\t\n 954:\tssize_t hfsplus_listxattr(struct dentry *dentry, char *buffer, size_t size)\n 955:\t{\n 956:\t\tssize_t err;\n 957:\t\tssize_t res;\n 958:\t\tstruct inode *inode = d_inode(dentry);\n 959:\t\tstruct hfs_find_data fd;\n 960:\t\tstruct hfsplus_attr_key attr_key;\n 961:\t\tchar *strbuf;\n 962:\t\tsize_t strbuf_size;\n 963:\t\tint xattr_name_len;\n 964:\t\n 965:\t\thfs_dbg(\"ino %llu\\n\", inode-\u003ei_ino);\n 966:\t\n 967:\t\tif (!is_xattr_operation_supported(inode))\n 968:\t\t\treturn -EOPNOTSUPP;\n 969:\t\n 970:\t\tres = hfsplus_listxattr_finder_info(dentry, buffer, size);\n 971:\t\tif (res \u003c 0)\n 972:\t\t\treturn res;\n 973:\t\telse if (!HFSPLUS_SB(inode-\u003ei_sb)-\u003eattr_tree)\n 974:\t\t\treturn (res == 0) ? -EOPNOTSUPP : res;\n 975:\t\n 976:\t\terr = hfs_find_init(HFSPLUS_SB(inode-\u003ei_sb)-\u003eattr_tree, \u0026fd);\n 977:\t\tif (err) {\n 978:\t\t\tpr_err(\"can't init xattr find struct\\n\");\n 979:\t\t\treturn err;\n 980:\t\t}\n 981:\t\n 982:\t\tstrbuf_size = NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN +\n 983:\t\t\t\tXATTR_MAC_OSX_PREFIX_LEN + 1;\n 984:\t\tstrbuf = kzalloc(strbuf_size, GFP_KERNEL);\n 985:\t\tif (!strbuf) {\n 986:\t\t\tres = -ENOMEM;\n 987:\t\t\tgoto out;\n 988:\t\t}\n 989:\t\n 990:\t\terr = hfsplus_find_attr(inode-\u003ei_sb, inode-\u003ei_ino, NULL, \u0026fd);\n 991:\t\tif (err) {\n 992:\t\t\tif (err == -ENOENT || err == -ENODATA) {\n 993:\t\t\t\tres = 0;\n 994:\t\t\t\tgoto end_listxattr;\n 995:\t\t\t} else {\n 996:\t\t\t\tres = err;\n 997:\t\t\t\tgoto end_listxattr;\n 998:\t\t\t}\n 999:\t\t}\n"
}

17/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 33560
							output: 39
							thoughts: 28
						

Thoughts:
Okay, here's my interpretation of what's going on in that code section, presented as a first-person summary geared towards an expert:

**Delving into `hfsplus_listxattr` – A Closer Look**

Alright, I'm heading deeper into `hfsplus_listxattr`, specifically lines 1000 through 1060. Let's see what's really happening. I'm expecting to find the nitty-gritty of how extended attributes are handled in HFS Plus, but I need a more concrete idea of where to begin. My focus is going to be on the logic here, any edge cases that might be covered, and efficient data structures being used. I will be looking to see how it interacts with the underlying HFS Plus structures, specifically if it references and/or uses other methods for managing extended attributes. I'll be keeping an eye out for potential performance bottlenecks or areas that could be optimized. Hopefully I'll glean something valuable from this section.



18/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/xattr.c",
  "FirstLine": 1000,
  "LineCount": 60
}

Results:
{
  "Contents": "1000:\t\n1001:\t\tfor (;;) {\n1002:\t\t\tu16 key_len = hfs_bnode_read_u16(fd.bnode, fd.keyoffset);\n1003:\t\n1004:\t\t\tif (key_len == 0 || key_len \u003e fd.tree-\u003emax_key_len) {\n1005:\t\t\t\tpr_err(\"invalid xattr key length: %d\\n\", key_len);\n1006:\t\t\t\tres = -EIO;\n1007:\t\t\t\tgoto end_listxattr;\n1008:\t\t\t}\n1009:\t\n1010:\t\t\thfs_bnode_read(fd.bnode, \u0026attr_key,\n1011:\t\t\t\t\tfd.keyoffset, key_len + sizeof(key_len));\n1012:\t\n1013:\t\t\tif (be32_to_cpu(attr_key.cnid) != inode-\u003ei_ino)\n1014:\t\t\t\tgoto end_listxattr;\n1015:\t\n1016:\t\t\txattr_name_len = NLS_MAX_CHARSET_SIZE * HFSPLUS_ATTR_MAX_STRLEN;\n1017:\t\t\tif (hfsplus_uni2asc_xattr_str(inode-\u003ei_sb,\n1018:\t\t\t\t\t\t      \u0026fd.key-\u003eattr.key_name, strbuf,\n1019:\t\t\t\t\t\t      \u0026xattr_name_len)) {\n1020:\t\t\t\tpr_err(\"unicode conversion failed\\n\");\n1021:\t\t\t\tres = -EIO;\n1022:\t\t\t\tgoto end_listxattr;\n1023:\t\t\t}\n1024:\t\n1025:\t\t\tif (!buffer || !size) {\n1026:\t\t\t\tif (can_list(strbuf))\n1027:\t\t\t\t\tres += name_len(strbuf, xattr_name_len);\n1028:\t\t\t} else if (can_list(strbuf)) {\n1029:\t\t\t\tif (size \u003c (res + name_len(strbuf, xattr_name_len))) {\n1030:\t\t\t\t\tpr_err(\"size %zu, res %zd, name_len %zu\\n\",\n1031:\t\t\t\t\t\tsize, res,\n1032:\t\t\t\t\t\tname_len(strbuf, xattr_name_len));\n1033:\t\t\t\t\tres = -ERANGE;\n1034:\t\t\t\t\tgoto end_listxattr;\n1035:\t\t\t\t} else\n1036:\t\t\t\t\tres += copy_name(buffer + res,\n1037:\t\t\t\t\t\t\tstrbuf, xattr_name_len);\n1038:\t\t\t}\n1039:\t\n1040:\t\t\tmemset(fd.key-\u003eattr.key_name.unicode, 0,\n1041:\t\t\t\tsizeof(fd.key-\u003eattr.key_name.unicode));\n1042:\t\t\tmemset(strbuf, 0, strbuf_size);\n1043:\t\n1044:\t\t\tif (hfs_brec_goto(\u0026fd, 1))\n1045:\t\t\t\tgoto end_listxattr;\n1046:\t\t}\n1047:\t\n1048:\tend_listxattr:\n1049:\t\tkfree(strbuf);\n1050:\tout:\n1051:\t\thfs_find_exit(\u0026fd);\n1052:\t\n1053:\t\thfs_dbg(\"finished: res %zd\\n\", res);\n1054:\t\n1055:\t\treturn res;\n1056:\t}\n1057:\t\n1058:\tstatic int hfsplus_removexattr(struct inode *inode, const char *name)\n1059:\t{\n"
}

19/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 34800
							output: 37
							thoughts: 703
						

Thoughts:
**Analyzing HFS+ Attribute and Catalog Entry Initialization**

Okay, let's break this down. First, in `hfsplus_listxattr`, the `can_list(strbuf)` function seems crucial. It returns 0 when `is_linux_hfs_internal_xattr(strbuf)` is true, and importantly, `copy_name` only operates if `can_list(strbuf)` returns true. That `strbuf` is allocated with `kzalloc`, and then zeroed out in each iteration. Good practice.

Now, let's move on to `hfsplus_build_xattr_name`. Again, `kzalloc` is used. Standard allocation. No immediate red flags there. It's properly checking the length after `snprintf` to avoid buffer overflows. Excellent.

Next, I'm digging into `hfsplus_cat_build_record`. I see that `timestamps` is initialized to `{0}`. Then the `hfsplus_set_create_date` function is called and it sets the fields of the struct and the `HFSPLUS_I(inode)` struct: `birthdate`, `create_date`, and all the `timestamps` fields are copied. Then `hfsplus_copy_timestamps2folder` or `hfsplus_copy_timestamps2file` is used to copy these into either a file or folder.

Hold on... I'm missing something. Does `hfsplus_cat_build_record` itself zero out the `entry` it receives? If it doesn't, we need to understand where it's being initialized. So, let's check its caller, `hfsplus_create_cat`. I'm going to look at that function around line 256 in `fs/hfsplus/catalog.c`. I need to ensure that the initial state of this record is understood and that any uninitialized values aren't an issue.



20/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/catalog.c",
  "FirstLine": 250,
  "LineCount": 50
}

Results:
{
  "Contents": " 250:\t{\n 251:\t\tint err;\n 252:\t\n 253:\t\tentry-\u003etype = cpu_to_be16(type);\n 254:\t\tentry-\u003ethread.reserved = 0;\n 255:\t\tentry-\u003ethread.parentID = cpu_to_be32(parentid);\n 256:\t\terr = hfsplus_asc2uni(sb, \u0026entry-\u003ethread.nodeName, HFSPLUS_MAX_STRLEN,\n 257:\t\t\t\t\tstr-\u003ename, str-\u003elen, HFS_REGULAR_NAME);\n 258:\t\tif (unlikely(err \u003c 0))\n 259:\t\t\treturn err;\n 260:\t\n 261:\t\treturn 10 + be16_to_cpu(entry-\u003ethread.nodeName.length) * 2;\n 262:\t}\n 263:\t\n 264:\t/* Try to get a catalog entry for given catalog id */\n 265:\tint hfsplus_find_cat(struct super_block *sb, u32 cnid,\n 266:\t\t\t     struct hfs_find_data *fd)\n 267:\t{\n 268:\t\thfsplus_cat_entry tmp = {0};\n 269:\t\tint err;\n 270:\t\tu16 type;\n 271:\t\n 272:\t\thfsplus_cat_build_key_with_cnid(sb, fd-\u003esearch_key, cnid);\n 273:\t\terr = hfsplus_brec_read_cat(fd, \u0026tmp);\n 274:\t\tif (err)\n 275:\t\t\treturn err;\n 276:\t\n 277:\t\ttype = be16_to_cpu(tmp.type);\n 278:\t\tif (!is_hfs_thread_record_type(type)) {\n 279:\t\t\tpr_err(\"found bad thread record in catalog\\n\");\n 280:\t\t\treturn -EIO;\n 281:\t\t}\n 282:\t\n 283:\t\tif (be16_to_cpu(tmp.thread.nodeName.length) \u003e 255) {\n 284:\t\t\tpr_err(\"catalog name length corrupted\\n\");\n 285:\t\t\treturn -EIO;\n 286:\t\t}\n 287:\t\n 288:\t\thfsplus_cat_build_key_uni(fd-\u003esearch_key,\n 289:\t\t\tbe32_to_cpu(tmp.thread.parentID),\n 290:\t\t\t\u0026tmp.thread.nodeName);\n 291:\t\treturn hfs_brec_find(fd, hfs_find_rec_by_key);\n 292:\t}\n 293:\t\n 294:\tstatic void hfsplus_subfolders_inc(struct inode *dir)\n 295:\t{\n 296:\t\tstruct hfsplus_sb_info *sbi = HFSPLUS_SB(dir-\u003ei_sb);\n 297:\t\n 298:\t\tif (test_bit(HFSPLUS_SB_HFSX, \u0026sbi-\u003eflags)) {\n 299:\t\t\t/*\n"
}

21/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 36790
							output: 37
							thoughts: 23
						

Thoughts:
Here's a summary of my thought process, as requested:

**Navigating the HFS+ Codebase**

Alright, let's dive back into this `catalog.c` file. I'm trying to get a better handle on the HFS+ file system implementation. The next logical step, given what I'm looking at, is to examine the function `hfsplus_create_cat`. That seems like the right place to start looking at the catalog. I'm going to scan further down in the file and see what insights this function can provide.



22/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/catalog.c",
  "FirstLine": 320,
  "LineCount": 70
}

Results:
{
  "Contents": " 320:\t\t\t\tHFSPLUS_I(dir)-\u003esubfolders--;\n 321:\t\t}\n 322:\t}\n 323:\t\n 324:\tint hfsplus_create_cat(u32 cnid, struct inode *dir,\n 325:\t\t\tconst struct qstr *str, struct inode *inode)\n 326:\t{\n 327:\t\tstruct super_block *sb = dir-\u003ei_sb;\n 328:\t\tstruct hfs_find_data fd;\n 329:\t\thfsplus_cat_entry entry;\n 330:\t\ttime64_t birthdate;\n 331:\t\tint entry_size;\n 332:\t\tint err;\n 333:\t\n 334:\t\thfs_dbg(\"name %s, cnid %u, i_nlink %d\\n\",\n 335:\t\t\tstr-\u003ename, cnid, inode-\u003ei_nlink);\n 336:\t\terr = hfs_find_init(HFSPLUS_SB(sb)-\u003ecat_tree, \u0026fd);\n 337:\t\tif (err)\n 338:\t\t\treturn err;\n 339:\t\n 340:\t\t/*\n 341:\t\t * Fail early and avoid ENOSPC during the btree operations. We may\n 342:\t\t * have to split the root node at most once.\n 343:\t\t */\n 344:\t\terr = hfs_bmap_reserve(fd.tree, 2 * fd.tree-\u003edepth);\n 345:\t\tif (err)\n 346:\t\t\tgoto err2;\n 347:\t\n 348:\t\thfsplus_cat_build_key_with_cnid(sb, fd.search_key, cnid);\n 349:\t\tentry_size = hfsplus_fill_cat_thread(sb, \u0026entry,\n 350:\t\t\tS_ISDIR(inode-\u003ei_mode) ?\n 351:\t\t\t\tHFSPLUS_FOLDER_THREAD : HFSPLUS_FILE_THREAD,\n 352:\t\t\tdir-\u003ei_ino, str);\n 353:\t\tif (unlikely(entry_size \u003c 0)) {\n 354:\t\t\terr = entry_size;\n 355:\t\t\tgoto err2;\n 356:\t\t}\n 357:\t\n 358:\t\terr = hfs_brec_find(\u0026fd, hfs_find_rec_by_key);\n 359:\t\tif (err != -ENOENT) {\n 360:\t\t\tif (!err)\n 361:\t\t\t\terr = -EEXIST;\n 362:\t\t\tgoto err2;\n 363:\t\t}\n 364:\t\terr = hfs_brec_insert(\u0026fd, \u0026entry, entry_size);\n 365:\t\tif (err)\n 366:\t\t\tgoto err2;\n 367:\t\n 368:\t\terr = hfsplus_cat_build_key(sb, fd.search_key, dir-\u003ei_ino, str);\n 369:\t\tif (unlikely(err))\n 370:\t\t\tgoto err1;\n 371:\t\n 372:\t\tentry_size = hfsplus_cat_build_record(\u0026entry, cnid, inode);\n 373:\t\terr = hfs_brec_find(\u0026fd, hfs_find_rec_by_key);\n 374:\t\tif (err != -ENOENT) {\n 375:\t\t\t/* panic? */\n 376:\t\t\tif (!err)\n 377:\t\t\t\terr = -EEXIST;\n 378:\t\t\tgoto err1;\n 379:\t\t}\n 380:\t\terr = hfs_brec_insert(\u0026fd, \u0026entry, entry_size);\n 381:\t\tif (err)\n 382:\t\t\tgoto err1;\n 383:\t\n 384:\t\tbirthdate = hfsplus_cat_record_birthdate(cnid, inode);\n 385:\t\tif (hfsp_ut_needs_xattr(birthdate)) {\n 386:\t\t\t/*\n 387:\t\t\t * Failure is not critical: the creation date\n 388:\t\t\t * will be read as HFS_MAX_TIMESTAMP_SECS.\n 389:\t\t\t */\n"
}

23/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 38072
							output: 37
							thoughts: 55
						

Thoughts:
Okay, let's dive into this `hfsplus_cat_build_record` function. I need to understand exactly how it constructs the `entry` data structure. My goal is to see how the catalog record is built, and specifically, I'm interested in the relationship between the `entry`, `cnid`, and `inode` parameters. This should tell me a lot about how HFS Plus manages its catalog.

Right, time to pull up the source code for `hfsplus_cat_build_record`. I know it lives in `fs/hfsplus/catalog.c`, so let's navigate there and get reading. I'm especially interested in following the data flow within this function to see what exactly it does with these input parameters. This is key to understanding how file and directory information is organized within the HFS Plus filesystem. This should be an interesting exercise.



24/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/catalog.c",
  "FirstLine": 155,
  "LineCount": 85
}

Results:
{
  "Contents": " 155:\t\treturn HFSPLUS_I(inode)-\u003ebirthdate;\n 156:\t}\n 157:\t\n 158:\tstatic int hfsplus_cat_build_record(hfsplus_cat_entry *entry,\n 159:\t\t\t\t\t    u32 cnid, struct inode *inode)\n 160:\t{\n 161:\t\tstruct hfsplus_sb_info *sbi = HFSPLUS_SB(inode-\u003ei_sb);\n 162:\t\tstruct hfsplus_timestamps timestamps = {0};\n 163:\t\n 164:\t\tif (S_ISDIR(inode-\u003ei_mode)) {\n 165:\t\t\tstruct hfsplus_cat_folder *folder;\n 166:\t\n 167:\t\t\tfolder = \u0026entry-\u003efolder;\n 168:\t\t\tmemset(folder, 0, sizeof(*folder));\n 169:\t\t\tfolder-\u003etype = cpu_to_be16(HFSPLUS_FOLDER);\n 170:\t\t\tif (test_bit(HFSPLUS_SB_HFSX, \u0026sbi-\u003eflags))\n 171:\t\t\t\tfolder-\u003eflags |= cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT);\n 172:\t\t\tfolder-\u003eid = cpu_to_be32(inode-\u003ei_ino);\n 173:\t\n 174:\t\t\thfsplus_set_create_date(inode, \u0026timestamps);\n 175:\t\t\thfsplus_copy_timestamps2folder(\u0026timestamps, folder);\n 176:\t\t\tif (hfsp_ut_needs_xattr(HFSPLUS_I(inode)-\u003ebirthdate))\n 177:\t\t\t\tfolder-\u003eflags |= cpu_to_be16(HFSPLUS_XATTR_EXISTS);\n 178:\t\n 179:\t\t\thfsplus_cat_set_perms(inode, \u0026folder-\u003epermissions);\n 180:\t\t\tif (inode == sbi-\u003ehidden_dir)\n 181:\t\t\t\t/* invisible and namelocked */\n 182:\t\t\t\tfolder-\u003euser_info.frFlags = cpu_to_be16(0x5000);\n 183:\t\t\treturn sizeof(*folder);\n 184:\t\t} else {\n 185:\t\t\tstruct hfsplus_cat_file *file;\n 186:\t\n 187:\t\t\tfile = \u0026entry-\u003efile;\n 188:\t\t\tmemset(file, 0, sizeof(*file));\n 189:\t\t\tfile-\u003etype = cpu_to_be16(HFSPLUS_FILE);\n 190:\t\t\tfile-\u003eflags = cpu_to_be16(HFSPLUS_FILE_THREAD_EXISTS);\n 191:\t\t\tfile-\u003eid = cpu_to_be32(cnid);\n 192:\t\n 193:\t\t\tif (cnid == inode-\u003ei_ino) {\n 194:\t\t\t\thfsplus_set_create_date(inode, \u0026timestamps);\n 195:\t\t\t} else {\n 196:\t\t\t\t/*\n 197:\t\t\t\t * Hard link record: don't change creation date\n 198:\t\t\t\t * of the linked inode.\n 199:\t\t\t\t */\n 200:\t\t\t\t__be32 now = __hfsp_ut2mt(ktime_get_real_seconds());\n 201:\t\n 202:\t\t\t\ttimestamps.create_date = now;\n 203:\t\t\t\ttimestamps.content_mod_date = now;\n 204:\t\t\t\ttimestamps.attribute_mod_date = now;\n 205:\t\t\t\ttimestamps.access_date = now;\n 206:\t\t\t}\n 207:\t\t\thfsplus_copy_timestamps2file(\u0026timestamps, file);\n 208:\t\n 209:\t\t\tif (cnid == inode-\u003ei_ino) {\n 210:\t\t\t\thfsplus_cat_set_perms(inode, \u0026file-\u003epermissions);\n 211:\t\t\t\tif (S_ISLNK(inode-\u003ei_mode)) {\n 212:\t\t\t\t\tfile-\u003euser_info.fdType =\n 213:\t\t\t\t\t\tcpu_to_be32(HFSP_SYMLINK_TYPE);\n 214:\t\t\t\t\tfile-\u003euser_info.fdCreator =\n 215:\t\t\t\t\t\tcpu_to_be32(HFSP_SYMLINK_CREATOR);\n 216:\t\t\t\t} else {\n 217:\t\t\t\t\tfile-\u003euser_info.fdType =\n 218:\t\t\t\t\t\tcpu_to_be32(sbi-\u003etype);\n 219:\t\t\t\t\tfile-\u003euser_info.fdCreator =\n 220:\t\t\t\t\t\tcpu_to_be32(sbi-\u003ecreator);\n 221:\t\t\t\t}\n 222:\t\t\t\tif (HFSPLUS_FLG_IMMUTABLE \u0026\n 223:\t\t\t\t\t\t(file-\u003epermissions.rootflags |\n 224:\t\t\t\t\t\tfile-\u003epermissions.userflags))\n 225:\t\t\t\t\tfile-\u003eflags |=\n 226:\t\t\t\t\t\tcpu_to_be16(HFSPLUS_FILE_LOCKED);\n 227:\t\t\t} else {\n 228:\t\t\t\tfile-\u003euser_info.fdType =\n 229:\t\t\t\t\tcpu_to_be32(HFSP_HARDLINK_TYPE);\n 230:\t\t\t\tfile-\u003euser_info.fdCreator =\n 231:\t\t\t\t\tcpu_to_be32(HFSP_HFSPLUS_CREATOR);\n 232:\t\t\t\tfile-\u003euser_info.fdFlags =\n 233:\t\t\t\t\tcpu_to_be16(0x100);\n 234:\t\t\t\tfile-\u003ecreate_date =\n 235:\t\t\t\t\tHFSPLUS_I(sbi-\u003ehidden_dir)-\u003ecreate_date;\n 236:\t\t\t\tfile-\u003epermissions.dev =\n 237:\t\t\t\t\tcpu_to_be32(HFSPLUS_I(inode)-\u003elinkid);\n 238:\t\t\t}\n 239:\t\n"
}

25/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 39835
							output: 35
							thoughts: 1326
						

Thoughts:
**Analyzing HFS+ Timestamp Handling**

Okay, let's break down this timestamp handling within the HFS+ filesystem code. It seems like the core issue revolves around how timestamps are read and written to the catalog and how they interact with extended attributes.

First, I observe a consistent pattern: `memset` is used to zero out memory allocated for both `folder` and `file` structures. This is a common practice to ensure that fields are initialized to a known state before being populated. This is good coding practice, but doesn't necessarily tell us anything about this specific case.

Now, I'm examining how timestamps are read during inode retrieval. The function `hfsplus_inode_read_timestamps` seems central.  I see that it's taking a `struct timespec64` and, importantly, initializes it with `struct timespec64 ts = {0};`.  Then, this `ts` struct is partially updated by a call to `hfsplus_read_timestamp`. Notably, `ts.tv_sec` will be populated, but `ts.tv_nsec` remains at 0. After the read operation, this `ts` struct is used to set the atime. The same process is then repeated for the mtime, ctime, and birthdate.

A critical point is how errors are handled. If `hfsplus_read_timestamp` returns an error (`res`), that error is propagated back, and the `hfsplus_cat_read_inode` function will return -EIO and abort reading the inode. This is important to note: a timestamp read failure completely halts inode retrieval. This seems like a reasonable error handling strategy.

Next, I'm looking at the write path, specifically `hfsplus_cat_write_inode`. Similar to the read process,  `struct hfsplus_timestamps timestamps = {0}` and `struct hfsplus_xattr_changes changes = {0}` are both initialized to zero. The function `hfsplus_inode_save_timestamps` is then called, which, in turn, populates the timestamp fields.  Crucially, it appears the dates are being explicitly written to `timestamps.access_date`, `timestamps.content_mod_date`, and `timestamps.attribute_mod_date` by a call to `__hfsp_ut2mt`, which likely converts the POSIX time to HFS+ time representation. This avoids the problem of defaulting to zero. Finally, these individual `timestamps` entries are written to the `folder` and `file` structures.

The last piece is `hfsplus_set_attr_cnid`.  This is used to set the attribute CNID. I need to understand what `hfsplus_alloc_attr_entry` does.  Is it just allocating memory for an `hfsplus_attr_entry`? I'll need to check the definition to confirm this, because, if it is not allocating memory, it may be causing a problem. This could be where the actual allocation and deallocation happen and where potential issues might arise.



26/3 2026/10/10 00:12 tool grepper
0m Args:
{
  "Expression": "hfsplus_alloc_attr_entry",
  "PathPrefixes": [
    "fs/hfsplus"
  ]
}

Results:
{
  "Output": "fs/hfsplus/attributes.c=50=int hfsplus_attr_build_key(struct super_block *sb, hfsplus_btree_key *key,\n--\nfs/hfsplus/attributes.c-83-\nfs/hfsplus/attributes.c:84:hfsplus_attr_entry *hfsplus_alloc_attr_entry(void)\nfs/hfsplus/attributes.c-85-{\n--\nfs/hfsplus/attributes.c=263=int hfsplus_create_attr(struct inode *inode,\n--\nfs/hfsplus/attributes.c-279-\nfs/hfsplus/attributes.c:280:\tentry_ptr = hfsplus_alloc_attr_entry();\nfs/hfsplus/attributes.c-281-\tif (!entry_ptr)\n--\nfs/hfsplus/attributes.c=456=int hfsplus_replace_attr(struct inode *inode,\n--\nfs/hfsplus/attributes.c-472-\nfs/hfsplus/attributes.c:473:\tentry_ptr = hfsplus_alloc_attr_entry();\nfs/hfsplus/attributes.c-474-\tif (!entry_ptr)\n--\nfs/hfsplus/attributes.c=509=int hfsplus_set_attr_cnid(struct inode *inode, u32 cnid,\n--\nfs/hfsplus/attributes.c-525-\nfs/hfsplus/attributes.c:526:\tentry_ptr = hfsplus_alloc_attr_entry();\nfs/hfsplus/attributes.c-527-\tif (!entry_ptr)\n--\nfs/hfsplus/hfsplus_fs.h=351=int hfsplus_attr_build_key(struct super_block *sb, hfsplus_btree_key *key,\nfs/hfsplus/hfsplus_fs.h-352-\t\t\t   u32 cnid, const char *name);\nfs/hfsplus/hfsplus_fs.h:353:hfsplus_attr_entry *hfsplus_alloc_attr_entry(void);\nfs/hfsplus/hfsplus_fs.h-354-void hfsplus_destroy_attr_entry(hfsplus_attr_entry *entry);\n--\nfs/hfsplus/xattr.c=630=static ssize_t __hfsplus_getxattr_nocheck(struct super_block *sb,\n--\nfs/hfsplus/xattr.c-643-\nfs/hfsplus/xattr.c:644:\tentry = hfsplus_alloc_attr_entry();\nfs/hfsplus/xattr.c-645-\tif (!entry) {\n"
}

27/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 42547
							output: 36
							thoughts: 35
						

Thoughts:
Okay, let's dive into this. Here's my take on those lines of code:

**Analyzing `hfsplus_alloc_attr_entry` in `fs/hfsplus/attributes.c`**

Alright, I'm going to quickly refresh my memory. Let's pull up that function, `hfsplus_alloc_attr_entry`, specifically lines 84 to 95 in `fs/hfsplus/attributes.c`. I need to understand how the allocation of attribute entries is handled here, and how it fits into the broader attribute management scheme. Specifically, I'm interested in the interplay between the internal data structures, the error handling, and the overall efficiency of this allocation procedure. My goal is to get a sense of how robust and performant this is. Let's see...



28/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/attributes.c",
  "FirstLine": 84,
  "LineCount": 20
}

Results:
{
  "Contents": "  84:\thfsplus_attr_entry *hfsplus_alloc_attr_entry(void)\n  85:\t{\n  86:\t\treturn kmem_cache_alloc(hfsplus_attr_tree_cachep, GFP_KERNEL);\n  87:\t}\n  88:\t\n  89:\tvoid hfsplus_destroy_attr_entry(hfsplus_attr_entry *entry)\n  90:\t{\n  91:\t\tif (entry)\n  92:\t\t\tkmem_cache_free(hfsplus_attr_tree_cachep, entry);\n  93:\t}\n  94:\t\n  95:\t#define HFSPLUS_INVALID_ATTR_RECORD -1\n  96:\t\n  97:\tstatic int hfsplus_attr_build_record(hfsplus_attr_entry *entry, int record_type,\n  98:\t\t\t\t\tu32 cnid, const void *value, size_t size)\n  99:\t{\n 100:\t\tif (record_type == HFSPLUS_ATTR_FORK_DATA) {\n 101:\t\t\t/*\n 102:\t\t\t * Mac OS X supports only inline data attributes.\n 103:\t\t\t * Do nothing\n"
}

29/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 43111
							output: 36
							thoughts: 38
						

Thoughts:
Okay, here's my interpretation of that thought process, tailored for an expert audience, and written in the first person:

**Analyzing `hfsplus_attr_build_record`**

Right, let's dive into the core of how HFS+ extended attributes are structured. My focus is immediately drawn to `fs/hfsplus/attributes.c` and specifically lines 97 through 140 within the `hfsplus_attr_build_record` function. This section is critical for understanding how the attribute record itself is constructed. I'm likely interested in the details of how the record headers and data blocks are formatted in memory. Are they packed or padded? What are the implications for alignment on different architectures, and does it use a lot of memory?

I'll be looking for how the function handles the different attribute types and sizes, and how it manages the overall attribute record size calculation to ensure that the metadata fits correctly within an HFS+ B-tree node or allocation block. Specifically, I need to know:
*   How does this code handle the attribute name and data lengths? Are there any potential overflow conditions?
*   Are there any assumptions about the maximum number of attributes that can be stored?
*   How does it validate the attribute data before inserting it into the record?
*   How is the allocation of the record done, and does it account for fragmentation in the underlying storage?

Ultimately, my goal is to assess whether this implementation is robust, efficient, and aligned with the HFS+ specification. I'll be looking for potential optimization opportunities and any areas that might introduce vulnerabilities, such as buffer overflows or data corruption.



30/3 2026/10/10 00:12 tool read-file
0m Args:
{
  "File": "fs/hfsplus/attributes.c",
  "FirstLine": 97,
  "LineCount": 45
}

Results:
{
  "Contents": "  97:\tstatic int hfsplus_attr_build_record(hfsplus_attr_entry *entry, int record_type,\n  98:\t\t\t\t\tu32 cnid, const void *value, size_t size)\n  99:\t{\n 100:\t\tif (record_type == HFSPLUS_ATTR_FORK_DATA) {\n 101:\t\t\t/*\n 102:\t\t\t * Mac OS X supports only inline data attributes.\n 103:\t\t\t * Do nothing\n 104:\t\t\t */\n 105:\t\t\tmemset(entry, 0, sizeof(*entry));\n 106:\t\t\treturn sizeof(struct hfsplus_attr_fork_data);\n 107:\t\t} else if (record_type == HFSPLUS_ATTR_EXTENTS) {\n 108:\t\t\t/*\n 109:\t\t\t * Mac OS X supports only inline data attributes.\n 110:\t\t\t * Do nothing.\n 111:\t\t\t */\n 112:\t\t\tmemset(entry, 0, sizeof(*entry));\n 113:\t\t\treturn sizeof(struct hfsplus_attr_extents);\n 114:\t\t} else if (record_type == HFSPLUS_ATTR_INLINE_DATA) {\n 115:\t\t\tu16 len;\n 116:\t\n 117:\t\t\tmemset(entry, 0, sizeof(struct hfsplus_attr_inline_data));\n 118:\t\t\tentry-\u003einline_data.record_type = cpu_to_be32(record_type);\n 119:\t\t\tif (size \u003c= HFSPLUS_MAX_INLINE_DATA_SIZE)\n 120:\t\t\t\tlen = size;\n 121:\t\t\telse {\n 122:\t\t\t\thfs_dbg(\"value size %zu is too big\\n\", size);\n 123:\t\t\t\treturn HFSPLUS_INVALID_ATTR_RECORD;\n 124:\t\t\t}\n 125:\t\t\tentry-\u003einline_data.length = cpu_to_be16(len);\n 126:\t\t\tmemcpy(entry-\u003einline_data.raw_bytes, value, len);\n 127:\t\t\t/*\n 128:\t\t\t * Align len on two-byte boundary.\n 129:\t\t\t * It needs to add pad byte if we have odd len.\n 130:\t\t\t */\n 131:\t\t\tlen = round_up(len, 2);\n 132:\t\t\treturn offsetof(struct hfsplus_attr_inline_data, raw_bytes) +\n 133:\t\t\t\t\t\tlen;\n 134:\t\t} else /* invalid input */\n 135:\t\t\tmemset(entry, 0, sizeof(*entry));\n 136:\t\n 137:\t\treturn HFSPLUS_INVALID_ATTR_RECORD;\n 138:\t}\n 139:\t\n 140:\tint hfsplus_find_attr(struct super_block *sb, u32 cnid,\n 141:\t\t\t\tconst char *name, struct hfs_find_data *fd)\n"
}

31/3 2026/10/10 00:12 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 44295
							output: 37
							thoughts: 1114
						

Thoughts:
**Analyzing Potential Uninitialized Memory in HFS+ Attribute Handling**

Okay, so I'm digging into this HFS+ code, specifically around the attribute handling, and I've got a few things I need to confirm regarding uninitialized memory reads. First, it looks like `memset(entry, 0, sizeof(struct hfsplus_attr_inline_data))` is used, which zeros the entire `entry`, including any padding, which is good. Furthermore, there's the existing function `hfsplus_attr_build_record`, which also performs a `memset` zero operation, and `hfsplus_create_attr_nolock` is used, and writes `entry_size` bytes from `entry_ptr`, so I'm confident there's no immediate uninitialized memory vulnerability there.

Now, let's turn to where the timestamps are set and removed to check. In `hfsplus_set_timestamp_xattr`, the `value` variable, an `__be64`, is initialized with `cpu_to_be64(ts)`, and the pointer and size are passed to `hfsplus_set_attr_cnid`.  So that seems safe – `value` is properly initialized. Similarly, for `hfsplus_remove_timestamp_xattr`, there's no direct concern regarding potentially uninitialized data.

However, I need to investigate `hfsplus_create_attributes_file`. The concern arises from the comment about storing the "fork of AttributesFile into the volume header." My primary concern here is if `hfsplus_inode_write_fork` could be reading uninitialized data. Specifically, I'm at line 590 of `fs/hfsplus/inode.c`, so I'll need to read the code there and confirm.



32/3 2026/10/10 00:13 tool read-file
0m Args:
{
  "File": "fs/hfsplus/inode.c",
  "FirstLine": 585,
  "LineCount": 25
}

Results:
{
  "Contents": " 585:\t\thip-\u003eclump_blocks =\n 586:\t\t\tbe32_to_cpu(fork-\u003eclump_size) \u003e\u003e sbi-\u003ealloc_blksz_shift;\n 587:\t\tif (!hip-\u003eclump_blocks) {\n 588:\t\t\thip-\u003eclump_blocks = HFSPLUS_IS_RSRC(inode) ?\n 589:\t\t\t\tsbi-\u003ersrc_clump_blocks :\n 590:\t\t\t\tsbi-\u003edata_clump_blocks;\n 591:\t\t}\n 592:\t}\n 593:\t\n 594:\tvoid hfsplus_inode_write_fork(struct inode *inode,\n 595:\t\t\tstruct hfsplus_fork_raw *fork)\n 596:\t{\n 597:\t\tmemcpy(\u0026fork-\u003eextents, \u0026HFSPLUS_I(inode)-\u003efirst_extents,\n 598:\t\t       sizeof(hfsplus_extent_rec));\n 599:\t\tfork-\u003etotal_size = cpu_to_be64(inode-\u003ei_size);\n 600:\t\tfork-\u003etotal_blocks = cpu_to_be32(HFSPLUS_I(inode)-\u003ealloc_blocks);\n 601:\t}\n 602:\t\n 603:\t/*\n 604:\t * hfsplus_read_timestamp - convert on-disk timestamp into 1970-base one\n 605:\t * @sb: superblock\n 606:\t * @cnid: catalog record ID\n 607:\t * @name: name of internal xattr that can keep the real timestamp\n 608:\t * @mt: on-disk timestamp\n 609:\t * @ut: pointer on converted timestamp [out]\n"
}

33/3 2026/10/10 00:13 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 46284
							output: 38
							thoughts: 62
						

Thoughts:
Here's my analysis of the provided code snippet, structured as if I were thinking through it myself:

**Investigating the HFS+ Extended Attributes Creation**

Okay, so I'm digging into how HFS+ extended attribute files are created. I see that within `hfsplus_create_attributes_file`, the `attr_file` inode is being allocated. Specifically, the code uses `new_inode(sb)` for the allocation, and then, immediately afterward, proceeds to initialize all its key fields. That sounds fairly standard, but I want to get a very clear picture here.

Let me take a look at the core of the extended attribute logic. I'm going to jump into `fs/hfsplus/xattr.c` and focus on lines 270 through 325. That should provide the crucial details about this initialization, and how the attribute file inode is set up. I'll pay attention to the inode flags, permissions, data structures linked to it, and any special handling or constraints. Understanding this section will likely give a good picture of how these attributes are handled. I'll look for how the inode gets connected to the file system, how its size is managed, and how its data blocks are allocated.



34/3 2026/10/10 00:13 tool read-file
0m Args:
{
  "File": "fs/hfsplus/xattr.c",
  "FirstLine": 270,
  "LineCount": 55
}

Results:
{
  "Contents": " 270:\t\t\t\t\t\t\t    sbi-\u003esect_count,\n 271:\t\t\t\t\t\t\t    HFSPLUS_ATTR_CNID);\n 272:\t\n 273:\t\tmutex_lock(\u0026hip-\u003eextents_lock);\n 274:\t\thip-\u003eclump_blocks = clump_size \u003e\u003e sbi-\u003ealloc_blksz_shift;\n 275:\t\tmutex_unlock(\u0026hip-\u003eextents_lock);\n 276:\t\n 277:\t\tif (sbi-\u003efree_blocks \u003c= (hip-\u003eclump_blocks \u003c\u003c 1)) {\n 278:\t\t\terr = -ENOSPC;\n 279:\t\t\tgoto end_attr_file_creation;\n 280:\t\t}\n 281:\t\n 282:\t\twhile (hip-\u003ealloc_blocks \u003c hip-\u003eclump_blocks) {\n 283:\t\t\terr = hfsplus_file_extend(attr_file, true);\n 284:\t\t\tif (unlikely(err)) {\n 285:\t\t\t\tpr_err(\"failed to extend attributes file\\n\");\n 286:\t\t\t\tgoto end_attr_file_creation;\n 287:\t\t\t}\n 288:\t\t\thip-\u003ephys_size = attr_file-\u003ei_size =\n 289:\t\t\t\t(loff_t)hip-\u003ealloc_blocks \u003c\u003c sbi-\u003ealloc_blksz_shift;\n 290:\t\t\thip-\u003efs_blocks = hip-\u003ealloc_blocks \u003c\u003c sbi-\u003efs_shift;\n 291:\t\t\tinode_set_bytes(attr_file, attr_file-\u003ei_size);\n 292:\t\t}\n 293:\t\n 294:\t\tbuf = kzalloc(node_size, GFP_NOFS);\n 295:\t\tif (!buf) {\n 296:\t\t\terr = -ENOMEM;\n 297:\t\t\tgoto end_attr_file_creation;\n 298:\t\t}\n 299:\t\n 300:\t\tmap_nodes = hfsplus_init_header_node(attr_file, clump_size, buf, node_size);\n 301:\t\n 302:\t\tdesc = (struct hfs_bnode_desc *)buf;\n 303:\t\tnext_node = be32_to_cpu(desc-\u003enext);\n 304:\t\n 305:\t\tindex = 0;\n 306:\t\n 307:\t\terr = hfsplus_write_attributes_file_node(attr_file, buf,\n 308:\t\t\t\t\t\t\t node_size, \u0026index);\n 309:\t\tif (unlikely(err))\n 310:\t\t\tgoto failed_header_node_init;\n 311:\t\n 312:\t\tfor (map_node_idx = 0; map_node_idx \u003c map_nodes; map_node_idx++) {\n 313:\t\t\tif (next_node \u003e= map_nodes)\n 314:\t\t\t\tnext_node = 0;\n 315:\t\n 316:\t\t\thfsplus_init_map_node(buf, node_size, next_node);\n 317:\t\n 318:\t\t\terr = hfsplus_write_attributes_file_node(attr_file, buf,\n 319:\t\t\t\t\t\t\t\t node_size, \u0026index);\n 320:\t\t\tif (unlikely(err))\n 321:\t\t\t\tgoto failed_header_node_init;\n 322:\t\n 323:\t\t\tnext_node++;\n 324:\t\t}\n"
}

35/3 2026/10/10 00:13 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 47534
							output: 37
							thoughts: 2443
						

Thoughts:
**Analyzing HFS+ Timestamp Handling**

Okay, let's break this down systematically. I'm examining this `is_create_date_identical` function and its interactions with extended attributes (xattrs) and timestamps. The key here is ensuring that create dates are correctly compared, especially when dealing with extended timestamps which store birthdates, particularly when the file was created in a Mac environment.

My initial thought is that if the create date isn't immediately identical, return `false`. Simple enough. Now, the logic checks if we're dealing with an "extended timestamp". This is where things get interesting. We're calling `hfsplus_read_timestamp`. This is crucial because it decides if a 64 bit timestamp xattr is actually used or not.

Looking at `hfsplus_read_timestamp`, I'm noting that the function initially sets `*ut` (presumably a "Unix Time" representation) from `mt` (likely a Mac Time, or HFS+ time). It then checks if the timestamp is extended. If so, it attempts to read the xattr and extract the 64-bit timestamp. If it cannot read the xattr and it's not present, it treats it as if the record does not have the xattr. But if it successfully extracts a timestamp and the timestamp is less than `HFS_MAX_TIMESTAMP_SECS`, it sets `*has_xattr = true` but keeps the value of `*ut`. Why? It doesn't seem to make sense. Then, the value of the xattr is assigned to `*ut`.

The function then goes on to use `hfsplus_get_timestamp_xattr` to read the actual timestamp. This looks fine on the surface, but I need to ensure it's properly handled in the calling function. So it looks like `hfsplus_read_timestamp` may return without setting `*ut`.

I now turn my attention to `hfsplus_get_timestamp_xattr`.  Here, a 64-bit value is read from the xattr. The code calls `hfsplus_getxattr_nocheck` to fetch the data. If the returned size isn't exactly the expected 8 bytes, it returns an error `-EIO`.  This is a potential attack vector! A malformed xattr, with a record length less than 8, could cause this function to return `-EIO` without initializing the provided pointer to store the retrieved data. This could result in uninitialized memory being used when the program moves forward to utilize the extracted value.

Now, thinking back to where this `timestamp` variable is used, I am reexamining how this interacts with the `hfsplus_read_timestamp` function. If `hfsplus_get_timestamp_xattr` returns `-EIO`, the caller may incorrectly assume the timestamp is valid. This has a large potential security impact.

I check `hfsplus_inode_save_timestamp`:
I need to check how it deals with existing xattr and potential errors.

Now I am going to check `hfsplus_getattr`, which reads the birthdate. I'm checking that the values are properly copied to the output, and if they will be exposed in user space. If this call returns without a valid birthdate, this may result in a kernel oops, a security vulnerability, or an error condition for users.



36/3 2026/10/10 00:13 tool read-file
0m Args:
{
  "File": "fs/hfsplus/inode.c",
  "FirstLine": 330,
  "LineCount": 35
}

Results:
{
  "Contents": " 330:\t\t\tinode_set_mtime_to_ts(inode, inode_set_ctime_current(inode));\n 331:\t\t}\n 332:\t\n 333:\t\tsetattr_copy(\u0026nop_mnt_idmap, inode, attr);\n 334:\t\tmark_inode_dirty(inode);\n 335:\t\n 336:\t\treturn 0;\n 337:\t}\n 338:\t\n 339:\tint hfsplus_getattr(struct mnt_idmap *idmap, const struct path *path,\n 340:\t\t\t    struct kstat *stat, u32 request_mask,\n 341:\t\t\t    unsigned int query_flags)\n 342:\t{\n 343:\t\tstruct inode *inode = d_inode(path-\u003edentry);\n 344:\t\tstruct hfsplus_inode_info *hip = HFSPLUS_I(inode);\n 345:\t\n 346:\t\tif (request_mask \u0026 STATX_BTIME) {\n 347:\t\t\tstat-\u003eresult_mask |= STATX_BTIME;\n 348:\t\t\tstat-\u003ebtime.tv_sec = hip-\u003ebirthdate;\n 349:\t\t\tstat-\u003ebtime.tv_nsec = 0;\n 350:\t\t}\n 351:\t\n 352:\t\tif (inode-\u003ei_flags \u0026 S_APPEND)\n 353:\t\t\tstat-\u003eattributes |= STATX_ATTR_APPEND;\n 354:\t\tif (inode-\u003ei_flags \u0026 S_IMMUTABLE)\n 355:\t\t\tstat-\u003eattributes |= STATX_ATTR_IMMUTABLE;\n 356:\t\tif (hip-\u003euserflags \u0026 HFSPLUS_FLG_NODUMP)\n 357:\t\t\tstat-\u003eattributes |= STATX_ATTR_NODUMP;\n 358:\t\n 359:\t\tstat-\u003eattributes_mask |= STATX_ATTR_APPEND | STATX_ATTR_IMMUTABLE |\n 360:\t\t\t\t\t STATX_ATTR_NODUMP;\n 361:\t\n 362:\t\tgeneric_fillattr(\u0026nop_mnt_idmap, request_mask, inode, stat);\n 363:\t\treturn 0;\n 364:\t}\n"
}

37/3 2026/10/10 00:13 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 51315
							output: 37
							thoughts: 1842
						

Thoughts:
**Analyzing HFS+ Timestamp Handling**

Okay, let's break this down. Initially, I was wrestling with how `stat->btime` (birth time) was being handled, specifically the assignment from `hfsp_mt2ut`. I remember there was a transition from `time64_t` to `struct timespec64` and I was trying to recall if this assignment was correct or if it had caused some issues in previous kernel versions. There was also a note that `stat->btime` used to be initialized with separate `tv_sec` and `tv_nsec` fields, but it's explicitly initialized now. This seems cleaner and less prone to errors.

My attention then shifted to the `hfsplus_lookup` function within `fs/hfsplus/dir.c`.  I was concerned about a specific comparison involving `create_date` and the `d_inode(sb->s_root)`. The potential for a NULL `d_inode(sb->s_root)` was the main concern.  While `sb->s_root` is the root dentry, I had to ensure that the logic wouldn't dereference a null pointer due to a KASAN or KMSAN issue.  

I started checking the `is_create_date_identical` function, which is used in that comparison.  I wanted to make sure all variables within were initialized before use. The key variables here were `time64_t timestamp` and `bool has_xattr`.  After inspecting `hfsplus_read_timestamp`, I confirmed that `*ut` and `*has_xattr` are initialized on entry, and that if `err` is returned, the function exits before `timestamp` or `has_xattr` are used.  I also found that if `err == 0`, both `timestamp` and `has_xattr` are written to, so everything is initialized, which is good.

I then considered `hfsplus_inode_read_timestamps`. The critical line here was `struct timespec64 ts = {0};` which clearly zero-initializes the struct, and that `ts.tv_sec` is written to later on.  Then, I moved on to `hfsplus_cat_read_inode`. I checked whether the function initializes the `timestamps` variable properly, by looking at how `timestamps` is populated within the `if` and `else if` statements.  I noticed the line `struct hfsplus_timestamps timestamps = {0};`, so that's good.  After that, `timestamps`' members are populated with data from a `copy_folder2timestamps` or `copy_file2timestamps` function, and that this data comes from an `entry`. This makes me think I should investigate where `entry` comes from, and whether it's initialized correctly. I will have to start checking the surrounding lines to find that information.



38/3 2026/10/10 00:13 tool read-file
0m Args:
{
  "File": "fs/hfsplus/inode.c",
  "FirstLine": 730,
  "LineCount": 40
}

Results:
{
  "Contents": " 730:\t}\n 731:\t\n 732:\tint hfsplus_cat_read_inode(struct inode *inode, struct hfs_find_data *fd)\n 733:\t{\n 734:\t\thfsplus_cat_entry entry;\n 735:\t\tstruct hfsplus_timestamps timestamps = {0};\n 736:\t\tint res = 0;\n 737:\t\tu16 type;\n 738:\t\n 739:\t\ttype = hfs_bnode_read_u16(fd-\u003ebnode, fd-\u003eentryoffset);\n 740:\t\n 741:\t\tHFSPLUS_I(inode)-\u003elinkid = 0;\n 742:\t\tif (type == HFSPLUS_FOLDER) {\n 743:\t\t\tstruct hfsplus_cat_folder *folder = \u0026entry.folder;\n 744:\t\n 745:\t\t\tif (fd-\u003eentrylength \u003c sizeof(struct hfsplus_cat_folder)) {\n 746:\t\t\t\tpr_err(\"bad catalog folder entry\\n\");\n 747:\t\t\t\tres = -EIO;\n 748:\t\t\t\tgoto out;\n 749:\t\t\t}\n 750:\t\t\thfs_bnode_read(fd-\u003ebnode, \u0026entry, fd-\u003eentryoffset,\n 751:\t\t\t\t\t\tsizeof(struct hfsplus_cat_folder));\n 752:\t\t\tres = hfsplus_get_perms(inode, \u0026folder-\u003epermissions, 1);\n 753:\t\t\tif (res)\n 754:\t\t\t\tgoto out;\n 755:\t\t\tset_nlink(inode, 1);\n 756:\t\t\tinode-\u003ei_size = 2 + be32_to_cpu(folder-\u003evalence);\n 757:\t\n 758:\t\t\thfsplus_copy_folder2timestamps(folder, \u0026timestamps);\n 759:\t\t\tres = hfsplus_inode_read_timestamps(inode, \u0026timestamps);\n 760:\t\t\tif (res) {\n 761:\t\t\t\tres = -EIO;\n 762:\t\t\t\tgoto out;\n 763:\t\t\t}\n 764:\t\n 765:\t\t\tHFSPLUS_I(inode)-\u003efs_blocks = 0;\n 766:\t\t\tif (folder-\u003eflags \u0026 cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {\n 767:\t\t\t\tHFSPLUS_I(inode)-\u003esubfolders =\n 768:\t\t\t\t\tbe32_to_cpu(folder-\u003esubfolders);\n 769:\t\t\t}\n"
}

39/3 2026/10/10 00:13 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 54478
							output: 37
							thoughts: 329
						

Thoughts:
**Analyzing HFS+ Folder Metadata Updates**

Okay, so I'm diving into the HFS+ code here. Specifically, I'm tracing how folder metadata gets updated. The `hfs_bnode_read` function seems to be crucial, reading the contents of a `struct hfsplus_cat_folder` directly into the `entry` variable. That populates all the `folder` fields from the B-tree node, which is expected.

Now, I need to understand how these folder fields are ultimately written. Let's look at `hfsplus_inode_save_timestamps`. I see it takes `inode`, `timestamps`, and `changes` as arguments. Initially, `timestamps` and `changes` are set to zero in `hfsplus_cat_write_inode`. Interesting. The zero initialization, is typical. I see it passed into `hfsplus_inode_save_timestamps`. Then, back in `hfsplus_cat_write_inode`, the timestamps are copied over to `folder`. That makes sense; it ensures the updated timestamps from the inode are reflected in the folder's B-tree entry. It then goes on to update some flags.

Hold on... I need to be sure about the context of 'folder' in `hfsplus_cat_write_inode`. Where is it defined? Let me quickly refer back to the source code around lines 700-730 in `fs/hfsplus/inode.c`. That will hopefully clarify its origin and lifecycle within this specific function.



40/3 2026/10/10 00:14 tool read-file
0m Args:
{
  "File": "fs/hfsplus/inode.c",
  "FirstLine": 945,
  "LineCount": 40
}

Results:
{
  "Contents": " 945:\t\t\tmain_inode = HFSPLUS_I(inode)-\u003ersrc_inode;\n 946:\t\n 947:\t\tif (!main_inode-\u003ei_nlink)\n 948:\t\t\treturn 0;\n 949:\t\n 950:\t\tif (hfs_find_init(tree, \u0026fd))\n 951:\t\t\t/* panic? */\n 952:\t\t\treturn -EIO;\n 953:\t\n 954:\t\tif (hfsplus_find_cat(main_inode-\u003ei_sb, main_inode-\u003ei_ino, \u0026fd))\n 955:\t\t\t/* panic? */\n 956:\t\t\tgoto out;\n 957:\t\n 958:\t\tif (S_ISDIR(main_inode-\u003ei_mode)) {\n 959:\t\t\tstruct hfsplus_cat_folder *folder = \u0026entry.folder;\n 960:\t\n 961:\t\t\tif (fd.entrylength \u003c sizeof(struct hfsplus_cat_folder)) {\n 962:\t\t\t\tpr_err(\"bad catalog folder entry\\n\");\n 963:\t\t\t\tres = -EIO;\n 964:\t\t\t\tgoto out;\n 965:\t\t\t}\n 966:\t\t\thfs_bnode_read(fd.bnode, \u0026entry, fd.entryoffset,\n 967:\t\t\t\t\t\tsizeof(struct hfsplus_cat_folder));\n 968:\t\t\t/* simple node checks? */\n 969:\t\t\thfsplus_cat_set_perms(inode, \u0026folder-\u003epermissions);\n 970:\t\n 971:\t\t\thfsplus_inode_save_timestamps(inode, \u0026timestamps, \u0026changes);\n 972:\t\t\tfolder-\u003eaccess_date = timestamps.access_date;\n 973:\t\t\tfolder-\u003econtent_mod_date = timestamps.content_mod_date;\n 974:\t\t\tfolder-\u003eattribute_mod_date = timestamps.attribute_mod_date;\n 975:\t\t\tfolder-\u003eflags = hfsplus_update_xattr_exists_flag(inode,\n 976:\t\t\t\t\t\t\t\t\t folder-\u003eflags,\n 977:\t\t\t\t\t\t\t\t\t \u0026changes);\n 978:\t\n 979:\t\t\tfolder-\u003evalence = cpu_to_be32(inode-\u003ei_size - 2);\n 980:\t\t\tif (folder-\u003eflags \u0026 cpu_to_be16(HFSPLUS_HAS_FOLDER_COUNT)) {\n 981:\t\t\t\tfolder-\u003esubfolders =\n 982:\t\t\t\t\tcpu_to_be32(HFSPLUS_I(inode)-\u003esubfolders);\n 983:\t\t\t}\n 984:\t\t\thfs_bnode_write(fd.bnode, \u0026entry, fd.entryoffset,\n"
}

41/3 2026/10/10 00:14 llm kmsan-evaluator
0m Model:
gemini-3.8-flash

Args:
null

Results:
null

Tokens:
							input: 55864
							output: 400
							thoughts: 2948
						

Thoughts:
**KMSAN Assessment: HFS+ Extended Timestamps Patch**

Okay, let's break this down. My goal is to ensure this patch is safe from uninitialized memory reads that KMSAN would catch. The core changes revolve around supporting extended timestamps in HFS+ beyond the 2040 limit, using internal xattrs (`linux_hfs.*`) for storage. The patch includes KUnit tests and some attribute handling refactoring, so I've looked at the diff with a fine-tooth comb.

Firstly, the most important thing is that **no new kernel structures are being sent to userspace** without proper initialization. Functions like `hfsplus_getattr` are making sure `stat->btime.tv_sec` and `stat->btime.tv_nsec` get set before any potential user-space copy. Internal xattrs are specifically filtered out from `listxattr` and `getxattr` calls by the `is_linux_hfs_internal_xattr` function, so we avoid any information leaks to user space. Furthermore, `hfsplus_build_xattr_name` consistently uses `kzalloc` to allocate xattr names, ensuring zero-initialization.

Secondly, I'm checking for **conditional logic dependent on uninitialized data**. All new variables, such as the `timestamps` struct, `changes`, `ts`, are *explicitly* initialized upon declaration with `{0}` (or equivalent). Any pointers returned from functions such as `hfsplus_read_timestamp` are safely initialized *before* use. No branch relies on undefined memory.

Thirdly, when it comes to **complex data structures**, things are looking good. In `hfsplus_cat_build_record`, `timestamps` is zeroed, and in functions like `hfsplus_iget`, fields like `HFSPLUS_I(inode)->birthdate` get initialized. The code sets `s_time_gran`, `s_time_min`, and `s_time_max` in `hfsplus_fill_super`. For dynamically allocated `entry_ptr` in `hfsplus_set_attr_cnid`, it is initialized by `hfsplus_attr_build_record`, or allocated using `kzalloc`.

Lastly, I'm verifying the absence of any **relaxed bounds checks**.  In other words, no code modifications inadvertently enable access to uninitialized bytes of existing buffers.  `hfsplus_build_xattr_name` uses a bounded `snprintf` with `kzalloc`, and `hfsplus_get_timestamp_xattr` has a check (`res != sizeof(value)`) to ensure that we're only operating within the bounds of a value that has been initialized.

Therefore, after a thorough review, I'm confident that this patch **does not introduce any new KMSAN risks** related to uninitialized memory access or information leaks to userspace. There's an explicit and consistent zero-initialization strategy across all structures, variables, and allocations. The patch only deals with standard memory safety and data integrity issues. Any concerns related to file system consistency, concurrent operations or even pointer handling, would, as always, be covered by KASAN and standard kernel debug facilities. No need for a dedicated KMSAN session on this one.



42/3 2026/10/10 00:14 tool set-results
0m Args:
{
  "KMSANReasoning": "The patch series adds support for 64-bit timestamps in the HFS+ filesystem by encoding timestamps beyond February 2040 (HFS_MAX_TIMESTAMP_SECS) in internal extended attributes prefixed with \"linux_hfs.\", along with corresponding KUnit tests.\n\nEvaluating against KMSAN vs KASAN:\n1. Stack and Heap Initialization: All new local stack structures (`struct hfsplus_timestamps`, `struct timespec64`, `struct hfsplus_xattr_changes`) are zero-initialized with `= {0}` at declaration. Inode structure fields such as `birthdate` are explicitly initialized to 0 in `hfsplus_iget()`. Dynamic name allocations use `kzalloc()`.\n2. Info-leak risks to user space: Internal timestamp xattrs are explicitly filtered out from user space visibility in `can_list()`, `hfsplus_osx_getxattr()`, and `hfsplus_osx_setxattr()`. In `hfsplus_getattr()`, both `stat-\u003ebtime.tv_sec` and `stat-\u003ebtime.tv_nsec` are explicitly assigned without leaving padding or fields uninitialized.\n3. Read safety: Output pointers in `hfsplus_read_timestamp()` are initialized on entry, and `hfsplus_get_timestamp_xattr()` validates that the xattr payload size matches `sizeof(value)` before interpreting the data.\n4. Applicability of other sanitizers: Any potential defects related to corrupted on-disk btree metadata, buffer bounds, or lock contention are detected by standard KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory reads or info-leak risks are exposed, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

Results:
{
  "KMSANReasoning": "The patch series adds support for 64-bit timestamps in the HFS+ filesystem by encoding timestamps beyond February 2040 (HFS_MAX_TIMESTAMP_SECS) in internal extended attributes prefixed with \"linux_hfs.\", along with corresponding KUnit tests.\n\nEvaluating against KMSAN vs KASAN:\n1. Stack and Heap Initialization: All new local stack structures (`struct hfsplus_timestamps`, `struct timespec64`, `struct hfsplus_xattr_changes`) are zero-initialized with `= {0}` at declaration. Inode structure fields such as `birthdate` are explicitly initialized to 0 in `hfsplus_iget()`. Dynamic name allocations use `kzalloc()`.\n2. Info-leak risks to user space: Internal timestamp xattrs are explicitly filtered out from user space visibility in `can_list()`, `hfsplus_osx_getxattr()`, and `hfsplus_osx_setxattr()`. In `hfsplus_getattr()`, both `stat-\u003ebtime.tv_sec` and `stat-\u003ebtime.tv_nsec` are explicitly assigned without leaving padding or fields uninitialized.\n3. Read safety: Output pointers in `hfsplus_read_timestamp()` are initialized on entry, and `hfsplus_get_timestamp_xattr()` validates that the xattr payload size matches `sizeof(value)` before interpreting the data.\n4. Applicability of other sanitizers: Any potential defects related to corrupted on-disk btree metadata, buffer bounds, or lock contention are detected by standard KASAN, UBSAN, and LOCKDEP.\n\nBecause no uninitialized memory reads or info-leak risks are exposed, a dedicated KMSAN fuzzing session is not warranted.",
  "NeedsKMSAN": false
}

LLM Calls Summary:
Total Calls Total Tokens Avg Tokens Total Duration (Seconds) Avg Duration (Seconds)
Tool Calls Summary:
Total Calls Total Duration (Seconds) Avg Duration (Seconds)