OmniCube Reference Manualoc_lock(3)

oc_lock(3)

Library Functions · OmniCube · August 10, 2026

NAME

oc_lock, acquire_lock_or_exit, _ensure_shared_dir - acquire a single-instance OmniCube lock and maintain the shared lock directory

SYNOPSIS

amp;. /opt/omnicube/lib/common/utils.sh

acquire_lock_or_exit lockdir
_ensure_shared_dir path

DESCRIPTION

OmniCube jobs are started from cron, from SMF methods and by hand, sometimes all three within the same minute. Every job that mutates state (snapshots, zfs sends, pool imports, streak counters) therefore takes a single-instance lock first. The lock is a directory under ${LOCK_BASE}, which is /var/run/omnicube and hard-coded.

acquire_lock_or_exit

Attempts mkdir lockdir once, with stderr discarded. Directory creation is atomic in the kernel, so the directory is the mutex and concurrent invocations contend correctly. Three outcomes:

created

Return 0. The caller holds the lock and proceeds.

already existed

exit 0 silently. A concurrent run of the same job is a normal, expected condition on a busy node, so it must produce no output, no non-zero status and no cron mail.

mkdir

failed for any other reason Report through error() (see oc_log(3)) and exit 1. This covers a missing parent, EPERM because the caller is not in ${LOCK_GROUP}, ENOSPC on tmpfs, and a plain file sitting where the lock directory should be.

This single call replaces the older

[ -d ${LOCK_DIR} ] && exit 0; mkdir ${LOCK_DIR} || exit 1

two-step, which had a TOCTOU window between the test and the mkdir, and which printed a bogus File exists on standard error on every concurrent invocation, generating cron mail for a non-event.

The function never removes anything. Releasing the lock is entirely the caller's responsibility and must be done from a trap, so that the lock is also released when the job is killed, when the run-level guard aborts it (see oc_runlevel(3)) and when it exits on error:

trap '[[ -d ${LOCK_DIR} ]] && rmdir ${LOCK_DIR} 2>/dev/null' \\
    EXIT HUP INT TERM

rmdir, not rm -rf: the lock directory is expected to be empty, and a recursive removal of a path built from a variable is not a risk worth taking in a trap.

The conventional lock name embeds ${logtag} so that two SMF instances of the same job (see OC_SMF_INSTANCE in omnicube_utils(3)) do not lock each other out:

LOCK_DIR=${LOCK_BASE}/${logtag}.autocleansnap.lock

_ensure_shared_dir

Creates path if needed and normalizes it to mode 1775 with group ${LOCK_GROUP}. The library calls it on ${LOCK_BASE} every single time it is sourced, because /var/run is tmpfs on illumos and is empty after every boot; consumers call it on their own runtime directories that have the same sharing requirement, for example /var/run/omnicube/sys_monitor in sys_monitor(8).

Steps: refuse immediately if path is a symbolic link; mkdir -p if it is not already a directory; re-test that it is now a directory and still not a symlink, since a concurrent racer may have won the creation; then chgrp ${LOCK_GROUP} and chmod 1775. Both ownership operations are attempted unconditionally and both have their errors discarded.

The symlink refusal is the security-relevant part. ${LOCK_BASE} is created by whichever OmniCube job runs first after boot, which may be an unprivileged pfexec(1) operator. If that path were a symlink, a later root invocation would chgrp and chmod the symlink target, handing an attacker a group-writable directory of their choosing. The shell has no O_NOFOLLOW equivalent for chmod or chgrp, so the only safe action is to touch nothing and let the caller fail loudly on its next mkdir.

Ownership model

Locks are shared between root and operators through the group, not by patching modes on individual lock directories:

mode 1775

Owner and group may create entries; others may not. The sticky bit means one user cannot rmdir another user's lock, so a stray operator run cannot release the lock a root cron job is holding.

group ${LOCK_GROUP}

From the SMF property config/lock_group, accepted only if it matches ^[a-zA-Z0-9_-]+$, otherwise sysadmin (gid 14, present on a stock illumos install).

