Subject: [PATCH] [FEAT VS1822] libutil: introduce util_lockfile
From: Matthew Rosato <mjrosato@linux.ibm.com>

Summary:     ap_tools: add ap-check and the ap device type to zdev   
Description: This feature adds multiple components in support of persistent
             configuration of vfio-ap mediated devices.

             The ap-check utility is added as a callout function for the
             mdevctl utility.  This allows for meaningful error messages to be
             presented to end-users when vfio-ap configuration errors are
             detected while using mdevctl to create/modify vfio-ap mediated
             devices.

             Additionally, the 'ap' device type is added to zdev, providing a
             command-line interface for managing the apmask and aqmask, which
             determine what AP resources are available for vfio-ap usage.
             'chzdev' is updated to allow for modifying the active masks as
             well as to specify persistent mask values via a udev rule.
             'lszdev' is updated to allow for querying of the active and
             persistent mask values.
Upstream-ID: e1aec24e843689524b470081fd46e67593161edd
Problem-ID:  VS1822

Upstream-Description:

             libutil: introduce util_lockfile

             Implement simple file-locking routines that use process PIDs for stale
             lock detection.  The implementation is meant to be a simplified subset of
             what liblockfile was previously being used for by libap, allowing the
             external dependency to be removed.

             This initial implementation provides a series of functions that allow for
             creating/release file locks using either the current process PID or the
             PID of the current process parent.  When creating a file lock, first a
             temporary file is created and the appropriate PID (either this process
             PID or the parent process PID) is placed in the file to specify the owner
             of the lock.  Then an attempt is made to link that file to the desired
             file location; if this succeeds, the lock is now held on behalf of the
             specified PID.  If it fails, this implies the file already exists
             (meaning the lock is already held).  In this case, stale lock detection
             is performed by reading the PID from the file and ensuring that the
             associated process still exists -- if it does not, then the lock is
             presumed stale and destroyed.  If the process still exists, then either
             the lock request fails or the caller will sleep and retry, depending on
             an optional retry setting.

             A lock remains valid until either 1) it is released via the corresponding
             util_lockfile function, which will delete the corresponding file 2) the
             associated PID no longer exists, which leaves the file in-place but will
             cause it to be destroyed the next time a different process attempts to
             lock that file or 3) the file is directly removed (e.g. rm).

             A typical usecase for such support would be to provide a means for
             multiple invocations of the same (or different) tools to ensure that they
             do not access the same shared resource simultaneously.  For example,
             ap-check, chzdev and lszdev all have a need to view and/or modify the AP
             and vfio-ap configuration files; util_lockfile can be used to ensure that
             only one instance of any of these utilities do that at a time by ensuring
             they all use the same lockfile.
             Additionally, providing the ability to specify the parent PID rather than
             the current PID allows for a general purpose tool (like mdevctl) to
             invoke a sub-program (ap-check) to acquire and release a lockfile as
             necssary while allowing stale lock detection to be controlled by that
             parent PID, allowing the lock to remain held over multiple sub-program
             invocations.

             Note that this implementation is sufficient for our current usage (e.g.
             lockfiles placed in tmpfs) but does not take into consideration things
             like NFS, which a more complete lockfile solution like liblockfile does.

             GitHub-ID: https://github.com/ibm-s390-linux/s390-tools/issues/142
             Suggested-by: Luca BRUNO <luca.bruno@coreos.com>
             Signed-off-by: Matthew Rosato <mjrosato@linux.ibm.com>
             Reviewed-by: Steffen Eiden <seiden@linux.ibm.com>
             Signed-off-by: Jan Hoeppner <hoeppner@linux.ibm.com>


Signed-off-by: Matthew Rosato <mjrosato@linux.ibm.com>
---
 include/lib/util_lockfile.h |   26 +++
 libutil/util_lockfile.c     |  300 ++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 326 insertions(+)

