Skip to content

Commit 973b42b

Browse files
committed
Limit symlink targets to 1024 bytes (XFS_SYMLINK_MAXLEN)
XFS limits symlink targets to 1024 bytes, and since generic Linux containers are commonly backed by XFS, enforce that limit in both the dumpfile parser and the EROFS reader rather than allowing up to PATH_MAX (4096). This also avoids exercising a known limitation in our EROFS reader where symlink data that spills into a non-inline data block (which can happen with long symlinks + xattrs) is not read back correctly. See composefs/composefs#342 for the corresponding C fix for that edge case. Assisted-by: OpenCode (Claude Opus 4) Signed-off-by: Colin Walters <walters@verbum.org>
1 parent c6ef089 commit 973b42b

5 files changed

Lines changed: 30 additions & 8 deletions

File tree

crates/composefs/src/dumpfile_parse.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use crate::MAX_INLINE_CONTENT;
2424

2525
/// https://github.com/torvalds/linux/blob/47ac09b91befbb6a235ab620c32af719f8208399/include/uapi/linux/limits.h#L13
2626
const PATH_MAX: u32 = 4096;
27+
use crate::SYMLINK_MAX;
2728
/// https://github.com/torvalds/linux/blob/47ac09b91befbb6a235ab620c32af719f8208399/include/uapi/linux/limits.h#L15
2829
/// This isn't exposed in libc/rustix, and in any case we should be conservative...if this ever
2930
/// gets bumped it'd be a hazard.
@@ -456,8 +457,10 @@ impl<'p> Entry<'p> {
456457
let target =
457458
unescape_to_path(payload.ok_or_else(|| anyhow!("Missing payload"))?)?;
458459
let targetlen = target.as_os_str().as_bytes().len();
459-
if targetlen > PATH_MAX as usize {
460-
anyhow::bail!("Target length too large {targetlen}");
460+
if targetlen > SYMLINK_MAX {
461+
anyhow::bail!(
462+
"Symlink target length {targetlen} exceeds limit {SYMLINK_MAX}"
463+
);
461464
}
462465
Item::Symlink { nlink, target }
463466
}

crates/composefs/src/erofs/reader.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1350,6 +1350,14 @@ fn populate_directory<ObjectID: FsVerityHashValue>(
13501350
}
13511351
S_IFLNK => {
13521352
let target_data = child_inode.inline().unwrap_or(&[]);
1353+
if target_data.len() > crate::SYMLINK_MAX {
1354+
anyhow::bail!(
1355+
"symlink target for {:?} is {} bytes (max {})",
1356+
name,
1357+
target_data.len(),
1358+
crate::SYMLINK_MAX,
1359+
);
1360+
}
13531361
let target = OsStr::from_bytes(target_data);
13541362
tree::LeafContent::Symlink(Box::from(target))
13551363
}

crates/composefs/src/erofs/writer.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,12 @@ impl<ObjectID: FsVerityHashValue> Leaf<'_, ObjectID> {
293293
(format::DataLayout::FlatPlain, 0, 0)
294294
}
295295
tree::LeafContent::Symlink(target) => {
296+
assert!(
297+
target.len() <= crate::SYMLINK_MAX,
298+
"symlink target is {} bytes (max {})",
299+
target.len(),
300+
crate::SYMLINK_MAX,
301+
);
296302
(format::DataLayout::FlatInline, 0, target.len() as u64)
297303
}
298304
};

crates/composefs/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ pub const INLINE_CONTENT_MAX_V0: usize = 64;
4545
/// <https://github.com/composefs/composefs-rs/issues/107>).
4646
pub const MAX_INLINE_CONTENT: usize = 512;
4747

48+
/// Maximum symlink target length in bytes.
49+
///
50+
/// XFS limits symlink targets to 1024 bytes (`XFS_SYMLINK_MAXLEN`). Since
51+
/// generic Linux containers are commonly backed by XFS, we enforce that
52+
/// limit rather than the Linux VFS `PATH_MAX` of 4096.
53+
pub const SYMLINK_MAX: usize = 1024;
54+
4855
/// Internal constants shared across workspace crates.
4956
///
5057
/// Not part of the public API — may change without notice.

crates/composefs/src/test.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,8 +131,7 @@ pub(crate) mod proptest_strategies {
131131
/// EROFS limit (`EROFS_NAME_LEN`).
132132
const NAME_MAX: usize = 255;
133133

134-
/// Maximum symlink target length on Linux (`PATH_MAX`).
135-
const PATH_MAX: usize = 4096;
134+
use crate::SYMLINK_MAX;
136135

137136
/// Strategy for valid filenames as OsString.
138137
///
@@ -230,8 +229,7 @@ pub(crate) mod proptest_strategies {
230229
/// Strategy for symlink targets as OsString.
231230
///
232231
/// Symlink targets on Linux are arbitrary bytes except `\0`, up to
233-
/// [`PATH_MAX`] (4096) bytes. We generate a mix of path-like ASCII
234-
/// targets and binary targets, occasionally long.
232+
/// [`SYMLINK_MAX`] (1024) bytes, matching the XFS limit.
235233
fn symlink_target() -> impl Strategy<Value = OsString> {
236234
prop_oneof![
237235
// Short path-like ASCII target (common case)
@@ -241,8 +239,8 @@ pub(crate) mod proptest_strategies {
241239
// Binary target with arbitrary bytes (no NUL)
242240
3 => prop::collection::vec(1..=0xFFu8, 1..=100)
243241
.prop_map(OsString::from_vec),
244-
// Long ASCII target (up to PATH_MAX)
245-
1 => proptest::string::string_regex(&format!("[a-zA-Z0-9/._-]{{100,{PATH_MAX}}}"))
242+
// Long ASCII target (up to SYMLINK_MAX)
243+
1 => proptest::string::string_regex(&format!("[a-zA-Z0-9/._-]{{100,{SYMLINK_MAX}}}"))
246244
.expect("valid regex")
247245
.prop_map(OsString::from),
248246
]

0 commit comments

Comments
 (0)