Skip to content

Conversation

@Ashod
Copy link
Contributor

@Ashod Ashod commented Oct 28, 2020

In certain cases changeLock to EXCLUSIVE fails
and throws LockedException. This leaves the
file locked as SHARED in file_put_contents,
which prevents retrying (because on second
call file_put_contents takes another SHARED
lock on the same file, and changeLock doesn't
allow more than a single SHARED lock to promote
to EXCLUSIVE).

To avoid this case, we catch the LockedException
and unlock before re-throwing.

Resolves #23740

@GretaD
Copy link
Contributor

GretaD commented Oct 29, 2020

Hello @Ashod, thank you for your PR, is this ready for review, you can use labels to show the status of this PR.

@Ashod
Copy link
Contributor Author

Ashod commented Oct 31, 2020

Hello @Ashod, thank you for your PR, is this ready for review, you can use labels to show the status of this PR.

Yes @GretaD, it is ready for review and does resolve the issue for me.

However, the maintainers might want to rework that function, such that there is a try/catch/finally block that does a single unlock in finally. I didn't do that, even though it's cleaner and catches other exceptions from other functions too, because I didn't want to refactor the function. My patch is the minimal change to fix the issue, not necessarily the most elegant.

Also, I suspect there are other similar bugs in the code. There are other places where the changeLock function is called without catching the exception. I didn't do an exhaustive search/investigation of those cases (probably not all need to unlock the shared lock on exception), but it's worth reviewing them, now that we know this pattern is problematic.

Let me know if I need to do anything further please.

Copy link
Member

@icewind1991 icewind1991 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense.

I'm not sure if using a finally here makes sense, since it's not always the same lock type that needs to be released.

@rullzer
Copy link
Member

rullzer commented Nov 2, 2020

@Ashod could you rebase your pr on latest master?

@rullzer
Copy link
Member

rullzer commented Nov 2, 2020

/backport to stable20

@rullzer
Copy link
Member

rullzer commented Nov 2, 2020

/backport to stable19

@rullzer
Copy link
Member

rullzer commented Nov 2, 2020

/backport to stable18

@MorrisJobke MorrisJobke added this to the Nextcloud 21 milestone Nov 3, 2020
In certain cases changeLock to EXCLUSIVE fails
and throws LockedException. This leaves the
file locked as SHARED in file_put_contents,
which prevents retrying (because on second
call file_put_contents takes another SHARED
lock on the same file, and changeLock doesn't
allow more than a single SHARED lock to promote
to EXCLUSIVE).

To avoid this case, we catch the LockedException
and unlock before re-throwing.

Signed-off-by: Ashod Nakashian <[email protected]>
@Ashod
Copy link
Contributor Author

Ashod commented Nov 7, 2020

I'm not sure if using a finally here makes sense, since it's not always the same lock type that needs to be released.

@icewind1991, that's a design decision, and it's fine (by me) not to use finally. But the $this->lockFile($path, ILockingProvider::LOCK_SHARED); call must be matched with $this->unlockFile($path, ILockingProvider::LOCK_SHARED); before returning (in virtually all cases). And the Exclusive lock must be matched with Exclusive unlock (in LIFO fashion, just like a stack).

It's true that between these two call there could be other locks, but those other locks need to be managed in their own right (like the Exclusive lock), such that at the point of returning from the function the state would be restored to what it was at the beginning. In other words, the lock state is always symmetric between the lock and unlock calls. And try/catch/finally is designed exactly for this use-case in mind. It can of course be nested, and any sub-section that has its own locks/logic would use finally to make sure it restores the state it inherited at the start of its block (so we could have two try/finally blocks in this function with my patch, the outer on for the Shared and the inner one for the Exclusive lock). In C++ this exact same pattern is implemented trivially by a class that takes the lock in its constructor and releases in its destructor, regardless of what happens in between.

Of course you could have functions that aren't symmetric, where you return with a different lock-state than you entered with. In those cases one shouldn't use finally to unlock (obviously), but those functions are quite rare (they are probably functions that implement the locking mechanisms itself) and not consumers of the lock.

At any rate, it's not up to me to decide. Just noting that there are many corner-cases that potentially leak the locks in the NC codebase. And not having a catch-all finally in those functions means these functions are forever prone to break after a trivial change that moves things around, or even when an unrelated function throws an exception that all the functions that lock don't catch and handle. It's better to standardize the management of locks everywhere with simple patterns that is easy to implement, maintain, and review. Leaking locks is a very serious problem, needless to say, so well worth the time investment.

@Ashod
Copy link
Contributor Author

Ashod commented Nov 7, 2020

@Ashod could you rebase your pr on latest master?

@rullzer, rebased. Thanks.

@mmeeks
Copy link

mmeeks commented Nov 9, 2020

FYI - we have to have a sleep(2) for our Nextcloud AppImage to try to avoid this race, and we're -really- eager to get rid of that ASAP =)

@MorrisJobke MorrisJobke merged commit 005a132 into nextcloud:master Nov 9, 2020
@welcome
Copy link

welcome bot commented Nov 9, 2020

Thanks for your first pull request and welcome to the community! Feel free to keep them coming! If you are looking for issues to tackle then have a look at this selection: https://github.com/nextcloud/server/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22

@MorrisJobke
Copy link
Member

/backport to stable19

@MorrisJobke
Copy link
Member

/backport to stable18

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

File lock leaks when changeLock fails

9 participants