Skip to content
API Documentation
BastionGuard™
xfs_dir.cpp
Go to the documentation of this file.
1// xfs_dir.cpp -- shortform directory parser.
2//
3// All inode numbers in a shortform directory are either ALL 4 bytes or
4// ALL 8 bytes; the choice is signalled by sf_hdr.i8count (0 -> 4-byte,
5// non-zero -> 8-byte). The parent inode follows that same rule.
6//
7// The directory ends when we've consumed `count` entries; we DO NOT
8// rely on data_len matching exactly because the data fork can have
9// trailing zero padding.
10
11#include "xfs_dir.hpp"
12
13#include <algorithm>
14#include <string>
15
16namespace xfs {
17
18namespace {
19
20uint32_t read_u32_be(const unsigned char* p) {
21 return be32(p);
22}
23
24uint64_t read_u64_be(const unsigned char* p) {
25 return be64(p);
26}
27
28} // namespace
29
30bool parse_shortform_dir(const unsigned char* data,
31 uint32_t data_len,
32 bool has_ftype,
33 ShortformDir& out) {
34 if (data == nullptr || data_len < 4) return false;
35 out = {};
36
37 const uint8_t count = data[0];
38 const uint8_t i8count = data[1];
39 const bool use_8byte = (i8count != 0);
40 const uint32_t inumber_size = use_8byte ? 8u : 4u;
41
42 // Parent comes right after the 2-byte header.
43 uint32_t pos = 2;
44 if (pos + inumber_size > data_len) return false;
45 out.parent_ino = use_8byte ? read_u64_be(data + pos)
46 : read_u32_be(data + pos);
47 pos += inumber_size;
48
49 out.entries.reserve(count);
50
51 for (uint8_t i = 0; i < count; ++i) {
52 if (pos + 1 > data_len) return false;
53 const uint8_t namelen = data[pos];
54 ++pos;
55
56 // 2 bytes of "offset" we skip
57 if (pos + 2 > data_len) return false;
58 pos += 2;
59
60 // name
61 if (pos + namelen > data_len) return false;
62 DirEntry e;
63 e.name.assign(reinterpret_cast<const char*>(data + pos), namelen);
64 pos += namelen;
65
66 // Sanity: refuse zero-length names and refuse '/' inside names
67 // (they can never be legal on XFS but corrupted dirs sometimes
68 // contain them).
69 if (namelen == 0) return false;
70 if (e.name.find('/') != std::string::npos) {
71 // Replace any embedded '/' with underscore so the name
72 // doesn't accidentally escape the export directory.
73 std::replace(e.name.begin(), e.name.end(), '/', '_');
74 }
75
76 // V5 + FTYPE: 1-byte ftype before inumber
77 if (has_ftype) {
78 if (pos + 1 > data_len) return false;
79 e.ftype = data[pos];
80 e.ftype_valid = true;
81 ++pos;
82 }
83
84 // inumber
85 if (pos + inumber_size > data_len) return false;
86 e.ino = use_8byte ? read_u64_be(data + pos)
87 : static_cast<uint64_t>(read_u32_be(data + pos));
88 pos += inumber_size;
89
90 // Skip "." and ".." which shouldn't appear in shortform but be
91 // defensive against odd metadata.
92 if (e.name == "." || e.name == "..") continue;
93
94 out.entries.push_back(std::move(e));
95 }
96
97 return true;
98}
99
100// =============================================================================
101// Block / leaf data-block parser
102// =============================================================================
103
104namespace {
105
106// Data-block header sizes (without the leaf tail at the end, which we
107// don't parse). After this header start either data entries or unused
108// (free) records.
109//
110// V4 xfs_dir2_data_hdr: __be32 magic + 3 * (be16 off + be16 len) = 16 bytes
111// V5 xfs_dir3_data_hdr (= dir3_blk_hdr 48 bytes + 3 * 4 + 4 pad = 64 bytes)
112// dir3_blk_hdr: magic(4) + crc(4) + blkno(8) + lsn(8) + uuid(16) + owner(8) = 48
113// bestfree: 12, pad: 4
114constexpr uint32_t DATA_HDR_SIZE_V4 = 16;
115constexpr uint32_t DATA_HDR_SIZE_V5 = 64;
116
117// Round up to next 8-byte boundary
118constexpr uint32_t align8(uint32_t v) { return (v + 7u) & ~7u; }
119
120} // namespace
121
122bool parse_dir_data_block(const unsigned char* block_buf,
123 uint32_t block_size,
124 bool is_v5,
125 bool has_ftype,
126 bool accept_block_magic,
127 bool accept_data_magic,
128 std::vector<DirEntry>& out) {
129 if (block_buf == nullptr || block_size < 64) return false;
130
131 const uint32_t magic = be32(block_buf + 0x00);
132 bool magic_ok = false;
133 if (is_v5) {
134 if (accept_block_magic && magic == XFS_DIR3_BLOCK_MAGIC) magic_ok = true;
135 if (accept_data_magic && magic == XFS_DIR3_DATA_MAGIC) magic_ok = true;
136 } else {
137 if (accept_block_magic && magic == XFS_DIR2_BLOCK_MAGIC) magic_ok = true;
138 if (accept_data_magic && magic == XFS_DIR2_DATA_MAGIC) magic_ok = true;
139 }
140 if (!magic_ok) return false;
141
142 const uint32_t header_size = is_v5 ? DATA_HDR_SIZE_V5 : DATA_HDR_SIZE_V4;
143 if (block_size <= header_size) return false;
144
145 // Compute the "stop" offset. For BLOCK-format dirs there is a
146 // trailing leaf table whose start we should not enter. The leaf
147 // tail offset lives in the LAST 8 bytes of the block (v4 dir2_block_tail
148 // = u32 count + u32 stale; the leaf table itself is at end - count*8).
149 //
150 // For DATA-format blocks (in leaf-format dirs) there is no tail, so
151 // we iterate to the end of the block.
152 //
153 // Heuristic: for block-format, we trust the block_tail at the end;
154 // when corrupted we still bound by block_size - 8 to be safe.
155 uint32_t stop_offset = block_size;
156 if ((is_v5 && magic == XFS_DIR3_BLOCK_MAGIC) ||
157 (!is_v5 && magic == XFS_DIR2_BLOCK_MAGIC)) {
158 // Block tail at end: u32 count + u32 stale. Leaf table size =
159 // count * 8. Leaf table starts at block_size - 8 - count*8.
160 if (block_size >= 8) {
161 const uint32_t bt_count = be32(block_buf + block_size - 8);
162 const uint32_t leaf_table_bytes = bt_count * 8u;
163 if (8u + leaf_table_bytes <= block_size) {
164 stop_offset = block_size - 8u - leaf_table_bytes;
165 } else {
166 stop_offset = block_size > 8u ? block_size - 8u : 0u;
167 }
168 }
169 }
170 if (stop_offset <= header_size) return false;
171
172 uint32_t pos = header_size;
173 while (pos + 4 <= stop_offset) {
174 // Peek at the first 2 bytes -- a valid entry's first 2 bytes are
175 // the high two bytes of the 64-bit inumber, which CANNOT both be
176 // 0xFF (no inode is 0xFFFF...). The free-record sentinel is
177 // 0xFFFF in those exact 2 bytes.
178 const uint16_t first_word = be16(block_buf + pos);
179 if (first_word == XFS_DIR2_DATA_FREE_TAG) {
180 // Unused record: skip. Length is at +2.
181 if (pos + 6 > stop_offset) break;
182 const uint16_t freelen = be16(block_buf + pos + 2);
183 if (freelen < 16 || pos + freelen > stop_offset) {
184 // Corrupted free-record header -- bail out instead of
185 // looping forever.
186 return !out.empty();
187 }
188 pos += freelen;
189 continue;
190 }
191
192 // It's a data entry: __be64 inumber, __u8 namelen, name[], (ftype),
193 // __be16 tag, then alignment to 8.
194 if (pos + 8 + 1 > stop_offset) break; // need inumber + namelen
195 const uint64_t inumber = be64(block_buf + pos);
196 const uint8_t namelen = block_buf[pos + 8];
197 if (namelen == 0) {
198 return !out.empty(); // malformed -- stop, keep what we got
199 }
200 // entry length: 8 (inumber) + 1 (namelen) + namelen + (has_ftype ? 1 : 0)
201 // + 2 (tag), padded to 8-byte boundary.
202 const uint32_t raw_entry_len = 8u + 1u + namelen +
203 (has_ftype ? 1u : 0u) + 2u;
204 const uint32_t entry_len = align8(raw_entry_len);
205 if (pos + entry_len > stop_offset) {
206 // Truncated trailing entry; keep what we already read.
207 break;
208 }
209
210 DirEntry e;
211 e.name.assign(reinterpret_cast<const char*>(block_buf + pos + 9), namelen);
212 // Reject embedded slashes (corrupt or malicious).
213 if (e.name.find('/') != std::string::npos) {
214 std::replace(e.name.begin(), e.name.end(), '/', '_');
215 }
216
217 if (has_ftype) {
218 e.ftype = block_buf[pos + 9 + namelen];
219 e.ftype_valid = true;
220 }
221 e.ino = inumber;
222
223 // Skip "." and ".." (recovery does not export them; the parent
224 // is exported separately, and "." is the directory itself).
225 if (e.name != "." && e.name != "..") {
226 out.push_back(std::move(e));
227 }
228
229 pos += entry_len;
230 }
231
232 return true;
233}
234
235} // namespace xfs
uint32_t read_u32_be(const unsigned char *p)
Definition xfs_dir.cpp:20
uint64_t read_u64_be(const unsigned char *p)
Definition xfs_dir.cpp:24
constexpr uint32_t align8(uint32_t v)
Definition xfs_dir.cpp:118
constexpr uint16_t XFS_DIR2_DATA_FREE_TAG
Definition xfs_dir.hpp:60
uint16_t be16(const unsigned char *p)
constexpr uint32_t XFS_DIR2_BLOCK_MAGIC
Definition xfs_dir.hpp:51
constexpr uint32_t XFS_DIR3_BLOCK_MAGIC
Definition xfs_dir.hpp:52
bool parse_dir_data_block(const unsigned char *block_buf, uint32_t block_size, bool is_v5, bool has_ftype, bool accept_block_magic, bool accept_data_magic, std::vector< DirEntry > &out)
Definition xfs_dir.cpp:122
constexpr uint32_t XFS_DIR3_DATA_MAGIC
Definition xfs_dir.hpp:57
uint64_t be64(const unsigned char *p)
uint32_t be32(const unsigned char *p)
bool parse_shortform_dir(const unsigned char *data, uint32_t data_len, bool has_ftype, ShortformDir &out)
Definition xfs_dir.cpp:30
constexpr uint32_t XFS_DIR2_DATA_MAGIC
Definition xfs_dir.hpp:56
std::string name
Definition xfs_dir.hpp:63
bool ftype_valid
Definition xfs_dir.hpp:66
uint8_t ftype
Definition xfs_dir.hpp:65
uint64_t ino
Definition xfs_dir.hpp:64
uint64_t parent_ino
Definition xfs_dir.hpp:70
std::vector< DirEntry > entries
Definition xfs_dir.hpp:71