Skip to contentSkip to conversation

fsck -l: fix racing between unlock/unlink and open

PATCH Yuriy M. Kaminskiy 3 participants 5 messages Apr 7, 2016 – Apr 22, 2016 Util-Linux package

lore
Share & tools
lore
#0 lore raw

Probably, there are better solutions, but I prefer KISS.

Quoted 1 line

From fcb97ddcbc336bc8860828c47cdaf21c7b1ca655 Mon Sep 17 00:00:00 2001

From: "Yuriy M. Kaminskiy" <[email protected]>
Date: Fri, 8 Apr 2016 00:38:56 +0300
Subject: [PATCH] fsck: fix racing between unlock/unlink and open

Process A Process B Process C
open()
[creates file]
lock()
[succeed]

		open()
		[open existing]
		lock()...
running()
close()
		[...succeed]
unlink()
		running()
				open()
				[creates file] {BAD!}
				lock()
				[succeed] {BAD!}
				running() {BAD!}
		close()

Cons: leaves empty (unlocked/harmless) .lock files in /run/fsck/

---
 disk-utils/fsck.c | 1 -
 1 file changed, 1 deletion(-)
disk-utils/fsck.c +0 -1
index 05cfbc4..84d2dcc 100644
@@ -410,7 +410,6 @@ static void unlock_disk(struct fsck_instance *inst)
 		printf(_("Unlocking %s.\n"), inst->lockpath);
 
 	close(inst->lock);			/* unlock */
-	unlink(inst->lockpath);
 
 	free(inst->lockpath);
 
Signature · 1 line
2.1.4
maintainer
#1 lore raw
Quoted 12 lines · Yuriy M. Kaminskiy

Probably, there are better solutions, but I prefer KISS.

From fcb97ddcbc336bc8860828c47cdaf21c7b1ca655 Mon Sep 17 00:00:00 2001
From: "Yuriy M. Kaminskiy" <[email protected]>
Date: Fri, 8 Apr 2016 00:38:56 +0300
Subject: [PATCH] fsck: fix racing between unlock/unlink and open

Process A Process B Process C
open()
[creates file]
lock()
[succeed]

Quoted 15 lines
		open()
		[open existing]
		lock()...
running()
close()
		[...succeed]
unlink()
		running()
				open()
				[creates file] {BAD!}
				lock()
				[succeed] {BAD!}
				running() {BAD!}
		close()

It would be better to check if the file still exist on the filesystem
with the same inode number, if not then re-open. It means that on race
you will create a new lock file. (You have to unlock after unlink.)
See http://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition

Simple example:

#include <stdio.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)