--- /dev/null
+++ b/include/lib/util_lockfile.h
@@ -0,0 +1,26 @@
+/**
+ * @defgroup util_lockfile_h util_lockfile: File locking utility
+ * @{
+ * @brief Create file-based locks
+ *
+ * Copyright IBM Corp. 2022
+ *
+ * s390-tools is free software; you can redistribute it and/or modify
+ * it under the terms of the MIT license. See LICENSE for details.
+ */
+#ifndef LIB_UTIL_LOCKFILE_H
+#define LIB_UTIL_LOCKFILE_H
+
+#define UTIL_LOCKFILE_OK	   0 /* Lock acquired/released successfully */
+#define UTIL_LOCKFILE_LOCK_FAIL	   1 /* Lock already held, ran out of retries */
+#define UTIL_LOCKFILE_RELEASE_NONE 2 /* Lock not held */
+#define UTIL_LOCKFILE_RELEASE_FAIL 3 /* Lock not held by specified pid */
+#define UTIL_LOCKFILE_ERR	   4 /* Other, unexpected error conditions */
+
+int util_lockfile_lock(char *lockfile, int retries);
+int util_lockfile_parent_lock(char *lockfile, int retries);
+
+int util_lockfile_release(char *lockfile);
+int util_lockfile_parent_release(char *lockfile);
+
+#endif /** LIB_UTIL_LOCKFILE_H @} */
--- /dev/null
+++ b/libutil/util_lockfile.c
@@ -0,0 +1,300 @@
+/*
+ * util - Utility function library
+ *
+ * Created file-based locks
+ *
+ * Copyright IBM Corp. 2022
+ *
+ * s390-tools is free software; you can redistribute it and/or modify
+ * it under the terms of the MIT license. See LICENSE for details.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "lib/util_libc.h"
+#include "lib/util_lockfile.h"
+#include "lib/util_panic.h"
+
+#define WAITPID 120 /* Time to wait for pid to be written */
+#define WAITINC 5   /* Additional time to wait each retry */
+#define MAXWAIT 60  /* Maximum wait between retries */
+#define BUFSIZE 40  /* Buffer must be large enough to fit pid string */
+
+/**
+ * Check if there is a process that exists with the specified identifier.
+ *
+ * @param[in]      pid        Process Identifier to check
+ *
+ * @retval         true       Process exists or we lack privelege to check
+ * @retval         false      Process does not exist
+ */
+static bool pid_exists(int pid)
+{
+	int rc;
+
+	/* Use sig 0 to determine if the owner is still alive */
+	rc = kill(pid, 0);
+	if (rc != 0) {
+		switch (errno) {
+		case EPERM:
+			/* Privilege issue, just assume PID exists */
+			break;
+		case ESRCH:
+			/* PID does not exist, this lock is stale */
+			return false;
+		default:
+			util_assert(false, "Unexpected return from kill: %d\n", rc);
+			break;
+		}
+	}
+
+	return true;
+}
+
+/**
+ * Check for an existing lock that is deemed stale (either the associated PID
+ * is gone or no PID has been written to the file in a reasonable timeframe).
+ * In the case a stale lock is found, remove it.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ *
+ * @retval         0          Either no lock or stale lock was found and removed
+ * @retval         1          Lock is held by another PID and is not stale
+ */
+static int handle_stale_lock(char *lockfile)
+{
+	int fd, rc, pid, len;
+	struct stat info;
+	char buf[BUFSIZE];
+	time_t curr;
+
+	fd = open(lockfile, O_RDONLY);
+
+	if (fd >= 0) {
+		/* Lock exists, see who owns it */
+		len = read(fd, buf, sizeof(buf));
+		if (len > 0) {
+			buf[len] = 0; /* Ensure null terminated string */
+			pid = atoi(buf);
+			if (!pid_exists(pid)) {
+				/* Stale lock detected unlink and retry now */
+				close(fd);
+				unlink(lockfile);
+				return 0;
+			} else if (pid != 0) {
+				/* Lock is held by an active pid, delay */
+				close(fd);
+				return 1;
+			}
+			/*
+			 * If we reach this point, the PID was 0 which is
+			 * unexpected.  Proceed under the assumption that the
+			 * proper PID hasn't been written yet.
+			 */
+		}
+		/*
+		 * PID hasn't been written yet?  Either a bad lock or a very
+		 * new one.
+		 */
+		time(&curr);
+		rc = fstat(fd, &info);
+		close(fd);
+		if (rc != 0) {
+			/* Can't read file anymore, retry now */
+			return 0;
+		}
+		if (curr > info.st_mtime + WAITPID) {
+			/*
+			 * PID should be in the file within 2 minutes,
+			 * something went wrong.  Treat as stale.
+			 */
+			unlink(lockfile);
+			return 0;
+		}
+		/* Otherwise, assume file was newly created and delay */
+		return 1;
+	}
+
+	/* Couldn't open, try again immediately */
+	return 0;
+}
+
+/**
+ * Attempt to create a lockfile at the specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ * @param[in]      retries    Number of times to retry if lock fails initially
+ * @param[in]      pid        PID to use for lock ownership
+ *
+ * @retval         0          Lock created with PID as owner
+ * @retval         !=0        Lock was not created
+ */
+static int do_lockfile_lock(char *lockfile, unsigned int retries, int pid)
+{
+	int fd, plen, len, rc = 0, snooze = 0;
+	unsigned int tries = retries + 1;
+	char buf[BUFSIZE];
+	char *tpath;
+
+	if (!lockfile)
+		return UTIL_LOCKFILE_ERR;
+
+	plen = snprintf(buf, sizeof(buf), "%d\n", pid);
+	if (plen < 0 || (plen > ((int)sizeof(buf) - 1)))
+		return UTIL_LOCKFILE_ERR;
+
+	/* Allocate temporary lock file with a sufficiently unique path */
+	len = util_asprintf(&tpath, "%s%05d%02x", lockfile, getpid(),
+			    (unsigned int)time(NULL) & 255);
+	if (len < 0)
+		return UTIL_LOCKFILE_ERR;
+
+	/* Open the temporary lockfile, write the specified pid */
+	fd = open(tpath, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
+	if (fd < 0) {
+		rc = UTIL_LOCKFILE_ERR;
+		goto out;
+	}
+
+	len = write(fd, buf, plen);
+	if (close(fd) != 0) {
+		rc = UTIL_LOCKFILE_ERR;
+		goto cleanup;
+	}
+	if (len != plen) {
+		/* Failed to write the temp lockfile, bail out */
+		rc = UTIL_LOCKFILE_ERR;
+		goto cleanup;
+	}
+
+	/* Link the temprorary file to the real path */
+	do {
+		rc = link(tpath, lockfile);
+		if (rc == 0) {
+			/* Lock successfully acquired */
+			rc = UTIL_LOCKFILE_OK;
+			goto cleanup;
+		}
+		/* Lock already held - check for stale lock */
+		rc = handle_stale_lock(lockfile);
+		/* Only wait if the lock was not stale */
+		if (rc != 0) {
+			tries--;
+			if (tries > 0) {
+				snooze += WAITINC;
+				snooze = (snooze > MAXWAIT) ? MAXWAIT : snooze;
+				sleep(snooze);
+			}
+		}
+	} while (tries > 0);
+
+	/* Exhausted specified number of retries, exit on failure */
+	rc = UTIL_LOCKFILE_LOCK_FAIL;
+
+cleanup:
+	unlink(tpath);
+	free(tpath);
+out:
+	return rc;
+}
+
+/**
+ * Attempt to release a lockfile at the specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ * @param[in]      pid        PID that should own the lock
+ *
+ * @retval         0          Lock released
+ * @retval         !=0        Lock was not released or did not exist
+ */
+static int do_lockfile_release(char *lockfile, int pid)
+{
+	int fd, len, lpid;
+	char buf[BUFSIZE];
+
+	if (!lockfile)
+		return UTIL_LOCKFILE_ERR;
+
+	/* Open lockfile, read the owning pid if it exists */
+	fd = open(lockfile, O_RDONLY);
+	if (fd < 0)
+		return UTIL_LOCKFILE_RELEASE_NONE;
+	len = read(fd, buf, sizeof(buf));
+	close(fd);
+	if (len <= 0)
+		return UTIL_LOCKFILE_RELEASE_FAIL;
+	buf[len] = 0;
+	lpid = atoi(buf);
+
+	/* Only release the lock if its held by the right pid */
+	if (pid != lpid)
+		return UTIL_LOCKFILE_RELEASE_FAIL;
+
+	unlink(lockfile);
+
+	return 0;
+}
+
+/**
+ * Attempt to create a lockfile owned by this process at the specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ * @param[in]      retries    Number of times to retry if lock fails initially
+ *
+ * @retval         0          Lock created
+ * @retval         !=0        Lock was not created
+ */
+int util_lockfile_lock(char *lockfile, int retries)
+{
+	return do_lockfile_lock(lockfile, retries, getpid());
+}
+
+/**
+ * Attempt to create a lockfile owned by the parent of this process at the
+ * specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ * @param[in]      retries    Number of times to retry if lock fails initially
+ *
+ * @retval         0          Lock created
+ * @retval         !=0        Lock was not created
+ */
+int util_lockfile_parent_lock(char *lockfile, int retries)
+{
+	return do_lockfile_lock(lockfile, retries, getppid());
+}
+
+/**
+ * Attempt to release a lockfile owned by this process at the specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ *
+ * @retval         0          Lock released
+ * @retval         !=0        Lock was not released or did not exist
+ */
+int util_lockfile_release(char *lockfile)
+{
+	return do_lockfile_release(lockfile, getpid());
+}
+
+/**
+ * Attempt to release a lockfile owned by the parent of this process at the
+ * specified path.
+ *
+ * @param[in]      lockfile   Path to the lock file
+ *
+ * @retval         0          Lock released
+ * @retval         !=0        Lock was not released or did not exist
+ */
+int util_lockfile_parent_release(char *lockfile)
+{
+	return do_lockfile_release(lockfile, getppid());
+}
