netatalk  4.6.0
Free and Open Source Apple Filing Protocol (AFP) Server
Loading...
Searching...
No Matches
subtests_lock.c File Reference

Unit tests for afpd fork/adouble open, close and locking paths. More...

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <atalk/adouble.h>
#include <atalk/directory.h>
#include <atalk/logger.h>
#include <atalk/volume.h>
#include "directory.h"
#include "faultinject.h"
#include "file.h"
#include "fork.h"
#include "peer_lock.h"
#include "subtests_lock.h"
#include "test.h"
#include "test_capabilities.h"

Functions

int utest_faultinject_selftest (const struct vol *vol)
 Framework capability probe: does fault injection reach into libatalk?
static int fd_is_open (int fd)
int utest_openfork_no_fd_leak (const struct vol *vol)
 afp_openfork() error cleanup closes an open fork's fd (no leak).
int utest_shared_adouble_refcount_balance (const struct vol *vol)
 Shared-adouble open/close refcount + fileno ledger stays balanced.
int utest_adclose_underflow_aborts (const struct vol *vol)
 ad_close() hard-fails on an adf_refcount underflow.
int utest_ro_retry_strips_destructive_flags (const struct vol *vol)
 Read-only downgrade retry re-opens without destructive flags.
int utest_ad2openflags_accmode (const struct vol *vol)
 ad2openflags(): RDONLY+SETSHRMD on the data fork is promoted to O_RDWR (share-mode locks need a writable fd); plain RDONLY stays O_RDONLY.
static void seed_path (struct path *path, char *u_name)
static int make_scratch (const struct vol *vol, const char *leaf, off_t size, char *out, size_t outlen)
int utest_testlock_range_clamp (const struct vol *vol)
 ad_testlock_range() clamps a data-zone probe; never sweeps the band.
int utest_testlock_whole (const struct vol *vol)
 ad_testlock_whole() issues one unclamped F_GETLK over the whole fd.
int utest_testlock_range_no_self_report (const struct vol *vol)
 ad_testlock_range() does not report this process's own band entry.
int utest_testlock_range_wrlck_sees_rdlck (const struct vol *vol)
 ad_testlock_range()'s F_WRLCK probe sees a peer's F_RDLCK band entry.
int utest_of_get_locks_contract (const struct vol *vol)
 of_get_locks(): positional band bitmap, independent df/rf, tri-valued.
int utest_of_get_locks_fastpath (const struct vol *vol)
 of_get_locks(): >=2-dimension whole-fd fast path, per-bit on a hit.
int utest_of_get_locks_failclosed (const struct vol *vol)
 of_get_locks(): indeterminate -> OF_LOCKS_ERROR; missing -> NOENT.
int utest_deletefile_quirk (struct vol *vol)
 deletefile()'s conflict GET through a held fd preserves the held lock.
int utest_deletefile_quirk_hazard (const struct vol *vol)
 Negative control: demonstrate the POSIX close-drops-locks hazard is real.
int utest_deletefile_nodelete (const struct vol *vol)
 deletefile() honours the NODELETE attribute (per backend).
int utest_shared_rlock (const struct vol *vol)
 Two same-range read locks from two fork owners share one refcount; the kernel lock survives the first release and drops only on the last.
int utest_overlap_strand (const struct vol *vol)
 Overlapping read locks from two fork owners coalesce in the kernel; the union unlock must release the whole coalesced range, leaving no stranded remainder.
int utest_freelock_unlck_fail_logs (const struct vol *vol)
 A failed refcount-0 F_UNLCK in adf_freelock() is logged and the entry still dropped (log-drop-continue), not silently swallowed.
int utest_fork_setmode_fdeny (const struct vol *vol)
 fork_setmode_deny() maps each access mode to the right F_SHARE deny bits. The pre-fix expression mis-parsed via operator precedence, collapsing every input to the deny-write bit or 0.
int utest_of_alloc_fifo (struct vol *vol)
 of_alloc()/of_dealloc() free-slot FIFO contract.

Detailed Description

Unit tests for afpd fork/adouble open, close and locking paths.

Uses the LD_PRELOAD fault-injection shim (faultinject.c) to force libc failures the black-box spectest cannot reach. Each test returns 0 on success, or a small nonzero code identifying the failure point (surfaced by the harness as "# got: N").

