summaryrefslogtreecommitdiffstats
path: root/libmount/src/lock.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:14:44 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-27 13:14:44 +0000
commit30ff6afe596eddafacf22b1a5b2d1a3d6254ea15 (patch)
tree9b788335f92174baf7ee18f03ca8330b8c19ce2b /libmount/src/lock.c
parentInitial commit. (diff)
downloadutil-linux-30ff6afe596eddafacf22b1a5b2d1a3d6254ea15.tar.xz
util-linux-30ff6afe596eddafacf22b1a5b2d1a3d6254ea15.zip
Adding upstream version 2.36.1.upstream/2.36.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'libmount/src/lock.c')
-rw-r--r--libmount/src/lock.c732
1 files changed, 732 insertions, 0 deletions
diff --git a/libmount/src/lock.c b/libmount/src/lock.c
new file mode 100644
index 0000000..cc5340d
--- /dev/null
+++ b/libmount/src/lock.c
@@ -0,0 +1,732 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * This file is part of libmount from util-linux project.
+ *
+ * Copyright (C) 2009-2018 Karel Zak <kzak@redhat.com>
+ *
+ * libmount is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ */
+
+/**
+ * SECTION: lock
+ * @title: Locking
+ * @short_description: locking methods for /etc/mtab or another libmount files
+ *
+ * The mtab lock is backwards compatible with the standard linux /etc/mtab
+ * locking. Note, it's necessary to use the same locking schema in all
+ * applications that access the file.
+ */
+#include <sys/time.h>
+#include <time.h>
+#include <signal.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sys/file.h>
+
+#include "strutils.h"
+#include "closestream.h"
+#include "pathnames.h"
+#include "mountP.h"
+#include "monotonic.h"
+
+/*
+ * lock handler
+ */
+struct libmnt_lock {
+ char *lockfile; /* path to lock file (e.g. /etc/mtab~) */
+ char *linkfile; /* path to link file (e.g. /etc/mtab~.<id>) */
+ int lockfile_fd; /* lock file descriptor */
+
+ unsigned int locked :1, /* do we own the lock? */
+ sigblock :1, /* block signals when locked */
+ simplelock :1; /* use flock rather than normal mtab lock */
+
+ sigset_t oldsigmask;
+};
+
+
+/**
+ * mnt_new_lock:
+ * @datafile: the file that should be covered by the lock
+ * @id: unique linkfile identifier or 0 (default is getpid())
+ *
+ * Returns: newly allocated lock handler or NULL on case of error.
+ */
+struct libmnt_lock *mnt_new_lock(const char *datafile, pid_t id)
+{
+ struct libmnt_lock *ml = NULL;
+ char *lo = NULL, *ln = NULL;
+ size_t losz;
+
+ if (!datafile)
+ return NULL;
+
+ /* for flock we use "foo.lock, for mtab "foo~"
+ */
+ losz = strlen(datafile) + sizeof(".lock");
+ lo = malloc(losz);
+ if (!lo)
+ goto err;
+
+ /* default is mtab~ lock */
+ snprintf(lo, losz, "%s~", datafile);
+
+ if (asprintf(&ln, "%s~.%d", datafile, id ? : getpid()) == -1) {
+ ln = NULL;
+ goto err;
+ }
+ ml = calloc(1, sizeof(*ml) );
+ if (!ml)
+ goto err;
+
+ ml->lockfile_fd = -1;
+ ml->linkfile = ln;
+ ml->lockfile = lo;
+
+ DBG(LOCKS, ul_debugobj(ml, "alloc: default linkfile=%s, lockfile=%s", ln, lo));
+ return ml;
+err:
+ free(lo);
+ free(ln);
+ free(ml);
+ return NULL;
+}
+
+
+/**
+ * mnt_free_lock:
+ * @ml: struct libmnt_lock handler
+ *
+ * Deallocates mnt_lock.
+ */
+void mnt_free_lock(struct libmnt_lock *ml)
+{
+ if (!ml)
+ return;
+ DBG(LOCKS, ul_debugobj(ml, "free%s", ml->locked ? " !!! LOCKED !!!" : ""));
+ free(ml->lockfile);
+ free(ml->linkfile);
+ free(ml);
+}
+
+/**
+ * mnt_lock_block_signals:
+ * @ml: struct libmnt_lock handler
+ * @enable: TRUE/FALSE
+ *
+ * Block/unblock signals when the lock is locked, the signals are not blocked
+ * by default.
+ *
+ * Returns: <0 on error, 0 on success.
+ */
+int mnt_lock_block_signals(struct libmnt_lock *ml, int enable)
+{
+ if (!ml)
+ return -EINVAL;
+ DBG(LOCKS, ul_debugobj(ml, "signals: %s", enable ? "BLOCKED" : "UNBLOCKED"));
+ ml->sigblock = enable ? 1 : 0;
+ return 0;
+}
+
+/* don't export this to API
+ */
+int mnt_lock_use_simplelock(struct libmnt_lock *ml, int enable)
+{
+ size_t sz;
+
+ if (!ml)
+ return -EINVAL;
+
+ assert(ml->lockfile);
+
+ DBG(LOCKS, ul_debugobj(ml, "flock: %s", enable ? "ENABLED" : "DISABLED"));
+ ml->simplelock = enable ? 1 : 0;
+
+ sz = strlen(ml->lockfile);
+ assert(sz);
+
+ if (sz < 1)
+ return -EINVAL;
+
+ /* Change lock name:
+ *
+ * flock: "<name>.lock"
+ * mtab lock: "<name>~"
+ */
+ if (ml->simplelock && endswith(ml->lockfile, "~"))
+ memcpy(ml->lockfile + sz - 1, ".lock", 6);
+
+ else if (!ml->simplelock && endswith(ml->lockfile, ".lock"))
+ memcpy(ml->lockfile + sz - 5, "~", 2);
+
+ DBG(LOCKS, ul_debugobj(ml, "new lock filename: '%s'", ml->lockfile));
+ return 0;
+}
+
+/*
+ * Returns path to lockfile.
+ */
+static const char *mnt_lock_get_lockfile(struct libmnt_lock *ml)
+{
+ return ml ? ml->lockfile : NULL;
+}
+
+/*
+ * Note that the filename is generated by mnt_new_lock() and depends on
+ * getpid() or 'id' argument of the mnt_new_lock() function.
+ *
+ * Returns: unique (per process/thread) path to linkfile.
+ */
+static const char *mnt_lock_get_linkfile(struct libmnt_lock *ml)
+{
+ return ml ? ml->linkfile : NULL;
+}
+
+/*
+ * Simple flocking
+ */
+static void unlock_simplelock(struct libmnt_lock *ml)
+{
+ assert(ml);
+ assert(ml->simplelock);
+
+ if (ml->lockfile_fd >= 0) {
+ DBG(LOCKS, ul_debugobj(ml, "%s: unflocking",
+ mnt_lock_get_lockfile(ml)));
+ close(ml->lockfile_fd);
+ }
+}
+
+static int lock_simplelock(struct libmnt_lock *ml)
+{
+ const char *lfile;
+ int rc;
+ struct stat sb;
+ const mode_t lock_mask = S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH;
+
+ assert(ml);
+ assert(ml->simplelock);
+
+ lfile = mnt_lock_get_lockfile(ml);
+
+ DBG(LOCKS, ul_debugobj(ml, "%s: locking", lfile));
+
+ if (ml->sigblock) {
+ sigset_t sigs;
+ sigemptyset(&ml->oldsigmask);
+ sigfillset(&sigs);
+ sigprocmask(SIG_BLOCK, &sigs, &ml->oldsigmask);
+ }
+
+ ml->lockfile_fd = open(lfile, O_RDONLY|O_CREAT|O_CLOEXEC,
+ S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
+ if (ml->lockfile_fd < 0) {
+ rc = -errno;
+ goto err;
+ }
+
+ rc = fstat(ml->lockfile_fd, &sb);
+ if (rc < 0) {
+ rc = -errno;
+ goto err;
+ }
+
+ if ((sb.st_mode & lock_mask) != lock_mask) {
+ rc = fchmod(ml->lockfile_fd, lock_mask);
+ if (rc < 0) {
+ rc = -errno;
+ goto err;
+ }
+ }
+
+ while (flock(ml->lockfile_fd, LOCK_EX) < 0) {
+ int errsv;
+ if ((errno == EAGAIN) || (errno == EINTR))
+ continue;
+ errsv = errno;
+ close(ml->lockfile_fd);
+ ml->lockfile_fd = -1;
+ rc = -errsv;
+ goto err;
+ }
+ ml->locked = 1;
+ return 0;
+err:
+ if (ml->sigblock)
+ sigprocmask(SIG_SETMASK, &ml->oldsigmask, NULL);
+ return rc;
+}
+
+/*
+ * traditional mtab locking
+ */
+
+static void mnt_lockalrm_handler(int sig __attribute__((__unused__)))
+{
+ /* do nothing, say nothing, be nothing */
+}
+
+/*
+ * Waits for F_SETLKW, unfortunately we have to use SIGALRM here to interrupt
+ * fcntl() to avoid neverending waiting.
+ *
+ * Returns: 0 on success, 1 on timeout, -errno on error.
+ */
+static int mnt_wait_mtab_lock(struct libmnt_lock *ml, struct flock *fl, time_t maxtime)
+{
+ struct timeval now;
+ struct sigaction sa, osa;
+ int ret = 0;
+
+ gettime_monotonic(&now);
+ DBG(LOCKS, ul_debugobj(ml, "(%d) waiting for F_SETLKW (now=%lu, maxtime=%lu, diff=%lu)",
+ getpid(),
+ (unsigned long) now.tv_sec,
+ (unsigned long) maxtime,
+ (unsigned long) (maxtime - now.tv_sec)));
+
+ if (now.tv_sec >= maxtime)
+ return 1; /* timeout */
+
+ /* setup ALARM handler -- we don't want to wait forever */
+ sa.sa_flags = 0;
+ sa.sa_handler = mnt_lockalrm_handler;
+ sigfillset (&sa.sa_mask);
+
+ sigaction(SIGALRM, &sa, &osa);
+
+
+ alarm(maxtime - now.tv_sec);
+ if (fcntl(ml->lockfile_fd, F_SETLKW, fl) == -1)
+ ret = errno == EINTR ? 1 : -errno;
+ alarm(0);
+
+ /* restore old sigaction */
+ sigaction(SIGALRM, &osa, NULL);
+
+ DBG(LOCKS, ul_debugobj(ml, "(%d) leaving mnt_wait_setlkw(), rc=%d",
+ getpid(), ret));
+ return ret;
+}
+
+/*
+ * Create the mtab lock file.
+ *
+ * The old code here used flock on a lock file /etc/mtab~ and deleted
+ * this lock file afterwards. However, as rgooch remarks, that has a
+ * race: a second mount may be waiting on the lock and proceed as
+ * soon as the lock file is deleted by the first mount, and immediately
+ * afterwards a third mount comes, creates a new /etc/mtab~, applies
+ * flock to that, and also proceeds, so that the second and third mount
+ * are now both scribbling in /etc/mtab.
+ *
+ * The new code uses a link() instead of a creat(), where we proceed
+ * only if it was us that created the lock, and hence we always have
+ * to delete the lock afterwards. Now the use of flock() is in principle
+ * superfluous, but avoids an arbitrary sleep().
+ *
+ * Where does the link point to? Obvious choices are mtab and mtab~~.
+ * HJLu points out that the latter leads to races. Right now we use
+ * mtab~.pid instead.
+ *
+ *
+ * The original mount locking code has used sleep(1) between attempts and
+ * maximal number of attempts has been 5.
+ *
+ * There was a very small number of attempts and extremely long waiting (1s)
+ * that is useless on machines with large number of mount processes.
+ *
+ * Now we wait for a few thousand microseconds between attempts and we have a global
+ * time limit (30s) rather than a limit for the number of attempts. The advantage
+ * is that this method also counts time which we spend in fcntl(F_SETLKW) and
+ * the number of attempts is not restricted.
+ * -- kzak@redhat.com [Mar-2007]
+ *
+ *
+ * This mtab locking code has been refactored and moved to libmount. The mtab
+ * locking is really not perfect (e.g. SIGALRM), but it's stable, reliable and
+ * backwardly compatible code.
+ *
+ * Don't forget that this code has to be compatible with 3rd party mounts
+ * (/sbin/mount.foo) and has to work with NFS.
+ * -- kzak@redhat.com [May-2009]
+ */
+
+/* maximum seconds between the first and the last attempt */
+#define MOUNTLOCK_MAXTIME 30
+
+/* sleep time (in microseconds, max=999999) between attempts */
+#define MOUNTLOCK_WAITTIME 5000
+
+static void unlock_mtab(struct libmnt_lock *ml)
+{
+ if (!ml)
+ return;
+
+ if (!ml->locked && ml->lockfile && ml->linkfile)
+ {
+ /* We (probably) have all the files, but we don't own the lock,
+ * Really? Check it! Maybe ml->locked wasn't set properly
+ * because the code was interrupted by a signal. Paranoia? Yes.
+ *
+ * We own the lock when linkfile == lockfile.
+ */
+ struct stat lo, li;
+
+ if (!stat(ml->lockfile, &lo) && !stat(ml->linkfile, &li) &&
+ lo.st_dev == li.st_dev && lo.st_ino == li.st_ino)
+ ml->locked = 1;
+ }
+
+ if (ml->linkfile)
+ unlink(ml->linkfile);
+ if (ml->lockfile_fd >= 0)
+ close(ml->lockfile_fd);
+ if (ml->locked && ml->lockfile) {
+ unlink(ml->lockfile);
+ DBG(LOCKS, ul_debugobj(ml, "unlink %s", ml->lockfile));
+ }
+}
+
+static int lock_mtab(struct libmnt_lock *ml)
+{
+ int i, rc = -1;
+ struct timespec waittime;
+ struct timeval maxtime;
+ const char *lockfile, *linkfile;
+
+ if (!ml)
+ return -EINVAL;
+ if (ml->locked)
+ return 0;
+
+ lockfile = mnt_lock_get_lockfile(ml);
+ if (!lockfile)
+ return -EINVAL;
+ linkfile = mnt_lock_get_linkfile(ml);
+ if (!linkfile)
+ return -EINVAL;
+
+ if (ml->sigblock) {
+ /*
+ * Block all signals when locked, mnt_unlock_file() will
+ * restore the old mask.
+ */
+ sigset_t sigs;
+
+ sigemptyset(&ml->oldsigmask);
+ sigfillset(&sigs);
+ sigdelset(&sigs, SIGTRAP);
+ sigdelset(&sigs, SIGALRM);
+ sigprocmask(SIG_BLOCK, &sigs, &ml->oldsigmask);
+ }
+
+ i = open(linkfile, O_WRONLY|O_CREAT|O_CLOEXEC, S_IRUSR|S_IWUSR);
+ if (i < 0) {
+ /* linkfile does not exist (as a file) and we cannot create it.
+ * Read-only or full filesystem? Too many files open in the system?
+ */
+ if (errno > 0)
+ rc = -errno;
+ goto failed;
+ }
+ close(i);
+
+ gettime_monotonic(&maxtime);
+ maxtime.tv_sec += MOUNTLOCK_MAXTIME;
+
+ waittime.tv_sec = 0;
+ waittime.tv_nsec = (1000 * MOUNTLOCK_WAITTIME);
+
+ /* Repeat until it was us who made the link */
+ while (!ml->locked) {
+ struct timeval now;
+ struct flock flock;
+ int j;
+
+ j = link(linkfile, lockfile);
+ if (j == 0)
+ ml->locked = 1;
+
+ if (j < 0 && errno != EEXIST) {
+ if (errno > 0)
+ rc = -errno;
+ goto failed;
+ }
+ ml->lockfile_fd = open(lockfile, O_WRONLY|O_CLOEXEC);
+
+ if (ml->lockfile_fd < 0) {
+ /* Strange... Maybe the file was just deleted? */
+ int errsv = errno;
+ gettime_monotonic(&now);
+ if (errsv == ENOENT && now.tv_sec < maxtime.tv_sec) {
+ ml->locked = 0;
+ continue;
+ }
+ if (errsv > 0)
+ rc = -errsv;
+ goto failed;
+ }
+
+ flock.l_type = F_WRLCK;
+ flock.l_whence = SEEK_SET;
+ flock.l_start = 0;
+ flock.l_len = 0;
+
+ if (ml->locked) {
+ /* We made the link. Now claim the lock. */
+ if (fcntl (ml->lockfile_fd, F_SETLK, &flock) == -1) {
+ DBG(LOCKS, ul_debugobj(ml,
+ "%s: can't F_SETLK lockfile, errno=%d\n",
+ lockfile, errno));
+ /* proceed, since it was us who created the lockfile anyway */
+ }
+ break;
+ }
+
+ /* Someone else made the link. Wait. */
+ int err = mnt_wait_mtab_lock(ml, &flock, maxtime.tv_sec);
+
+ if (err == 1) {
+ DBG(LOCKS, ul_debugobj(ml,
+ "%s: can't create link: time out (perhaps "
+ "there is a stale lock file?)", lockfile));
+ rc = -ETIMEDOUT;
+ goto failed;
+
+ } else if (err < 0) {
+ rc = err;
+ goto failed;
+ }
+ nanosleep(&waittime, NULL);
+ close(ml->lockfile_fd);
+ ml->lockfile_fd = -1;
+ }
+ DBG(LOCKS, ul_debugobj(ml, "%s: (%d) successfully locked",
+ lockfile, getpid()));
+ unlink(linkfile);
+ return 0;
+
+failed:
+ mnt_unlock_file(ml);
+ return rc;
+}
+
+
+/**
+ * mnt_lock_file
+ * @ml: pointer to struct libmnt_lock instance
+ *
+ * Creates a lock file (e.g. /etc/mtab~). Note that this function may
+ * use alarm().
+ *
+ * Your application always has to call mnt_unlock_file() before exit.
+ *
+ * Traditional mtab locking scheme:
+ *
+ * 1. create linkfile (e.g. /etc/mtab~.$PID)
+ * 2. link linkfile --> lockfile (e.g. /etc/mtab~.$PID --> /etc/mtab~)
+ * 3. a) link() success: setups F_SETLK lock (see fcntl(2))
+ * b) link() failed: wait (max 30s) on F_SETLKW lock, goto 2.
+ *
+ * Note that when the lock is used by mnt_update_table() interface then libmount
+ * uses flock() for private library file /run/mount/utab. The fcntl(2) is used only
+ * for backwardly compatible stuff like /etc/mtab.
+ *
+ * Returns: 0 on success or negative number in case of error (-ETIMEOUT is case
+ * of stale lock file).
+ */
+int mnt_lock_file(struct libmnt_lock *ml)
+{
+ if (!ml)
+ return -EINVAL;
+
+ if (ml->simplelock)
+ return lock_simplelock(ml);
+
+ return lock_mtab(ml);
+}
+
+/**
+ * mnt_unlock_file:
+ * @ml: lock struct
+ *
+ * Unlocks the file. The function could be called independently of the
+ * lock status (for example from exit(3)).
+ */
+void mnt_unlock_file(struct libmnt_lock *ml)
+{
+ if (!ml)
+ return;
+
+ DBG(LOCKS, ul_debugobj(ml, "(%d) %s", getpid(),
+ ml->locked ? "unlocking" : "cleaning"));
+
+ if (ml->simplelock)
+ unlock_simplelock(ml);
+ else
+ unlock_mtab(ml);
+
+ ml->locked = 0;
+ ml->lockfile_fd = -1;
+
+ if (ml->sigblock) {
+ DBG(LOCKS, ul_debugobj(ml, "restoring sigmask"));
+ sigprocmask(SIG_SETMASK, &ml->oldsigmask, NULL);
+ }
+}
+
+#ifdef TEST_PROGRAM
+
+static struct libmnt_lock *lock;
+
+/*
+ * read number from @filename, increment the number and
+ * write the number back to the file
+ */
+static void increment_data(const char *filename, int verbose, int loopno)
+{
+ long num;
+ FILE *f;
+ char buf[256];
+
+ if (!(f = fopen(filename, "r" UL_CLOEXECSTR)))
+ err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename);
+
+ if (!fgets(buf, sizeof(buf), f))
+ err(EXIT_FAILURE, "%d failed read: %s", getpid(), filename);
+
+ fclose(f);
+ num = atol(buf) + 1;
+
+ if (!(f = fopen(filename, "w" UL_CLOEXECSTR)))
+ err(EXIT_FAILURE, "%d: failed to open: %s", getpid(), filename);
+
+ fprintf(f, "%ld", num);
+
+ if (close_stream(f) != 0)
+ err(EXIT_FAILURE, "write failed: %s", filename);
+
+ if (verbose)
+ fprintf(stderr, "%d: %s: %ld --> %ld (loop=%d)\n", getpid(),
+ filename, num - 1, num, loopno);
+}
+
+static void clean_lock(void)
+{
+ if (!lock)
+ return;
+ mnt_unlock_file(lock);
+ mnt_free_lock(lock);
+}
+
+static void __attribute__((__noreturn__)) sig_handler(int sig)
+{
+ errx(EXIT_FAILURE, "\n%d: catch signal: %s\n", getpid(), strsignal(sig));
+}
+
+static int test_lock(struct libmnt_test *ts, int argc, char *argv[])
+{
+ time_t synctime = 0;
+ unsigned int usecs;
+ const char *datafile = NULL;
+ int verbose = 0, loops = 0, l, idx = 1;
+
+ if (argc < 3)
+ return -EINVAL;
+
+ if (strcmp(argv[idx], "--synctime") == 0) {
+ synctime = (time_t) atol(argv[idx + 1]);
+ idx += 2;
+ }
+ if (idx < argc && strcmp(argv[idx], "--verbose") == 0) {
+ verbose = 1;
+ idx++;
+ }
+
+ if (idx < argc)
+ datafile = argv[idx++];
+ if (idx < argc)
+ loops = atoi(argv[idx++]);
+
+ if (!datafile || !loops)
+ return -EINVAL;
+
+ if (verbose)
+ fprintf(stderr, "%d: start: synctime=%u, datafile=%s, loops=%d\n",
+ getpid(), (int) synctime, datafile, loops);
+
+ atexit(clean_lock);
+
+ /* be paranoid and call exit() (=clean_lock()) for all signals */
+ {
+ int sig = 0;
+ struct sigaction sa;
+
+ sa.sa_handler = sig_handler;
+ sa.sa_flags = 0;
+ sigfillset(&sa.sa_mask);
+
+ while (sigismember(&sa.sa_mask, ++sig) != -1 && sig != SIGCHLD)
+ sigaction (sig, &sa, (struct sigaction *) 0);
+ }
+
+ /* start the test in exactly defined time */
+ if (synctime) {
+ struct timeval tv;
+
+ gettimeofday(&tv, NULL);
+ if (synctime && synctime - tv.tv_sec > 1) {
+ usecs = ((synctime - tv.tv_sec) * 1000000UL) -
+ (1000000UL - tv.tv_usec);
+ xusleep(usecs);
+ }
+ }
+
+ for (l = 0; l < loops; l++) {
+ lock = mnt_new_lock(datafile, 0);
+ if (!lock)
+ return -1;
+
+ if (mnt_lock_file(lock) != 0) {
+ fprintf(stderr, "%d: failed to lock %s file\n",
+ getpid(), datafile);
+ return -1;
+ }
+
+ increment_data(datafile, verbose, l);
+
+ mnt_unlock_file(lock);
+ mnt_free_lock(lock);
+ lock = NULL;
+
+ /* The mount command usually finishes after a mtab update. We
+ * simulate this via short sleep -- it's also enough to make
+ * concurrent processes happy.
+ */
+ if (synctime)
+ xusleep(25000);
+ }
+
+ return 0;
+}
+
+/*
+ * Note that this test should be executed from a script that creates many
+ * parallel processes, otherwise this test does not make sense.
+ */
+int main(int argc, char *argv[])
+{
+ struct libmnt_test tss[] = {
+ { "--lock", test_lock, " [--synctime <time_t>] [--verbose] <datafile> <loops> "
+ "increment a number in datafile" },
+ { NULL }
+ };
+
+ return mnt_run_test(tss, argc, argv);
+}
+
+#endif /* TEST_PROGRAM */