Added support for decmpfs raw compression (9 and 10) - #88
Conversation
|
Can you provide corresponding test data as well? |
2ec3fed to
9de2c00
Compare
|
@joachimmetz Apologies for the confusion - meant to open this as a draft initially. Added test cases for type 9 - still need to go through and clean up type 10. Will mark this for review when I'm done. |
|
thanks for the proposed changes and the explanation. Would be good to have test files with the corresponding format cases as well. |
9de2c00 to
56c0cac
Compare
|
Alright - tests have been added in. I believe everything matches with the existing harness - quite a bit of template-y code for a rather simple format here. In testing, I found 0 real-world examples of type 10 files, and zero instances of corrupt non 0xCC type 9 files. This was checked on both Intel (Sequoia) and Apple Silicon (Tahoe). -> The 0xCC behavior is clear, but non-0xCC returns empty on MacOS rather than actually providing an error. This is almost certainly an error state based on the behavior, so I set things up to throw an error. I imagine this is the desired behavior. I do have test code I used locally to create an APFS volume and confirm MacOS + libfsapfs return identical results. That can be added to the CI/CD pipeline if you're okay with something that only works on the MacOS runners, but is omitted from this PR as-is. |
This is why it was never added. Previously I was able to create other cases with afstools see https://github.com/dfirlabs/apfs-specimens |
|
Makes sense. As far as I can tell Apple is using type 9 to minimize file reads during boot. Still find it a bit weird they have an explicit uncompressed type instead of just using sentinels with another type, but it is cleaner. Honestly if I'd known type 9 to be decompressed I wouldn't have implemented anything here. I was sort of hoping type 9/10 would lzma/zstd, and didn't quite clue in to the concept of an actually fully uncompressed type until it became obvious there literally was no other code path. Any interest in LZFSE / the bitmaps? I've already done about 80% of the work for those as well, so it's just a matter of packaging it up. |
Still useful research, maybe add additional notes to the format documentation?
Sure. Would recommend doing this in a separate PR though. |
|
Separate PR for sure for LZFSE / bitmaps. No guarantees I get around to that. Want me to add type 9/10 doc updates to this PR? Nothing groundbreaking but having the specific format would be a mild improvement. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #88 +/- ##
==========================================
+ Coverage 28.05% 28.50% +0.44%
==========================================
Files 73 73
Lines 15918 15885 -33
Branches 3659 3664 +5
==========================================
+ Hits 4466 4528 +62
+ Misses 10281 10170 -111
- Partials 1171 1187 +16 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Types 9 (xattr-inline) and 10 (resource-fork chunked) store file data verbatim rather than compressing it. Both use a 0xCC sentinel as the first byte of the payload to distinguish them from actual compressed streams; all subsequent bytes are raw plaintext. Changes: - definitions.h.in: add LIBFSAPFS_COMPRESSION_METHOD_STORED = 4 - file_entry.c: dispatch cases 9 and 10 to STORED; add type 10 to the resource-fork routing condition (even-numbered types use rsrc fork) - compression.c: add STORED decompressor that validates the 0xCC sentinel and memcpys the remaining bytes, mirroring the existing LZVN 0x06 stored-chunk path - compressed_data_handle.c: add STORED to the compression method allowlist in libfsapfs_compressed_data_handle_initialize Empirical evidence (macOS 26.5.1, no-SIP VM + dtrace): - ARM64e kernel disassembly of the type-9 READ path shows a 3-instruction sequence ending in BL uiomove64 with zero CMP #0xCC in the entire function — the kernel takes a pure memcpy path, no decompressor involved. - dtrace apfs_vnop_getxattr tracing confirms the kernel never consults com.apple.ResourceFork for type-9 reads; only com.apple.decmpfs is read. - Any first byte other than 0xCC causes the kernel to return 0 bytes (decmpfs_validate_compressed_file fails to populate the cnode). - Type 9 max payload: 3785 bytes (APFS inline xattr limit of 3804 bytes minus 2 bytes xv_flags, 16 bytes decmpfs header, 1 byte sentinel). - Type 10 confirmed functional with correct chunk table format (table_size = (N+1)*4, chunk_ends[] = absolute end offsets from rsrc start). - 0 type-10 files observed in 662K macOS 26.5.1 system files (full scan via XATTR_SHOWCOMPRESSION); type 9 has 545 files in the same scan.
fsapfs_test_compression.c (libfsapfs_decompress_data): - valid 0xCC sentinel + payload, with output verified via memory_compare - bad sentinel -> -1 with error set - SSIZE_MAX and zero-size bounds cases, plus a memcpy-failure path - mirrors the existing DEFLATE and LZVN case structure fsapfs_test_compressed_data_handle.c: - get_compressed_block_offsets STORED cases for the type-9 fpmc single-block path and the type-10 chunk-table path - read_segment_data STORED cases for both paths, with decompressed bytes verified via memory_compare Verified with the compression and compressed_data_handle test programs passing on macOS / ARM64.
|
A couple of updates, I'll make some tweaks to this PR to merge it:
|
56c0cac to
1d2dd79
Compare
|
BTW with bitmaps are you referring to LZBITMAP (#73) ? if so libfmos will need to be first updated with support for this compression method first. |
Yes I currently have a compressor + decompressor that is compatible with Apple's here. Planning to release it within the next 2 months. Will be a GPL compatible license, undecided what yet. Couldn't win on speed (LZBITMAP is stupidly fast), but got about a quarter of the way there in both directions. I did manage to win on ratio by ~5% if you don't care about compression speed. There's also https://github.com/eafer/libzbitmap. It's a great starting point if you'd like to build your own. There's a few things that it seems the format should support that Apple's decompressor refuses, which probably is the reason behind whatever trick they're using to get 6GB/s decompression on a single thread. |
like is a strong word in this context. Libfsapfs would need something that could be used cross platform for decompression only, but understanding the compression method is beneficial for troubleshooting data format edge cases. |
Summary
Types 9 and 10 are the "stored" (uncompressed) decmpfs variants. The library currently hits the
default:error branch for both. This PR adds support for reading files compressed with these types.Format
Both types use a single byte
0xCCas a sentinel marking stored (uncompressed) data: for type 9 it sits immediately after the 16-byte decmpfs header; for type 10 it prefixes each resource-fork chunk. All remaining bytes are raw plaintext.com.apple.decmpfsxattr (max ~3785 bytes of plaintext)com.apple.ResourceForkusing the same chunk-table format as types 8/12/14, with each chunk starting with0xCCfollowed by raw dataThe
0xCCsentinel presumably serves the same role as0xFFin DEFLATE stored chunks and0x06in LZVN stored chunks to cause a compressor-skip.0xFF0x060xCC0xFFType 9 files exist within the OS, however I was unable to locate any type 10 files. The type 10 decompression behavior does exist in the kernel, however, and the behavior matched up cleanly with expectations in testing.
Implementation
libfsapfs_definitions.h.in: AddLIBFSAPFS_COMPRESSION_METHOD_STORED = 4libfsapfs_file_entry.c: Dispatch cases 9 and 10 toSTORED; add type 10 to the resource-fork routing condition (even-numbered types use rsrc fork)libfsapfs_compression.c: AddSTOREDbranch that validates the0xCCsentinel andmemory_copys the remaining bytes. Structurally identical to the existing LZVN stored-chunk pathlibfsapfs/libfsapfs_compressed_data_handle.c: AddSTOREDto supported formats in method checks.dtrace + kernel disassembly (macOS 26.4, ARM VM)
Type 9 stored data is copied verbatim - no decompressor in the path. The read path computes
src = xattr_buf + 17(past the 16-byte decmpfs header + 1 sentinel byte) andlen = uncompressed_size, then callsuiomove64. No branch to any decompressor or compression kext appears in the path.0xCCis an open-time validation sentinel, not a stream opcode. It is checked once indecmpfs_validate_compressed_file(loads byte 16 of the xattr, bit-tests it), not by any decompressor.non-
0xCCWith a wrong sentinel, validation fails, thedecmpfscnode is never populated, and reads return 0 bytes. Since there is no decompressor in the path, the empty read cannot be "a decompressor rejecting bad input". dtraceapfs_vnop_getxattrshows onlycom.apple.decmpfsis ever read for a type-9 file.Type 10 uses the same chunk table as types 8/12/14 (
table_size = (num_chunks + 1) * 4,chunk_ends[i]= absolute end offset from rsrc start); on-disk type-10 files were created and read back correctly.