{
	const char *lockpath = "test.lock";
	int fd;
	struct stat st_lock, st_file;

	while (1) {
		fd = open(lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
				    S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
		fstat(fd, &st_lock);
		flock(fd, LOCK_EX);

		errno = 0;
		if (stat(lockpath, &st_file) == 0 && st_file.st_ino == st_lock.st_ino)
			break;
		if (errno)
			fprintf(stderr, "%d: stat failed: %m\n", getpid());
		else
			fprintf(stderr, "%d: ignore %lu (fs has:%lu)\n", getpid(), st_lock.st_ino, st_file.st_ino);
		close(fd);
	}

	fprintf(stderr, "%d: -->locked ino: %lu\n", getpid(), st_lock.st_ino);
	sleep(10);
	unlink(lockpath);
	close(fd);
	return 0;
}

See untested fsck patch below. Comments?

Karel

disk-utils/fsck.c +30 -15
index 05cfbc4..0a7dbc8 100644
@@ -370,23 +370,38 @@ static void lock_disk(struct fsck_instance *inst)
 	if (verbose)
 		printf(_("Locking disk by %s ... "), inst->lockpath);
 
-	inst->lock = open(inst->lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
-				    S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
-	if (inst->lock >= 0) {
-		int rc = -1;
-
-		/* inform users that we're waiting on the lock */
-		if (verbose &&
-		    (rc = flock(inst->lock, LOCK_EX | LOCK_NB)) != 0 &&
-		    errno == EWOULDBLOCK)
-			printf(_("(waiting) "));
-
-		if (rc != 0 && flock(inst->lock, LOCK_EX) != 0) {
-			close(inst->lock);			/* failed */
-			inst->lock = -1;
+	while (1) {
+		struct stat st_lock, st_file;
+
+		inst->lock = open(inst->lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
+					    S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
+		if (inst->lock >= 0) {
+			int rc = -1;
+
+			/* inform users that we're waiting on the lock */
+			if (verbose &&
+			    (rc = flock(inst->lock, LOCK_EX | LOCK_NB)) != 0 &&
+			    errno == EWOULDBLOCK)
+				printf(_("(waiting) "));
+
+			if (rc != 0 && flock(inst->lock, LOCK_EX) != 0) {
+				close(inst->lock);			/* failed */
+				inst->lock = -1;
+			}
 		}
+
+		/*
+		 * Make sure the locked lockfile really exist on the filesystem
+		 * to avoid race between lock and unlink (see unlock_disk())
+		 */
+		fstat(inst->lock, &st_lock);
+		if (stat(inst->lockpath, &st_file) == 0
+		    && st_file.st_ino == st_lock.st_ino)
+			break;				/* success */
+		close(inst->lock);
 	}
 
+
 	if (verbose)
 		/* TRANSLATORS: These are followups to "Locking disk...". */
 		printf("%s.\n", inst->lock >= 0 ? _("succeeded") : _("failed"));
@@ -409,8 +424,8 @@ static void unlock_disk(struct fsck_instance *inst)
 	if (verbose)
 		printf(_("Unlocking %s.\n"), inst->lockpath);
 
-	close(inst->lock);			/* unlock */
 	unlink(inst->lockpath);
+	close(inst->lock);			/* unlock */
 
 	free(inst->lockpath);
 
#2 lore raw
Quoted 45 lines · Karel Zak
Quoted 12 lines · Yuriy M. Kaminskiy

Probably, there are better solutions, but I prefer KISS.

From fcb97ddcbc336bc8860828c47cdaf21c7b1ca655 Mon Sep 17 00:00:00 2001
From: "Yuriy M. Kaminskiy" <[email protected]>
Date: Fri, 8 Apr 2016 00:38:56 +0300
Subject: [PATCH] fsck: fix racing between unlock/unlink and open

Process A Process B Process C
open()
[creates file]
lock()
[succeed]

Quoted 15 lines
		open()
		[open existing]
		lock()...
running()
close()
		[...succeed]
unlink()
		running()
				open()
				[creates file] {BAD!}
				lock()
				[succeed] {BAD!}
				running() {BAD!}
		close()

It would be better to check if the file still exist on the filesystem
with the same inode number, if not then re-open. It means that on race
you will create a new lock file. (You have to unlock after unlink.)
See http://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition

Simple example:

#include <stdio.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)

Quoted 14 lines
{
	const char *lockpath = "test.lock";
	int fd;
	struct stat st_lock, st_file;

	while (1) {
		fd = open(lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
				    S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
		fstat(fd, &st_lock);
		flock(fd, LOCK_EX);

		errno = 0;
		if (stat(lockpath, &st_file) == 0 && st_file.st_ino == st_lock.st_ino)
			break;

Requires stable inode numbers (well, probably acceptable assumption for {/var,}/run).

Quoted 15 lines
		if (errno)
			fprintf(stderr, "%d: stat failed: %m\n", getpid());
		else
			fprintf(stderr, "%d: ignore %lu (fs has:%lu)\n", getpid(), st_lock.st_ino, st_file.st_ino);
		close(fd);
	}

	fprintf(stderr, "%d: -->locked ino: %lu\n", getpid(), st_lock.st_ino);
	sleep(10);
	unlink(lockpath);
	close(fd);
	return 0;
}
Quoted 1 line

See untested fsck patch below. Comments?

I still prefer KISS. (And I bet it will eat more resources than just leaving those empty files around.)
(But either way, bug will be fixed, so I do as you like).

[...]

#3 lore raw
In reply to Karel Zak
Quoted 73 lines · Karel Zak
Quoted 12 lines · Yuriy M. Kaminskiy

Probably, there are better solutions, but I prefer KISS.

From fcb97ddcbc336bc8860828c47cdaf21c7b1ca655 Mon Sep 17 00:00:00 2001
From: "Yuriy M. Kaminskiy" <[email protected]>
Date: Fri, 8 Apr 2016 00:38:56 +0300
Subject: [PATCH] fsck: fix racing between unlock/unlink and open

Process A Process B Process C
open()
[creates file]
lock()
[succeed]

Quoted 15 lines
              open()
              [open existing]
              lock()...
running()
close()
              [...succeed]
unlink()
              running()
                              open()
                              [creates file] {BAD!}
                              lock()
                              [succeed] {BAD!}
                              running() {BAD!}
              close()

It would be better to check if the file still exist on the filesystem
with the same inode number, if not then re-open. It means that on race
you will create a new lock file. (You have to unlock after unlink.)
See http://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition

Simple example:

#include <stdio.h>
#include <sys/file.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)

{
        const char *lockpath = "test.lock";
        int fd;
        struct stat st_lock, st_file;

        while (1) {
                fd = open(lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
                                    S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
                fstat(fd, &st_lock);
                flock(fd, LOCK_EX);

                errno = 0;
                if (stat(lockpath, &st_file) == 0 && st_file.st_ino == st_lock.st_ino)
                        break;
                if (errno)
                        fprintf(stderr, "%d: stat failed: %m\n", getpid());
                else
                        fprintf(stderr, "%d: ignore %lu (fs has:%lu)\n", getpid(), st_lock.st_ino, st_file.st_ino);
                close(fd);
        }

        fprintf(stderr, "%d: -->locked ino: %lu\n", getpid(), st_lock.st_ino);
        sleep(10);
        unlink(lockpath);
        close(fd);
        return 0;
}
Quoted 1 line

See untested fsck patch below. Comments?

When multiple processes blocked on the same lock file, this ensure that all of them will fail to validate the lock when flock return, and will have to try again, but this is probably fine for this use case.

Quoted 18 lines

Karel

disk-utils/fsck.c +0 -6
index 05cfbc4..0a7dbc8 100644
@@ -370,23 +370,38 @@ static void lock_disk(struct fsck_instance *inst)
        if (verbose)
                printf(_("Locking disk by %s ... "), inst->lockpath);
 
-       inst->lock = open(inst->lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
-                                   S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
-       if (inst->lock >= 0) {
-               int rc = -1;
-
-               /* inform users that we're waiting on the lock */

disk-utils/fsck.c static void lock_disk(struct fsck_instance *inst) · line 374

Quoted 10 lines
disk-utils/fsck.c +2 -8
-               if (verbose &&
-                   (rc = flock(inst->lock, LOCK_EX | LOCK_NB)) != 0 &&
-                   errno == EWOULDBLOCK)
-                       printf(_("(waiting) "));
-
-               if (rc != 0 && flock(inst->lock, LOCK_EX) != 0) {
-                       close(inst->lock);                      /* failed */
-                       inst->lock = -1;
+       while (1) {
+               struct stat st_lock, st_file;

I would rename st_file to st_lockpath.

disk-utils/fsck.c static void lock_disk(struct fsck_instance *inst) · line 378

Quoted 4 lines
disk-utils/fsck.c +4 -0
+
+               inst->lock = open(inst->lockpath, O_RDONLY|O_CREAT|O_CLOEXEC,
+                                           S_IWUSR|S_IRUSR|S_IRGRP|S_IROTH);
+               if (inst->lock >= 0) {

We can replace this with

if (inst->lock == -1)
break; /* failed */

Simplifying the normal flow, and eliminating the issue of validating a lock after open failed.

Quoted 9 lines
disk-utils/fsck.c +9 -0
+                       int rc = -1;
+
+                       /* inform users that we're waiting on the lock */
+                       if (verbose &&
+                           (rc = flock(inst->lock, LOCK_EX | LOCK_NB)) != 0 &&
+                           errno == EWOULDBLOCK)
+                               printf(_("(waiting) "));
+
+                       if (rc != 0 && flock(inst->lock, LOCK_EX) != 0) {

disk-utils/fsck.c static void lock_disk(struct fsck_instance *inst) · line 397

Quoted 10 lines
disk-utils/fsck.c +9 -0
+                               close(inst->lock);                      /* failed */
+                               inst->lock = -1;
+                       }
                }
+
+               /*
+                * Make sure the locked lockfile really exist on the filesystem
+                * to avoid race between lock and unlink (see unlock_disk())
+                */
+               fstat(inst->lock, &st_lock);

This is going to fail when inst->lock == -1. Then the contents of st_lock are random junk from the stack.

disk-utils/fsck.c static void lock_disk(struct fsck_instance *inst) · line 399

Quoted 2 lines
disk-utils/fsck.c +2 -0
+               if (stat(inst->lockpath, &st_file) == 0
+                   && st_file.st_ino == st_lock.st_ino)

This check is relevant only if inst->lock >= 0

disk-utils/fsck.c static void unlock_disk(struct fsck_instance *inst) · line 412

Quoted 13 lines
disk-utils/fsck.c +3 -1
+                       break;                          /* success */
+               close(inst->lock);
        }
 
+
        if (verbose)
                /* TRANSLATORS: These are followups to "Locking disk...". */
                printf("%s.\n", inst->lock >= 0 ? _("succeeded") : _("failed"));
@@ -409,8 +424,8 @@ static void unlock_disk(struct fsck_instance *inst)
        if (verbose)
                printf(_("Unlocking %s.\n"), inst->lockpath);
 
-       close(inst->lock);                      /* unlock */

Here we should add a comment about the order and link to lock_disk() depending on this order.

disk-utils/fsck.c static void unlock_disk(struct fsck_instance *inst) · line 430

Quoted 4 lines
disk-utils/fsck.c +1 -0
        unlink(inst->lockpath);
+       close(inst->lock);                      /* unlock */
 
        free(inst->lockpath);

Nir