Function Documentation

◆ fd_is_open()

int fd_is_open ( int fd)
static

◆ make_scratch()

int make_scratch ( const struct vol * vol,
const char * leaf,
off_t size,
char * out,
size_t outlen )
static

◆ seed_path()

void seed_path ( struct path * path,
char * u_name )
static

◆ utest_ad2openflags_accmode()

int utest_ad2openflags_accmode ( const struct vol * vol)

ad2openflags(): RDONLY+SETSHRMD on the data fork is promoted to O_RDWR (share-mode locks need a writable fd); plain RDONLY stays O_RDONLY.

Category: targeted. This promotion is load-bearing and easy to break silently (O_RDONLY is 0, so a regression only surfaces as subtle lock behaviour or strict-backend EINVAL). The access mode has no fd/refcount ledger; observe it via the shim, which records each armed open()'s flags in fi.open_last_flags. Arm with open_fail_after = -1 to record without failing. Skips where the shim cannot intercept libatalk's open().

◆ utest_adclose_underflow_aborts()

int utest_adclose_underflow_aborts ( const struct vol * vol)

ad_close() hard-fails on an adf_refcount underflow.

Category: targeted (nuanced). Accounting invariants are too broad to reach this: the underflow guard's else-branch fires only in a desync no correct open/close sequence produces — fileno still valid (>= 0) yet adf_refcount already <= 0 (a balanced close clears the fileno exactly as the refcount hits 0). The guard is defense-in-depth against a future stray double-close on a shared adouble, so the test deliberately manufactures that precise state: open the data fork, force adf_refcount to 0, then ad_close() — the outer fileno guard passes, the refcount else fires, AFP_ASSERT (live in debugoptimized) abort()s.

AFP_ASSERT abort()s the process, so run it in a fork()ed child and assert the child died via SIGABRT; the parent (the harness) survives. AFP_ASSERT self-gates on NDEBUG: in a release/NDEBUG build it compiles to a no-op and the guard cannot abort, so the test skips there rather than failing — its result is correctly build-type-aware.

◆ utest_deletefile_nodelete()

int utest_deletefile_nodelete ( const struct vol * vol)

deletefile() honours the NODELETE attribute (per backend).

Category: targeted (FU-D, the NODELETE read path). Create a file, set ATTRBIT_NODELETE, and assert deletefile(checkAttrib=1) returns AFPERR_OLOCK and leaves the file; clear it and assert the delete succeeds. Driven with the volume's own backend (called once per backend from test.c).

◆ utest_deletefile_quirk()

int utest_deletefile_quirk ( struct vol * vol)

deletefile()'s conflict GET through a held fd preserves the held lock.

Category: targeted (the same-process lock-on-close repro). Register a tracked ofork holding a byte-range lock on an inode, then drive of_get_locks() the way deletefile() does (reusing the held fd, opening nothing). Assert the conflict read succeeds and a post-call F_GETLK in this process still shows the held lock (a transient open+close would have dropped it). No peer / no shim needed.

◆ utest_deletefile_quirk_hazard()

int utest_deletefile_quirk_hazard ( const struct vol * vol)

Negative control: demonstrate the POSIX close-drops-locks hazard is real.

