Skip to content

Support safe quarantine removal on FreeBSD - #1791

Open
val-ms wants to merge 3 commits into
Cisco-Talos:mainfrom
val-ms:CLAM-2959-freebsd-quarantine
Open

Support safe quarantine removal on FreeBSD#1791
val-ms wants to merge 3 commits into
Cisco-Talos:mainfrom
val-ms:CLAM-2959-freebsd-quarantine

Conversation

@val-ms

@val-ms val-ms commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Summary

Restore safe --move and --remove quarantine actions on FreeBSD by using
funlinkat() to atomically bind pathname removal to the file descriptor that
ClamAV retained while scanning.

This change:

  • detects funlinkat() during CMake configuration;
  • resolves an open descriptor's source path on FreeBSD with F_KINFO;
  • retains the existing no-follow traversal that pins the source parent;
  • calls funlinkat(parent_fd, basename, scan_fd, 0) to remove the source only
    if the pathname still identifies the scanned object; and
  • maps FreeBSD's EDEADLK replacement-race result to EAGAIN, matching the
    existing quarantine action-failure behavior.

Linux, macOS, Windows, and the conservative fallback for other POSIX systems
are unchanged.

Problem

The CLAM-2959 quarantine hardening prevents --move and --remove from acting
on a different filesystem object if an attacker replaces the submitted path
after it is scanned. The generic POSIX implementation captures the source in a
private directory and requires an atomic no-replace rename to restore it after
a failed action.

Supported FreeBSD releases do not provide the no-replace primitive used by
that path, so the safe fallback intentionally returns ENOTSUP. Consequently,
--remove leaves the infected source in place. The --move hard-link path
removes its quarantine link again, while the copy fallback can leave a copied
quarantine file but still reports failure and leaves the source in place.
--copy is not affected by this unlink limitation.

FreeBSD 13 and newer provide funlinkat(). The kernel atomically verifies that
the basename still names the supplied open descriptor before unlinking it,
which directly provides the object-binding guarantee required by CLAM-2959
without weakening replacement-race handling.

Affected Versions

This FreeBSD regression is present in ClamAV 1.4.5, ClamAV 1.5.3, and the
1.6.0 development branch before this change. These versions contain the
CLAM-2959 quarantine hardening but lack a FreeBSD-safe unlink implementation.
Earlier versions do not have this specific regression because they predate
that hardening.

Background and the original FreeBSD analysis are recorded in
Cisco-Talos/clamav#1755.

Resulting Behavior

  • --move and --remove remove an unchanged FreeBSD source successfully.
  • Symlink submissions act on the resolved file that was actually scanned.
  • Replacing the submitted path preserves the replacement and reports the
    quarantine action failure.
  • Search-only quarantine destination directories continue to work.
  • --copy behavior is unchanged.

Validation

Local macOS:

cmake --build build --target clamscan -j12
ctest -V -R '^(libclamav|clamscan)$'
git diff --check

FreeBSD 13.3 and 14.3 jails:

  • clean configure and full build passed;
  • generated configuration defined HAVE_FUNLINKAT=1;
  • quarantine regression subset passed with 10 tests passed and one existing
    extended-attribute test skipped; and
  • the full CTest suite passed all 7 test targets on each FreeBSD release.

The CLAM-2959 quarantine hardening requires an atomic way to prove that a
pathname still identifies the source retained during scanning before removing
it. FreeBSD lacks the no-replace rename primitive used by the generic POSIX
capture path on supported releases, causing --move and --remove to fail with
ENOTSUP.

Detect funlinkat() and use it with the retained scan descriptor after securely
traversing to the source parent. FreeBSD atomically checks that the basename
still names the retained descriptor before unlinking it. Normalize EDEADLK to
EAGAIN so replacement races follow the existing action-failure behavior.

Resolve descriptor paths through F_KINFO so FreeBSD symlink submissions retain
the real scanned source path. Other platform implementations and the
conservative unsupported-POSIX fallback remain unchanged.

Reported-by: Hiroki Imai from Ricerca Security, Inc.

CLAM-2959
@val-ms
val-ms requested review from jhumlick and a lite review from Copilot August 5, 2026 19:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The new FreeBSD F_KINFO usage is not feature-guarded, which risks breaking compilation on FreeBSD toolchains that don’t provide F_KINFO/kinfo_file.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR restores secure quarantine --move/--remove behavior on FreeBSD by leveraging funlinkat() so pathname removal is atomically bound to the scanned file descriptor, eliminating TOCTOU replacement races while keeping existing behavior unchanged on other platforms.

Changes:

  • Add CMake feature detection for funlinkat() and expose HAVE_FUNLINKAT in generated config headers.
  • Implement FreeBSD file-descriptor-to-path resolution via F_KINFO to support quarantine source-path handling.
  • Use funlinkat(parent_fd, basename, scan_fd, 0) on FreeBSD (when available), mapping EDEADLK to EAGAIN to match existing replacement-race failure semantics.
File summaries
File Description
libclamav/others.h Updates platform documentation for fd→path resolution to include FreeBSD.
libclamav/others_common.c Adds FreeBSD F_KINFO-based filepath resolution for open file descriptors.
common/actions.c Introduces FreeBSD funlinkat()-based safe unlink path for quarantine actions.
CMakeLists.txt Detects funlinkat() availability during configuration.
clamav-config.h.cmake.in Adds HAVE_FUNLINKAT to the generated config header template.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread libclamav/others_common.c Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d98bafe5f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread libclamav/others_common.c
FreeBSD F_KINFO resolves a vnode through the name cache and may return a
different hard-link name from the path used to open the scanned file. Using
that result for a path-based quarantine action could remove another link and
leave the submitted path in place.

Retain the already-resolved path supplied to path-based action sources and use
descriptor path resolution only when no authoritative path is available. Guard
F_KINFO use so FreeBSD releases without that command fall back cleanly instead
of failing to compile. Add hard-link regression coverage for move and remove
actions.

CLAM-2959

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Ready to approve

The changes are well-scoped, guarded by feature detection, and include targeted regression tests that cover the restored FreeBSD-safe unlink behavior.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

jhumlick
jhumlick previously approved these changes Aug 5, 2026
On macOS, /tmp is reported as its canonical /private/tmp path. The quarantine
directory replacement tests compared the complete clamscan success message
against paths created from the noncanonical temporary directory. A successful
copy or move could therefore be misclassified as an action failure.

Match the stable copy and move action markers instead. Existing filesystem
assertions continue to verify that the payload reached the pinned quarantine
directory and was not redirected through a replaced path.

CLAM-2959
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants