Skip to content

Comments

Create lock files 644, only check read bit for locks, respect umask#17646

Open
dcwatson wants to merge 4 commits intoastral-sh:mainfrom
dcwatson:16769-lockfile-perms
Open

Create lock files 644, only check read bit for locks, respect umask#17646
dcwatson wants to merge 4 commits intoastral-sh:mainfrom
dcwatson:16769-lockfile-perms

Conversation

@dcwatson
Copy link
Contributor

Summary

Resolves #16769 by creating the temp lock files 644, and not requiring write access when trying to acquire a lock on a file. Additionally, I removed the forcing of permissions to override umask -- if a umask is set more restrictively it was probably for a reason, and now that failing to acquire a lock is just a warning, it doesn't seem worth it to override.

Test Plan

Manually, either via docker or CLI, as it requires separate users to both run a uv command that takes a lock on the same project.

I tested with a locally built uv. I ran an initial sync using root, and verified the lock file was as expected:

dcwatson@alektra T % ls -l uv*
.rw-r--r-- 0 root staff 2026-01-21 14:48 uv-ff189de60ce1a681.lock

Then running a sync via my own user:

DEBUG Acquired exclusive lock for `/Users/dcwatson/testapp`
DEBUG Reading Python requests from version file at `/Users/dcwatson/testapp/.python-version`
DEBUG Using Python request `3.14` from version file at `.python-version`
DEBUG Checking for Python environment at: `.venv`
DEBUG The project environment's Python version satisfies the request: `Python 3.14`
DEBUG Released lock at `/var/folders/qs/c9v6ntlx05x4mt88cgs9p4sc0000gn/T/uv-ff189de60ce1a681.lock`

Finally, I modified the lock file so it was owner-read-only (chmod 400), and verified a warning (but not an error) was logged when I ran a sync as my own user:

WARN Failed to acquire project environment lock: failed to open file `/var/folders/qs/c9v6ntlx05x4mt88cgs9p4sc0000gn/T/uv-ff189de60ce1a681.lock`: Permission denied (os error 13)

@zanieb zanieb self-assigned this Jan 21, 2026
@zanieb
Copy link
Member

zanieb commented Jan 21, 2026

cc @EliteTK since this code last changed in #17115

@zanieb zanieb added the test:extended Enable extended tests for a pull request label Jan 21, 2026
@dcwatson
Copy link
Contributor Author

FWIW, I just verified the manual test from #17115 still works:

$ hdiutil create -size 1g -fs ExFAT -volname EXFATDISK exfat.dmg
$ hdiutil attach exfat.dmg
$ cd /Volumes/EXFATDISK
$ uv init --bare --cache-dir build/uv/cache -v

Copy link
Contributor

@EliteTK EliteTK left a comment

Choose a reason for hiding this comment

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

Thanks.

Overall I don't see anything wrong with the idea. Changing the mode to 0o444 will be a good test to ensure that we can still take exclusive locks on MacOS.

I am also curious to see what error message we get if umask is 777, but I have a feeling that we'll never get this far in that case, although you could use fault injection with strace to test it.

// Otherwise, create a temporary file with 666 permissions. We must set
// permissions _after_ creating the file, to override the `umask`.
// Otherwise, create a temporary file with 644 perms in the same directory as path.
let perms = Permissions::from_mode(0o644);
Copy link
Contributor

Choose a reason for hiding this comment

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

If we don't need to write, this should just be 0o444.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't have strong feelings either way. 644 is a pretty typical "new file" permission, but you're right we don't strictly need the owner write bit. It does make deleting them annoying in zsh since it will prompt you 😅

Copy link
Member

Choose a reason for hiding this comment

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

(I presume that could break cache cleans?)

Copy link
Contributor

Choose a reason for hiding this comment

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

Regarding cache cleans, the behaviour described is just the behaviour from rm implementations. It's a courtesy to tell you that you're removing a write protected file. There's nothing stopping you from deleting it as deleting it is an operation on the directory not the file so only the directory permissions matter. As such, I imagine that cache cleans (unless they use rm -r or something) shouldn't be affected.

Regarding 0o444 vs 0o644. I don't think we have a test which specifically checks the case this code was initially written to address (please correct me if I'm wrong, I couldn't find anything). That is, originally the lock files were made with 0o600 permissions which caused locking issues if some other user had touched the cache first and written the lockfile. We support this presumably because of containers with delegated UIDs and also running as root.

So, if we have it at 0o644 we might end up regressing there.

I think people are already used to rm complaining when deleting .git directories so I am not sure it's that big of a deal here. There might be an argument here that this is a breaking change... not sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure a detail like this would warrant it, but we could add a UV_LOCK_MODE environment variable (a la UV_LOCK_TIMEOUT).

Copy link
Contributor

Choose a reason for hiding this comment

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

@dcwatson if you don't mind, just push a commit for setting the mode to 0o444. I am curious to see the test suite results. We can always just drop that commit quite easily once we make a decision on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure, done.

Comment on lines 282 to 285
.read(true)
.write(true)
.create(true)
.open(path.as_ref())?;
Copy link
Contributor

@EliteTK EliteTK Jan 21, 2026

Choose a reason for hiding this comment

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

This should also have had .write(true) dropped. But instead, it should actually just use .mode(0o444).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initially dropped the .write(true) but tests failed on Windows. I added it back to both implementations (this and the #[cfg(not(unix))] one) since I'm not very familiar with Rust's filesystem calls, but you're probably right that it can be dropped from this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I can understand why the windows variant didn't change since on windows we're not using flock. But keeping it in this branch of the unix version is definitely inconsistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to set DESIRED_MODE. What that mode should be seems like a bit of an open question, but for my purposes anything without world write/execute is good.

@EliteTK
Copy link
Contributor

EliteTK commented Jan 22, 2026

So test:extended doesn't seem run cargo test on macos... I'll fix that, but I'll just run those manually.

Pending that, it seems like this change maybe won't regress #11324 for uv, but will regress it for using pip with uv's cache, which I had no idea we supported...

Edit: Good news is, the tests pass on mac.

@dcwatson
Copy link
Contributor Author

dcwatson commented Jan 22, 2026

I'm a little confused as to why the /home/runner/.cache/uv/.lock file is the problem in those failing tests - all the uv invocations (aside from the initial venv) in that script pass --cache-dir=temp_dir.

Edit: I just noticed the uv pip show command (https://github.com/astral-sh/uv/blob/main/scripts/check_cache_compat.py#L43) does not specify --cache-dir, maybe that's part of the problem

It does seem like pip creates .lock files as 0o600 (https://github.com/pypa/pip/blob/794c144edc1a6fdf1090d1fa633acd9bde822639/src/pip/_vendor/cachecontrol/caches/file_cache.py#L22), which uv may have trouble with if running as a separate user, but probably not much to do about that?

I also ran these locally on my mac and couldn't reproduce, unless I manually set the lock to something like 0o400 owned by root.

@EliteTK
Copy link
Contributor

EliteTK commented Jan 22, 2026

The tests are failing when we're asking pip to use a cache directory that uv has created the lockfile in.

And the reason is probably this: https://github.com/tox-dev/filelock/blob/680b51c4b027b43bbf2d9bae2de45aebeebbdd03/src/filelock/_unix.py#L41

@dcwatson
Copy link
Contributor Author

Does uv pip actually call out to pip? I saw but discounted the open flags in filelock because check_cache_compat just uses uv pip. But if that is indeed the problem, going with 644 or even 664 seems like a reasonable compromise.

@EliteTK
Copy link
Contributor

EliteTK commented Jan 22, 2026

Oh hold on, I didn't notice the uv in front of "pip" until now. You're right, we're not using pip with uv's cache...

That makes this much simpler to understand then.

Those tests are testing current uv vs previous uv. In the current uv we are only opening for reading, and only setting the read bits. In the previous version we were opening for reading and writing, and setting both bits.

So, if we stick to 644 (or just 666 and let the umask handle dropping the group/other write bits) we would actually be breaking backwards compatibility with older uv versions in the case described in #11324.

And, if we use 444, then we're just flat out breaking older uv versions accessing caches created by a newer uv.

@EliteTK
Copy link
Contributor

EliteTK commented Jan 22, 2026

The good news is, the upcoming 0.10.0 release will incorporate breaking changes. On the other hand, this may be a bit too breaking, not sure.

I'll sleep on it.

@dcwatson
Copy link
Contributor Author

If we went with 644, the only breaking change would be when using an old version with a new lock file, and doing so as separate users. I can definitely see putting it off until 0.10.0, but it still seems like an edge case with an easy fix (update uv). Our current situation is we can't use uv because of "security" scans complaining about world-writable files, and we can't even set a umask as a workaround. Is it a silly thing? Probably. But it's also a bit of a smell that these files are world-writable when they don't have to be (imo).

Anyway, I really appreciate you taking the time to consider this! I'm pushing because I love the project and want to use it at work 😄

@zanieb
Copy link
Member

zanieb commented Jan 22, 2026

The easiest path forward is to ship this behind a preview feature flag, would it unblock your adoption if you could change the behavior by setting an environment variable or configuration file option?

@dcwatson
Copy link
Contributor Author

Yes, we could definitely work with that. In that case, is it worth considering a UV_LOCKFILE_MODE environment variable, and/or a tool.uv.lockfile-mode config that defaults to 0o666 to match current behavior?

@EliteTK
Copy link
Contributor

EliteTK commented Jan 23, 2026

Yes, we could definitely work with that. In that case, is it worth considering a UV_LOCKFILE_MODE environment variable, and/or a tool.uv.lockfile-mode config that defaults to 0o666 to match current behavior?

For the current behaviour (without this PR applied), it only makes sense for write and read bits to be set or unset in tandem. The lockfile is never written to or read from, but we won't be able to open it for locking without both bits set1. Being able to configure the lockfile mode to 752 or 725 would be equivalent in that it would effectively work the same as 600. Moreover, it only makes sense for the lockfile's read and write permissions to be as permissive as a cache entry's read permissions. So, to avoid confusion, we'd probably want a cache-perms setting or something which lets you specify the permissions at the group, user and other level, rather than specific mode bits.

But, instead, we could already do something like this most likely without breaking anything:

let read_mode = 0o444 & !umask;
// Mirror the read permission into the write permissions
let write_mode = read_mode >> 1;
let desired_mode = read_mode | write_mode;

The reason why this probably won't break anything is that if the umask restricts reading for group or other, then cache entries written by a uv running with such a umask would already not work for other users. So creating a lockfile with less restrictive permissions would only delay the inevitable permission errors down the road (although I'd like to test this a bit further to make sure there isn't actually a new failure case).

With the above approach, we wouldn't need any new configuration, we could just use the umask to inform the lockfile mode. Furthermore, once the behaviour of using 0o444 perms over 0o666 perms got stabilised, this behaviour would continue to work (without needing to ignore the umask for the write permission bits any more).

Unfortunately for your case, it would still set 0o666 permissions for a 0o022 umask. But, as I understand it, adding and enabling the new behaviour preview feature should get you the behaviour you want. e.g.:

  • umask of 0o022 would create cache entries with 0o644 and a lockfile with 0o444
  • umask of 0o027 would create cache entries with 0o640 and a lockfile with 0o440

Do you think this kind of approach would be sufficient for your purposes? I completely open to the possibility that I missed something in my analysis, so let me know if you think there's still another justification for a separate lockfile mode configurable or maybe even the cache permission setting described above.

Regardless, the change I describe here should probably be a separate PR, and it should include adding some targetted tests to the test suite.

On the other hand, we would be happy to accept this PR with the changes to the lockfile creation mode behind a preview flag and unconditional removal of the .write(true) on the lockfile opening. I'd even suggest opening a new PR to just drop .write(true) so we can merge that sooner, as that's kind of the whole source of all this unfortunate compatibility pain.

Footnotes

  1. Going forwards, if we drop .write(true) on open, this would no longer be true. But compatibility with older versions of uv would still be affected. So we would want to continue to keep the write and read bits in synch when no preview feature is enabled to avoid having an option which could in effect cause the same breakage as the preview option.

@dcwatson
Copy link
Contributor Author

I think what you're saying makes sense in regard to lock files for caches not needing to be more permissive than the cache files themselves. However, LockedFile is used in more contexts than just caching, so it would feel weird tying lock permissions generally to cache-perms specifically.

I'm also not convinced we want to create lock files as 0o444 necessarily. Internally to uv it would likely be fine, as we would open the file for reading if it exists and lock on it. But it would prevent overwriting (or opening in append mode) even by the owner, which would affect compatibility with pip/filelock (if that is actually desired). Maybe it's just a gut feeling that it could be a footgun, I'm not sure.

My preference would be to drop the .write(true) and gate the mode behind a preview flag or config or whatever - I just don't know how to do that 😁 I'm also less inclined to think try_set_permissions needs to go if there's a mode setting.

@EliteTK
Copy link
Contributor

EliteTK commented Jan 23, 2026

I think what you're saying makes sense in regard to lock files for caches not needing to be more permissive than the cache files themselves. However, LockedFile is used in more contexts than just caching, so it would feel weird tying lock permissions generally to cache-perms specifically.

Good point. But I think this just reinforces that maybe we shouldn't have a UV_LOCKFILE_MODE or similar and instead should just aim towards a situation where we can rely solely on umask instead.

But it would prevent overwriting (or opening in append mode) even by the owner, which would affect compatibility with pip/filelock (if that is actually desired). Maybe it's just a gut feeling that it could be a footgun, I'm not sure.

Sorry, you can ignore the thing about pip and filelock, I got very confused because I didn't see that we were running uv pip. Our cache format isn't compatible with pip, there's no concern with regard to 0o444 there.

The only concern is that users trying to manually rm -r the cache or rm a lock file would now get the warning from rm...

On the one hand, .git directories are already famous for this. On the other hand, maybe we could just request 0o666 or 0o664 with the umask applied to avoid causing any grief in the 90% of cases.

My preference would be to drop the .write(true) and gate the mode behind a preview flag or config or whatever - I just don't know how to do that 😁

Regarding how to add a preview flag, this is a recent PR which may help: #17464

But, it looks somewhat involved to get the flag to the right place... Feel free to discuss it with us.

One last option is also that we take over and build on top of your PR.

I'm also less inclined to think try_set_permissions needs to go if there's a mode setting.

try_set_permissions must stay for now in non-preview, to avoid breakage. But for preview and beyond, it should be dropped.

I feel like any mode setting we introduce should probably also respect umask.

@dcwatson
Copy link
Contributor Author

On the other hand, maybe we could just request 0o666 or 0o664 with the umask applied to avoid causing any grief in the 90% of cases.

This sounds ideal to me!

But, it looks somewhat involved to get the flag to the right place... Feel free to discuss it with us.

At first glance, it seems like it would need to be passed in to every acquire call? I didn't see a clean way to get settings into uv-cache (and maybe elsewhere) without a lot of churn. During that same glance, I also realized I'm out of my depth 😁

One last option is also that we take over and build on top of your PR.

That would be amazing if it's on the table. I'm not opposed to giving it a go with some guidance, but it will almost certainly be faster for you all to take it over. Happy to stub it out if it would be helpful, but it feels like the actual changes to LockedFile::create will be pretty minimal in comparison to whatever else needs to be done.

I feel like any mode setting we introduce should probably also respect umask.

Funny, I feel the other way - that if I'm going to tell uv specifically what mode the lockfiles should be, I'd expect them to be that mode. But I do agree that not having a specific mode setting and letting umask do its thing is better.

@seanb1974

This comment was marked as off-topic.

EliteTK added a commit that referenced this pull request Feb 10, 2026
## Summary

When opening lock files, don't open them for writing to avoid needing
the write permission in the future.

Related: #17646

## Test Plan

Test suite ran.

Co-authored-by: Dan Watson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:extended Enable extended tests for a pull request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Setting To No Longer Force Lock Files to have 777 Permissions in Linux

4 participants