Category: targeted (the hazard utest_deletefile_quirk's cure defends against). POSIX advisory locks are owned by the process, so closing ANY fd to an inode drops every lock the process holds on it. This test reproduces that hazard directly: take a lock through one fd, then open and close a SECOND, transient fd to the same inode, and observe (via a peer) that the lock is now gone — exactly what the legacy delete-time transient open+close did to a held fork's lock.

Without this control, utest_deletefile_quirk's "the lock survived" could pass vacuously on a platform that never drops the lock in the first place. Here, if the transient close does NOT drop the lock, this platform does not exhibit the quirk, so there is nothing to defend against: return TEST_SKIP (the cure is trivially safe here and its positive result is not meaningful). A reproduced hazard returns 0 — the danger is real, and the cure test then proves 9B avoids it. Cross-process peer (our own F_GETLK never reports our own locks).

◆ utest_faultinject_selftest()

int utest_faultinject_selftest ( const struct vol * vol)

Framework capability probe: does fault injection reach into libatalk?

Category: framework integrity (meta-test). Not a netatalk behavioural test — it validates the harness. The LD_PRELOAD shim interposes libc symbols at dynamic-link resolution; this arms an EMFILE on the next open() and calls ad_open() (whose open() lives in the shared libatalk.so), then checks the open failed with the injected errno.

Interception is not reliable on every platform — macOS's two-level namespace ignores a plain symbol preload, so the injected fault never fires there. In that case this returns TEST_SKIP rather than failing: the result is then honest per-platform (injection-based tests run where the mechanism works and skip where it does not).

The errno check lives only in the probe, which owns a direct open() it controls; after ad_open() returns, errno is unreliable (its EC_* error path runs close()/logging that can overwrite it), so the libatalk leg asserts only that the open FAILED while armed, not the specific errno.

◆ utest_fork_setmode_fdeny()

int utest_fork_setmode_fdeny ( const struct vol * vol)

fork_setmode_deny() maps each access mode to the right F_SHARE deny bits. The pre-fix expression mis-parsed via operator precedence, collapsing every input to the deny-write bit or 0.

◆ utest_freelock_unlck_fail_logs()

int utest_freelock_unlck_fail_logs ( const struct vol * vol)

A failed refcount-0 F_UNLCK in adf_freelock() is logged and the entry still dropped (log-drop-continue), not silently swallowed.

◆ utest_of_alloc_fifo()

int utest_of_alloc_fifo ( struct vol * vol)

of_alloc()/of_dealloc() free-slot FIFO contract.

Public API only (of_alloc/of_dealloc/of_find): refnum == slot and never 0; of_find() round-trips a live refnum and rejects a closed one; a freed slot waits behind all other free slots before reuse (FIFO, not LIFO).

◆ utest_of_get_locks_contract()

int utest_of_get_locks_contract ( const struct vol * vol)

of_get_locks(): positional band bitmap, independent df/rf, tri-valued.

Category: targeted. With a peer holding a DENY_WR band lock, request a subset of band bits plus the data range and assert: the held bit comes back in its own position, an unrequested neighbour bit stays 0, df/rf flags are independent, and the status is OF_LOCKS_OK (separate from the data). Cross-process peer.

◆ utest_of_get_locks_failclosed()

int utest_of_get_locks_failclosed ( const struct vol * vol)

of_get_locks(): indeterminate -> OF_LOCKS_ERROR; missing -> NOENT.

Category: targeted (fail-closed contract). A missing target returns OF_LOCKS_NOENT (definite: no locks), distinct from an unreadable one. A forced F_GETLK hard error (shim fcntl -> EIO) must return OF_LOCKS_ERROR, never a silent "no conflict". Skips without shim fcntl interception for the error leg.

◆ utest_of_get_locks_fastpath()

int utest_of_get_locks_fastpath ( const struct vol * vol)

of_get_locks(): >=2-dimension whole-fd fast path, per-bit on a hit.

Category: targeted. With >= 2 data-fd dimensions requested on a clear file, exactly one whole-fd F_GETLK is issued and the per-bit band loop is skipped. With a peer band lock present, the whole-fd probe hits and the per-bit loop then runs and attributes the exact bit. Counts F_GETLK via the shim.

◆ utest_openfork_no_fd_leak()

int utest_openfork_no_fd_leak ( const struct vol * vol)

afp_openfork() error cleanup closes an open fork's fd (no leak).

Category: accounting invariant. The invariant: after the open-error cleanup ad_close(DF|RF|HF|SETSHRMD), no fork fd survives. When afp_openfork() fails partway it must close whatever the first ad_open() opened, or that fd leaks into the long-lived child. Verified by accounting (not an fd-count scan, which is racy and Linux-only): capture the raw data-fork fd, run the cleanup, and assert two independent ledgers agree the fd is gone — the ad layer (ad_data_fileno == -1) and the kernel (fcntl(fd) -> EBADF). A negative control (fd open before cleanup) keeps the assertion from passing vacuously.

◆ utest_overlap_strand()

int utest_overlap_strand ( const struct vol * vol)

Overlapping read locks from two fork owners coalesce in the kernel; the union unlock must release the whole coalesced range, leaving no stranded remainder.

◆ utest_ro_retry_strips_destructive_flags()

int utest_ro_retry_strips_destructive_flags ( const struct vol * vol)

Read-only downgrade retry re-opens without destructive flags.

Category: targeted (nuanced). ad_open_df() retries a failed open() read-only when the caller passed ADFLAGS_SETSHRMD | ADFLAGS_RDONLY and attempt 1 hit EACCES/EPERM/EROFS; the retry must strip O_TRUNC/O_CREAT/O_EXCL, not just flip the access mode, or a SETSHRMD|RDONLY|TRUNC caller would truncate on the retry a file it asked to open read-only. No fd/refcount ledger sees this – the bug is in which flags the second open() carries – so drive the retry and inspect the flags the shim recorded. ad2openflags() makes attempt 1 O_RDWR|O_TRUNC; arming EACCES on it forces the retry, whose flags land in open_last_flags (open_calls == 2). Skips where the shim cannot intercept libatalk's open().

◆ utest_shared_adouble_refcount_balance()

int utest_shared_adouble_refcount_balance ( const struct vol * vol)

Shared-adouble open/close refcount + fileno ledger stays balanced.

Category: accounting invariant (broad). When several forks reference one inode they share a single struct adouble; ad_open_df()'s already-open path bumps the fd-level adf_refcount instead of opening a second fd, and ad_close() decrements it, closing the fd only at the last reference. This pins down that whole family with one test by walking the ledger across a multi-reference open/close sequence and asserting the two coupled invariants at every step:

  • the fd is open iff adf_refcount > 0 (fileno reset to -1 exactly at 0);
  • refcount tracks references exactly (N opens need N closes; an early close must not close the shared fd while a sibling reference remains). It is the regression net that protects every open/close/cleanup path the fork series touches.

◆ utest_shared_rlock()

int utest_shared_rlock ( const struct vol * vol)

Two same-range read locks from two fork owners share one refcount; the kernel lock survives the first release and drops only on the last.

◆ utest_testlock_range_clamp()

int utest_testlock_range_clamp ( const struct vol * vol)

ad_testlock_range() clamps a data-zone probe; never sweeps the band.

Category: targeted. A whole-data-zone request (len == 0) must be bounded to the data zone (issued l_len != 0, l_start + l_len <= BYTELOCK_MAX), not turned into a POSIX l_len == 0 ("to infinity") that would reach the share-mode band. An explicit band probe (off >= AD_FILELOCK_BASE) must pass through unclamped. Observed via the shim's lock-fcntl recorder. Skips where the shim cannot intercept libatalk's fcntl().

◆ utest_testlock_range_no_self_report()

int utest_testlock_range_no_self_report ( const struct vol * vol)

ad_testlock_range() does not report this process's own band entry.

Category: targeted (the crux of using a sibling primitive). Plant an OPEN_RD band entry owned by this process via ad_lock(), then ad_testlock_range on the same offset must return 0 (kernel F_GETLK never reports our own locks), while the array-scanning ad_testlock() returns 1 on the same handle. No shim needed.

◆ utest_testlock_range_wrlck_sees_rdlck()

int utest_testlock_range_wrlck_sees_rdlck ( const struct vol * vol)

ad_testlock_range()'s F_WRLCK probe sees a peer's F_RDLCK band entry.

Category: targeted (the read-lock visibility the old detector missed). The legacy delete probe opened the fork read-only and so issued an F_RDLCK test, which does not conflict with another holder's read lock — a peer read-lock (and the F_RDLCK share-mode band entries) were invisible. ad_testlock_range() always probes with an explicit F_WRLCK, which conflicts with a peer's read OR write lock, so it must report a peer's F_RDLCK as a conflict. A forked peer holds an F_RDLCK at a band offset; assert the range probe returns 1. Control: the same offset with no peer returns 0. Cross-process (our own F_GETLK never reports our own locks), no shim needed.

◆ utest_testlock_whole()

int utest_testlock_whole ( const struct vol * vol)

ad_testlock_whole() issues one unclamped F_GETLK over the whole fd.

Category: targeted. The fast-path probe must be deliberately unclamped (l_start == 0, l_len == 0) so it spans the data zone and the share-mode band in a single call, and report a peer's band lock as a conflict. Uses a forked peer holding a band-offset lock; skips without shim fcntl interception.