From 7e5df6a5adb4b9c4531568a5113020daad3af5b0 Mon Sep 17 00:00:00 2001
From: Levente Polyak <anthraxx@archlinux.org>
Date: Tue, 26 Dec 2023 18:20:46 +0100
Subject: [PATCH] fix(db-functions): fix locking issue that overwrote the same
 fd

Calling `acquire_fd` in a subshell doesn't populate the global lockfd
associate array properly. This resulted in the reuse of the same fd
multiple times, effectively only holding a single latest lock and
releasing any previous.

Fix the issue by avoiding a global variable that causes issues in
subshell calls by using the file descriptor table directly. Instead
of storing file descriptors, the lookup call iterates through all fds
and checks their handle. In case a file is not yet opened, allocate the
next free fd between 4 and 1023.

Operating directly on the file descriptor table has the nice side effect
that we avoid reusing descriptors by accident in case they have been
opened for none locking purpose within the statically defined range.

Reported-by: Felix Yan <felixonmars@archlinux.org>
Co-authored-by: Felix Yan <felixonmars@archlinux.org>
Signed-off-by: Levente Polyak <anthraxx@archlinux.org>
---
 db-functions | 61 ++++++++++++++++++++++++++++++----------------------
 1 file changed, 35 insertions(+), 26 deletions(-)

diff --git a/db-functions b/db-functions
index 432ecce..c4cadd4 100644
--- a/db-functions
+++ b/db-functions
@@ -79,35 +79,36 @@ stat_done() {
 	printf "${BOLD}done${ALL_OFF}\n" >&2
 }
 
-declare -A lockfd=()
-readonly lockfd_start=20
 if [[ -z "${LOCK_DIR}" ]]; then
 	die "No configuration provided where to store locks in LOCK_DIR"
 fi
 
 acquire_fd() {
 	local handle="${1}"
-	local last key next fd
+	local fd fd_handle
 
-	# check if already acquired
-	if [[ "${lockfd["${handle}"]+exists}" ]]; then
-		echo "${lockfd["${handle}"]}"
-		return
-	fi
+	# store the resolved path
+	handle=$(realpath -- "${handle}")
 
-	# determine last used fd
-	last=${lockfd_start}
-	for key in "${!lockfd[@]}"; do
-		fd="${lockfd[${key}]}"
-		if (( fd > last )); then
-			last=${fd}
+	# try to find open fd for handle
+	for fd in /dev/fd/*; do
+		fd_handle=$(realpath -- "${fd}")
+		if [[ ${handle} -ef ${fd_handle} ]]; then
+			fd=$(basename -- "${fd}")
+			printf "%s" "${fd}"
+			return 0
 		fi
 	done
 
-	# assign next fd
-	next=$(( last + 1 ))
-	lockfd["${handle}"]=${next}
-	echo "${next}"
+	# return first unused fd
+	for fd in $(seq 4 1023); do
+		if [[ ! -f /dev/fd/${fd} ]]; then
+			printf "%s" "${fd}"
+			return 0
+		fi
+	done
+
+	return 1
 }
 
 ##
@@ -116,24 +117,32 @@ acquire_fd() {
 acquire_lock() {
 	local lock_mode=$1
 	local handle=$2
-	local fd
-	local ret
+	local fd message
 
 	# acquire fd from handle
-	fd=$(acquire_fd "${handle}")
+	if ! fd=$(acquire_fd "${handle}"); then
+		error "Failed to acquire free fd for locking"
+		return 1
+	fi
+
+	# assign busy message
+	message=("${@:3}")
+	if (( ! ${#message[@]} )); then
+		message=("Locking %s" "${handle}")
+	fi
 
 	# Only reopen the FD if it wasn't handed to us
 	if ! [[ "/dev/fd/${fd}" -ef "${handle}" ]]; then
 		mkdir -p -- "$(dirname -- "${handle}")"
-		eval "exec ${fd}>"'"${handle}"'
+		eval "exec ${fd}>>"'"${handle}"'
 	fi
 
+	# Acquire lock via flock on the fd
 	if ! flock "${lock_mode}" --nonblock "${fd}"; then
-		stat_busy "${@:3}"
+		stat_busy "${message[@]}"
 		if ! flock "${lock_mode}" "${fd}"; then
-			ret=$?
-			error "failed to lock %s: %s" "${handle}" "${ret}"
-			return ${ret}
+			error "Failed to acquire lock on %s" "${handle}"
+			return 1
 		fi
 		stat_done
 	fi
-- 
GitLab