-
Notifications
You must be signed in to change notification settings - Fork 0
Backport Lix CVE fixes to Nix 2.3 #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: 2.3-maintenance
Are you sure you want to change the base?
Conversation
This is useful for certain error recovery paths (no pun intended) that does not thread through the original path name. Change-Id: I2d800740cb4f9912e64c923120d3f977c58ccb7e Signed-off-by: Raito Bezarius <raito@lix.systems>
We now keep around a proper AutoCloseFD around the temporary directory which we plan to use for openat operations and avoiding the build directory being swapped out while we are doing something else. Change-Id: I18d387b0f123ebf2d20c6405cd47ebadc5505f2a Signed-off-by: Raito Bezarius <raito@lix.systems>
We use it immediately for the build temporary directory. Change-Id: I180193c63a2b98721f5fb8e542c4e39c099bb947 Signed-off-by: Raito Bezarius <raito@lix.systems>
- call close explicitly in writeFile to prevent the close exception from being ignored - fsync after writing schema file to flush data to disk - fsync schema file parent to flush metadata to disk NixOS#7064
`writeFile` lose its `sync` boolean flag to make things simpler. A new `writeFileAndSync` function is created and all call sites are converted to it. Change-Id: Ib871a5283a9c047db1e4fe48a241506e4aab9192 Signed-off-by: Raito Bezarius <raito@lix.systems>
This ensures that `passAsFile` data is created inside the expected temporary build directory by `openat()` from the parent directory file descriptor. Fixes CVE-2025-52993. Change-Id: Ie5273446c4a19403088d0389ae8e3f473af8879a Signed-off-by: Raito Bezarius <raito@lix.systems>
Suppose I have a path /nix/store/[hash]-[name]/a/a/a/a/a/[...]/a, long enough that everything after "/nix/store/" is longer than 4096 (MAX_PATH) bytes. Nix will happily allow such a path to be inserted into the store, because it doesn't look at all the nested structure. It just cares about the /nix/store/[hash]-[name] part. But, when the path is deleted, we encounter a problem. Nix will move the path to /nix/store/trash, but then when it's trying to recursively delete the trash directory, it will at some point try to unlink /nix/store/trash/[hash]-[name]/a/a/a/a/a/[...]/a. This will fail, because the path is too long. After this has failed, any store deletion operation will never work again, because Nix needs to delete the trash directory before recreating it to move new things to it. (I assume this is because otherwise a path being deleted could already exist in the trash, and then moving it would fail.) This means that if I can trick somebody into just fetching a tarball containing a path of the right length, they won't be able to delete store paths or garbage collect ever again, until the offending path is manually removed from /nix/store/trash. (And even fixing this manually is quite difficult if you don't understand the issue, because the absolute path that Nix says it failed to remove is also too long for rm(1).) This patch fixes the issue by making Nix's recursive delete operation use unlinkat(2). This function takes a relative path and a directory file descriptor. We ensure that the relative path is always just the name of the directory entry, and therefore its length will never exceed 255 bytes. This means that it will never even come close to AX_PATH, and Nix will therefore be able to handle removing arbitrarily deep directory hierachies. Since the directory file descriptor is used for recursion after being used in readDirectory, I made a variant of readDirectory that takes an already open directory stream, to avoid the directory being opened multiple times. As we have seen from this issue, the less we have to interact with paths, the better, and so it's good to reuse file descriptors where possible. I left _deletePath as succeeding even if the parent directory doesn't exist, even though that feels wrong to me, because without that early return, the linux-sandbox test failed. Reported-by: Alyssa Ross <hi@alyssa.is> Thanks-to: Puck Meerburg <puck@puckipedia.com> Tested-by: Puck Meerburg <puck@puckipedia.com> Reviewed-by: Puck Meerburg <puck@puckipedia.com>
…tors Also remove an erroneous comment.
…irfds When calling `_deletePath` with a parent file descriptor, `openat` is made effective by using relative paths to the directory file descriptor. To avoid the problem, the signature is changed to resist misuse with an assert in the prologue of the function. Fixes CVE-2025-46415. Change-Id: I6b3fc766bad2afe54dc27d47d1df3873e188de96 Signed-off-by: Raito Bezarius <raito@lix.systems>
sternenseemann
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just found the one mistake when backporting the Lix patches. I've also added some confusions I've had, but I guess they are to some extent also directed at Raito.
Also looks like this would break the build on darwin which isn't super urgent for depot at least.
| return readLink(fmt("/proc/self/fd/%1%", fd).c_str()); | ||
| } catch (...) { | ||
| } | ||
| #elif defined (HAVE_F_GETPATH) && HAVE_F_GETPATH |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This CPP symbol is set by meson in Lix, so I think this is effectively dead code here unless we add it to the autoconf machinery.
| void writeFile(AutoCloseFD & fd, const std::string& s, mode_t mode = 0666); | ||
|
|
||
| /* Write a string to a file and flush the file and its parent directory to disk. */ | ||
| void writeFileAndSync(const Path & path, const std::string& s, mode_t mode = 0666); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you know the significance of writeFileUninterruptible? Is that something we also need?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Apparently related to cgroups in Lix, unnecessary.
| void writeFile(AutoCloseFD & fd, const std::string& s, mode_t mode) | ||
| { | ||
| assert(fd); | ||
| writeFull(fd.get(), s); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why isn't the fd closed in here, but in the wrapping function? Is the calling function entitled to keep using the fd? A previous specifcally started closing the fd here to “propagate exceptions”, so I'm confused why it's being reverted here…
| throw SysError("opening file '%1%'", path); | ||
|
|
||
| writeFile(fd, s, mode); | ||
| fd.fsync(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't closeForWrite be used here?
| for (auto & i : readDirectory(path)) | ||
| _deletePath(path + "/" + i.name, bytesFreed); | ||
| int fd = openat(parentfd, name.c_str(), O_RDONLY | O_DIRECTORY | O_NOFOLLOW); | ||
| if (fd = -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (fd = -1) | |
| if (fd == -1) |
|
|
||
|
|
||
| static void _deletePath(int parentfd, const Path & path, unsigned long long & bytesFreed) | ||
| static void _deletePath(int parentfd, const Path& name, unsigned long long & bytesFreed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we continuing to use Path here?
| for (auto & i : readDirectory(dir.get(), path)) | ||
| _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed); | ||
| throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd)); | ||
| for (auto & i : readDirectory(dir.get(), name)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also here we don't have a flag for interruptibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Apparently related to cgroups in Lix, unnecessary.)
| _deletePath(dirfd(dir.get()), path + "/" + i.name, bytesFreed); | ||
| throw SysError("opening directory '%1%' in directory '%2%'", name, guessOrInventPathFromFD(parentfd)); | ||
| for (auto & i : readDirectory(dir.get(), name)) | ||
| _deletePath(dirfd(dir.get()), i.name, bytesFreed); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And here…
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Apparently related to cgroups in Lix, unnecessary.)
|
Seems like backporting the following commits would also be a good idea, though I'm not sure whether this is entangled with pasta somehow?
The following commits aren't backported, but related to pasta (and thus future work):
|
Seems like this needs some extra care: https://lix.systems/blog/2025-06-27-lix-critical-bug/ |
This backports the most important CVE fixes, but not all of them.
Included are fixes for:
Not included are fixes for:
We might backport these separately, but I don't think we'll go all the way to the
pastastuff in this fork. We'll see.