Skip to content
Shell & TerminalDeep Dive Published Updated 5 min readViews unavailable

stat in Shell Scripts: Metadata Formats, Symlinks, and Race Limits

Read file metadata with an explicit stat implementation and format while avoiding fragile parsing, symlink confusion, and check-then-use security claims.

The stat utility reports filesystem metadata, but it is not one portable command-line interface across Unix systems. GNU stat commonly uses -c for a format string; BSD stat commonly uses -f for a format string, while GNU uses -f to report filesystem metadata. Format directives also differ. A script that assumes one syntax can print the wrong fields or fail when moved to another host.

Metadata is a snapshot. A stat result describes an object at a moment in time; it does not reserve that object or guarantee that a later open, chmod, copy, or delete will act on the same object. Use stat for inventory, reporting, and advisory checks. Do not describe a stat-then-act shell sequence as a race-free authorization boundary.

Pin the implementation and output format

Establish which utility will run: invoke the expected binary, record its version where supported, and test the format string on target operating systems. GNU coreutils can report inode, device, size, and mode with an explicit format:

stat -c '%d:%i %s %a' -- "$path"

For FreeBSD, option and format grammar differ; consult that release’s stat manual rather than mechanically replacing -c with -f. macOS is BSD-derived, but release details should be tested if it is in the support matrix. An unqualified stat can be affected by shell functions or aliases interactively. Scripts should use a known PATH or explicit utility path.

Choose only fields required for a defined purpose. Human-oriented default output is not a stable machine protocol: labels, time zone formatting, locale, and spacing can change. A custom format narrows the interface, but filenames may contain tabs or newlines and make line-oriented output ambiguous. For machine processing, use a NUL-aware mode where supported or process one path at a time using an API that returns structured fields.

GNU stat reports a symbolic link itself by default; -L follows the target. BSD variants have their own options and defaults. The distinction affects file type, inode, permissions, owner, size, and timestamps. A report about “the file” is ambiguous unless it states whether it means the link object or referent.

Broken links are important test cases. A command that follows links may fail on a broken target, while no-follow mode can still report the link. A link can point outside the expected tree or change between inspection and later use. If the purpose is to inventory link objects, do not follow them accidentally. If the purpose is to authorize access to targets, a shell-level canonical path check is not a substitute for opening the target with desired kernel-enforced flags.

Metadata output is not a safe record protocol

It is tempting to create rows containing path, owner, mode, and size separated by tabs. That is unsafe for arbitrary file names because tab and newline are valid filename bytes. Even if the utility quotes names for display, parsing shell-quoted output requires a shell parser and can create code injection risk. Do not eval stat output or reconstruct arguments by splitting printed text.

Prefer a NUL-aware protocol for pathnames. GNU stat has filename output modes that can include NUL separators; support and exact syntax vary by implementation. When portability matters, iterate with a find -exec helper written in a language with safe filesystem APIs, or use a manifest format that correctly escapes strings. Separate metadata fields from filenames and define an unambiguous encoding.

Locale and time zones matter. Numeric mode and epoch timestamps are more stable than localized names and dates, but units and precision should still be explicit. Network and cached filesystems may return stale attributes. GNU stat exposes cached-attribute controls on supported filesystems, but refreshing attributes can trigger work and increase latency. Do not use timestamps as proof of content identity; hash content when that is the actual question.

Check-then-act is a race

if [ "$(stat -c '%a' -- "$path")" = 600 ]; then
    read_secret "$path"
fi

This is not an access-control guarantee. The path could be replaced after stat and before read_secret opens it. Symlinks, renames, mount changes, and concurrent writers complicate the relationship between the inspected object and later operation. A mode check may omit ACLs, parent-directory permissions, capabilities, and the identity of the process that opens the file.

For security decisions, rely on operating-system permissions and a helper that opens the file once, validates the opened descriptor, and uses that same descriptor. Directory-relative opens and no-follow flags can constrain resolution on suitable systems. A shell script cannot emulate every such primitive with a sequence of path checks.

Metadata can still be an operational invariant. A deployment can assert that a generated config has owner root and mode 0640 after installation. That assertion should be part of post-deploy validation and report failure; it should not be portrayed as protection against an attacker who can concurrently replace the file or its parent.

Build a portable metadata adapter

If a fleet includes GNU and BSD systems, hide differences behind one small tested function per platform. Select the implementation using deployment configuration, not a weak uname guess. Test formats on CI images matching each supported OS. Treat unknown implementations as unsupported and fail with a clear diagnostic rather than silently producing malformed output.

For an inventory, document field names, type, units, symlink policy, locale, and failure behavior. Verify output with paths containing spaces, quotes, tabs, and newlines. Test missing files, permission-denied parents, broken links, sockets, devices, directories, and changes during the scan. A partial report must carry an incomplete status if any entry could not be inspected.

stat is a reporting utility, not an object handle, authorization primitive, or serialization format. Pin its dialect, state the symlink policy, design unambiguous output, and use descriptor-based tools when a security decision depends on object identity.

Related:

Sources:

Comments