The consequence is a deployment requirement: every account that runs these tools under pfexec(1) must be a member of ${LOCK_GROUP}. A non-member gets EPERM from mkdir and acquire_lock_or_exit() exits 1 with an error() line, which is the intended, visible failure. Nothing in the library grants access by widening a mode.

chgrp is attempted by every caller and not just by root because POSIX allows an owner who is also a member of the target group to change a directory's group. A ${LOCK_BASE} created by an operator therefore converges on the correct group and mode at the first invocation after boot rather than waiting for the next root cron tick.

RETURN VALUES

acquire_lock_or_exit()

Returns 0 when the lock was acquired. Otherwise it does not return: it calls exit 0 when the lock is already held by another run, and exit 1 after an error() for any other mkdir failure. Because it exits rather than returns, it must be called from the script body and not from a subshell or a pipeline, where exit would only terminate the subshell.

_ensure_shared_dir()

0 if path exists as a real directory afterwards (whether or not the chgrp and chmod succeeded), 1 if it is a symlink or could not be created. Callers are expected to test it and degrade, as sys_monitor(8) does when it disables lock-streak detection for the run.

FILES

/var/run/omnicube

${LOCK_BASE}, mode 1775, group ${LOCK_GROUP}. On tmpfs, so all locks vanish at boot; a lock cannot be stale across a reboot.

/var/run/omnicube/<logtag>.<job>.lock

Conventional per-job lock, e.g. <logtag>.sys_monitor.lock or <logtag>.autocleansnap.lock.

/var/run/omnicube/sync_pool.lock
/var/run/omnicube/pool_monitor.lock

Locks whose names are fixed rather than ${logtag}-derived.

/var/run/omnicube/isolate_node.lock

${isolate_lock}. A state marker, not a mutex: isolate_node.sh(8) creates it with its own mkdir, stores a mode file inside it, and the monitors only test for its presence. acquire_lock_or_exit () is not used for it.

ENVIRONMENT

OC_SMF_INSTANCE

Changes ${logtag}, and hence the name of every ${logtag}-derived lock, giving each SMF instance its own mutex. Must be set before utils.sh is sourced.

EXAMPLES

Example 1: the standard prologue

amp;. /opt/omnicube/lib/common/utils.sh
LOCK_DIR=${LOCK_BASE}/${logtag}.myjob.lock
acquire_lock_or_exit "${LOCK_DIR}"
trap '[[ -d ${LOCK_DIR} ]] && rmdir ${LOCK_DIR} 2>/dev/null' \\
    EXIT HUP INT TERM
abort_if_shutting_down

Example 2: a shared runtime directory of one's own

STREAK_DIR=/var/run/omnicube/myjob
if ! _ensure_shared_dir ${STREAK_DIR}; then
    error "cannot prepare ${STREAK_DIR}; counters disabled this run"
fi

Example 3: checking whether a job is running

[[ -d /var/run/omnicube/sync_pool.lock ]] && echo "sync_pool is running"

SECURITY

Lock creation is unprivileged; the sharing between root and RBAC operators is provided by group membership in ${LOCK_GROUP} plus mode 1775, and the sticky bit keeps users from releasing each other's locks. _ensure_shared_dir () refuses to chgrp or chmod a path that is a symbolic link, which prevents a symlink planted on tmpfs by an unprivileged first caller from being followed by a later root invocation.

SEE ALSO

omnicube_utils(3), oc_log(3), oc_runlevel(3), oc_validate(3), oc_ssh(3), oc_policy(3), isolate_node.sh(8), sys_monitor(8), sync_pool.sh(8), omnicube(7).

NOTES

A job that forgets the trap leaves its lock behind and locks itself out on the next tick, silently, because a pre-existing lock is an exit 0 condition. Nothing in the library detects an orphaned lock; sys_monitor(8) implements its own streak counters for that purpose.

acquire_lock_or_exit() protects against concurrent runs on one host only. It is not a cluster-wide lock. Serialization across nodes is done with ZFS user properties such as ${PROPPREFIX}:is_locked, not with this function.

man3/oc_lock.3generated 2026-09-02 05:17 CEST