Skip to content

Commit a2e5edb

Browse files
committed
syscalls: retry openat2 on -EAGAIN
In order to avoid in-kernel DoS due to unbounded retries, openat2(2) will return -EAGAIN when trying to walk through ".." if there is any racing rename or mount on the entire system. Note that this applies regardless of whether the rename was on the same filesytem or mount was in the same mount namespace as the process doing openat(2) -- as a result, calling openat2(2) on even a modestly busy system will result in spurious -EAGAIN errors every once in a while and it is necessary to implement a retry loop for it. (Libraries such as libpathrs and heavy users of openat2(2) like runc all do this, and in our testing we found that ~256 iterations is enough to provide resilience even on incredibly rename-heavy machines.) Fixes: 4fa7156 ("syscall: use openat2(RESOLVE_BENEATH) on Linux for secure_relative_open") Signed-off-by: Aleksa Sarai <aleksa@amutable.com>
1 parent f49a494 commit a2e5edb

1 file changed

Lines changed: 11 additions & 6 deletions

File tree

syscall.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1806,14 +1806,19 @@ int secure_relative_open(const char *basedir, const char *relpath, int flags, mo
18061806
}
18071807

18081808
#if defined(__linux__) && defined(HAVE_OPENAT2)
1809-
{
1809+
/* openat2(2) can fail with -EAGAIN if the path contains a ".." component
1810+
* and there was a rename or mount on the system, so on busy machines it is
1811+
* necessary to retry this a few times. Based on experiments in libpathrs,
1812+
* ~256 iterations is enough to ensure that ~50k openat2(2) runs on a very
1813+
* rename-heavy system never fail. */
1814+
for (int tries = 0; tries < 256; tries++) {
18101815
int fd = secure_relative_open_linux(basedir, relpath, flags, mode);
1811-
/* ENOSYS = kernel < 5.6 doesn't have the syscall even though
1812-
* glibc/kernel-headers do; fall through to the portable path.
1813-
* (Built unconditionally unless --disable-openat2, which forces
1814-
* the portable resolver below so that tier is exercised.) */
1815-
if (fd != -1 || errno != ENOSYS)
1816+
if (fd != -1)
18161817
return fd;
1818+
if (errno == ENOSYS)
1819+
break; // fallback to portable path
1820+
if (errno != EAGAIN)
1821+
return -1;
18171822
}
18181823
#elif defined(O_RESOLVE_BENEATH)
18191824
return secure_relative_open_resolve_beneath(basedir, relpath, flags, mode);

0 commit comments

Comments
 (0)