Skip to content

Commit 294a6e3

Browse files
committed
feat(utils): implement safe openat2 fallback
Walk path components via O_PATH|O_NOFOLLOW, detect symlinks with fstat, and block escape above root. Drops exception-based control flow from syscall_openat2 in favor of std::optional + error_code. Signed-off-by: Yuming He <ComixHe1895@outlook.com>
1 parent 0e1051d commit 294a6e3

2 files changed

Lines changed: 177 additions & 40 deletions

File tree

src/linyaps_box/utils/file.cpp

Lines changed: 172 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
#include "linyaps_box/utils/inspect.h"
77
#include "linyaps_box/utils/log.h"
8+
#include "linyaps_box/utils/symlink.h"
89

910
#include <sys/syscall.h>
1011

1112
#include <cassert>
1213
#include <fstream>
14+
#include <optional>
1315

1416
#ifdef LINYAPS_BOX_HAVE_OPENAT2_H
1517
# include <linux/openat2.h>
@@ -26,6 +28,98 @@
2628
#endif
2729

2830
namespace {
31+
32+
// see: https://man7.org/linux/man-pages/man7/path_resolution.7.html
33+
constexpr auto MAX_SYMLINK_DEPTH = 40;
34+
35+
auto stat_same_inode(const struct stat &a, const struct stat &b) noexcept -> bool
36+
{
37+
return a.st_dev == b.st_dev && a.st_ino == b.st_ino;
38+
}
39+
40+
auto check_not_above_root(const linyaps_box::utils::file_descriptor &current,
41+
const linyaps_box::utils::file_descriptor &root) -> void
42+
{
43+
auto cur_stat = linyaps_box::utils::fstat(current);
44+
auto root_stat = linyaps_box::utils::fstat(root);
45+
if (stat_same_inode(cur_stat, root_stat)) {
46+
throw std::system_error(EACCES,
47+
std::system_category(),
48+
"path escape detected: attempted to go above root");
49+
}
50+
}
51+
52+
auto walk_component(linyaps_box::utils::file_descriptor &dir_fd,
53+
const std::string &component,
54+
const linyaps_box::utils::file_descriptor &root,
55+
int symlink_depth) -> void;
56+
57+
auto resolve_symlink_component(linyaps_box::utils::file_descriptor &dir_fd,
58+
linyaps_box::utils::file_descriptor &link_fd,
59+
const linyaps_box::utils::file_descriptor &root,
60+
int symlink_depth) -> void
61+
{
62+
if (UNLIKELY(symlink_depth > MAX_SYMLINK_DEPTH)) {
63+
throw std::system_error(ELOOP, std::system_category(), "too many symlinks");
64+
}
65+
66+
auto target = linyaps_box::utils::readlinkat(link_fd, "");
67+
LINYAPS_BOX_DEBUG() << "resolve symlink -> " << target;
68+
69+
if (target.is_absolute()) {
70+
dir_fd = root.duplicate();
71+
}
72+
73+
for (const auto &part : target.relative_path()) {
74+
walk_component(dir_fd, part.string(), root, symlink_depth);
75+
}
76+
}
77+
78+
auto walk_component(linyaps_box::utils::file_descriptor &dir_fd,
79+
const std::string &component,
80+
const linyaps_box::utils::file_descriptor &root,
81+
int symlink_depth) -> void
82+
{
83+
if (component == ".") {
84+
return;
85+
}
86+
87+
if (component == "..") {
88+
check_not_above_root(dir_fd, root);
89+
auto fd = ::openat(dir_fd.get(), "..", O_PATH | O_CLOEXEC | O_NOFOLLOW);
90+
if (UNLIKELY(fd < 0)) {
91+
throw std::system_error(errno,
92+
std::system_category(),
93+
"openat: failed to open parent directory");
94+
}
95+
96+
linyaps_box::utils::file_descriptor parent_fd{ fd };
97+
dir_fd = std::move(parent_fd);
98+
return;
99+
}
100+
101+
auto fd = ::openat(dir_fd.get(), component.c_str(), O_PATH | O_CLOEXEC | O_NOFOLLOW);
102+
if (UNLIKELY(fd < 0)) {
103+
throw std::system_error(errno,
104+
std::system_category(),
105+
std::string{ "openat: failed to open component " } + component);
106+
}
107+
108+
linyaps_box::utils::file_descriptor child_fd{ fd };
109+
auto child_stat = linyaps_box::utils::fstat(child_fd);
110+
111+
if (S_ISLNK(child_stat.st_mode)) {
112+
resolve_symlink_component(dir_fd, child_fd, root, symlink_depth + 1);
113+
return;
114+
}
115+
116+
if (UNLIKELY(!S_ISDIR(child_stat.st_mode))) {
117+
throw std::system_error(ENOTDIR, std::system_category(), "not a directory: " + component);
118+
}
119+
120+
dir_fd = std::move(child_fd);
121+
}
122+
29123
auto open_at_fallback(const linyaps_box::utils::file_descriptor &root,
30124
const std::filesystem::path &path,
31125
int flag,
@@ -35,23 +129,65 @@ auto open_at_fallback(const linyaps_box::utils::file_descriptor &root,
35129
<< linyaps_box::utils::inspect_fcntl_or_open_flags(
36130
static_cast<size_t>(flag))
37131
<< "\n\t" << linyaps_box::utils::inspect_fd(root.get());
38-
// TODO: we need implement a compatible fallback
39-
// currently we just use openat and do some simple check
40-
const auto &file_path = path.relative_path();
41-
const auto fd = ::openat(root.get(), file_path.c_str(), flag, mode);
42-
if (fd < 0) {
43-
auto full_path = root.current_path() / path.relative_path();
132+
133+
auto current = root.duplicate();
134+
const auto &parts = path.relative_path();
135+
136+
auto it = parts.begin();
137+
auto end = parts.end();
138+
size_t index{ 0 };
139+
auto total = std::distance(it, end);
140+
141+
for (; it != end; ++it, ++index) {
142+
auto component = it->string();
143+
auto is_last = (index + 1 == static_cast<size_t>(total));
144+
145+
if (component == ".") {
146+
continue;
147+
}
148+
149+
if (component == "..") {
150+
walk_component(current, component, root, 0);
151+
continue;
152+
}
153+
154+
if (is_last) {
155+
auto fd = ::openat(current.get(), component.c_str(), flag, mode);
156+
if (UNLIKELY(fd < 0)) {
157+
auto full_path = root.current_path() / path.relative_path();
158+
throw std::system_error(errno,
159+
std::system_category(),
160+
std::string{ "openat: failed to open " }
161+
+ full_path.string());
162+
}
163+
164+
return linyaps_box::utils::file_descriptor{ fd };
165+
}
166+
167+
walk_component(current, component, root, 0);
168+
}
169+
170+
auto fd = ::openat(current.get(), ".", flag, mode);
171+
if (UNLIKELY(fd < 0)) {
44172
throw std::system_error(errno,
45173
std::system_category(),
46-
std::string{ "openat: failed to open " } + full_path.string());
174+
"openat: failed to open resolved path");
47175
}
48176

49177
return linyaps_box::utils::file_descriptor{ fd };
50178
}
51179

52-
auto syscall_openat2(int dirfd, const char *path, uint64_t flag, uint64_t mode, uint64_t resolve)
53-
-> linyaps_box::utils::file_descriptor
180+
// TODO: use expected for better error handling
181+
auto syscall_openat2(int dirfd,
182+
const char *path,
183+
uint64_t flag,
184+
uint64_t mode,
185+
uint64_t resolve,
186+
std::error_code &ec) noexcept
187+
-> std::optional<linyaps_box::utils::file_descriptor>
54188
{
189+
ec.clear();
190+
55191
struct openat2_how
56192
{
57193
uint64_t flags;
@@ -60,8 +196,9 @@ auto syscall_openat2(int dirfd, const char *path, uint64_t flag, uint64_t mode,
60196
} how{ flag, mode, resolve };
61197

62198
const auto ret = syscall(__NR_openat2, dirfd, path, &how, sizeof(openat2_how), 0);
63-
if (ret < 0) {
64-
throw std::system_error(errno, std::system_category(), "openat2");
199+
if (UNLIKELY(ret < 0)) {
200+
ec.assign(errno, std::system_category());
201+
return std::nullopt;
65202
}
66203

67204
return linyaps_box::utils::file_descriptor{ static_cast<int>(ret) };
@@ -70,7 +207,7 @@ auto syscall_openat2(int dirfd, const char *path, uint64_t flag, uint64_t mode,
70207
auto read_pseudo_file(const std::filesystem::path &path) -> std::string
71208
{
72209
std::ifstream ifs(path, std::ios::in | std::ios::binary);
73-
if (!ifs) {
210+
if (UNLIKELY(!ifs)) {
74211
throw std::runtime_error("Can't open pseudo file " + path.string());
75212
}
76213

@@ -87,7 +224,7 @@ auto open(const std::filesystem::path &path, int flag, mode_t mode)
87224
LINYAPS_BOX_DEBUG() << "open " << path.c_str() << " with "
88225
<< inspect_fcntl_or_open_flags(static_cast<size_t>(flag));
89226
const auto fd = ::open(path.c_str(), flag, mode);
90-
if (fd == -1) {
227+
if (UNLIKELY(fd == -1)) {
91228
throw std::system_error(errno,
92229
std::system_category(),
93230
"open: failed to open " + path.string());
@@ -105,34 +242,38 @@ auto open_at(const linyaps_box::utils::file_descriptor &root,
105242
<< inspect_fcntl_or_open_flags(static_cast<size_t>(flag)) << "\n\t"
106243
<< inspect_fd(root.get());
107244

245+
// currently box is running in single thread, so use bool is safe
108246
static bool support_openat2{ true };
109247
while (support_openat2) {
110-
try {
111-
return syscall_openat2(root.get(),
248+
std::error_code ec;
249+
250+
auto ret = syscall_openat2(root.get(),
112251
path.c_str(),
113252
static_cast<uint64_t>(flag),
114253
mode,
115-
RESOLVE_IN_ROOT);
116-
} catch (const std::system_error &e) {
117-
const auto code = e.code().value();
118-
if (code == EINTR || code == EAGAIN) {
254+
RESOLVE_IN_ROOT,
255+
ec);
256+
if (ec) {
257+
auto val = ec.value();
258+
if (val == EINTR || val == EAGAIN) {
119259
continue;
120260
}
121261

122-
if (code == ENOSYS) {
262+
if (val == ENOSYS) {
123263
support_openat2 = false;
124264
break;
125265
}
126266

127-
if (code == EINVAL || code == EPERM) {
128-
break;
267+
if (val == EINVAL || val == E2BIG) {
268+
// EINVAL can be an unknown flag or invalid value was specified in how.
269+
// E2BIG can be an extension that this kernel does not support was specified in how
270+
return open_at_fallback(root, path, flag, mode);
129271
}
130272

131-
throw std::system_error(code,
132-
std::system_category(),
133-
std::string{ e.what() } + ": failed to open "
134-
+ (root.current_path() / path.relative_path()).string());
273+
throw std::system_error(ec, "failed to openat2 " + path.string());
135274
}
275+
276+
return std::move(*ret);
136277
}
137278

138279
// NOTE: openat2 only available after linux 5.15
@@ -144,7 +285,7 @@ auto touch(const file_descriptor &root, const std::filesystem::path &path, int f
144285
{
145286
LINYAPS_BOX_DEBUG() << "touch " << path << " at " << inspect_fd(root.get());
146287
const auto fd = ::openat(root.get(), path.c_str(), flag, mode);
147-
if (fd == -1) {
288+
if (UNLIKELY(fd == -1)) {
148289
throw std::system_error(errno,
149290
std::system_category(),
150291
"openat: " + (root.current_path() / path.relative_path()).string());
@@ -157,7 +298,7 @@ auto fstat(const file_descriptor &fd) -> struct stat
157298
{
158299
struct stat statbuf{ };
159300
auto ret = ::fstat(fd.get(), &statbuf);
160-
if (ret == -1) {
301+
if (UNLIKELY(ret == -1)) {
161302
throw std::system_error(errno, std::system_category(), "fstat");
162303
}
163304

@@ -170,7 +311,7 @@ auto fstatat(const file_descriptor &fd, const std::filesystem::path &path, int f
170311
{
171312
struct stat statbuf{ };
172313
auto ret = ::fstatat(fd.get(), path.c_str(), &statbuf, flag);
173-
if (ret == -1) {
314+
if (UNLIKELY(ret == -1)) {
174315
throw std::system_error(errno, std::system_category(), "fstatat");
175316
}
176317

@@ -197,7 +338,7 @@ auto lstat(const std::filesystem::path &path) -> struct stat
197338
{
198339
struct stat statbuf{ };
199340
auto ret = ::lstat(path.c_str(), &statbuf);
200-
if (ret == -1) {
341+
if (UNLIKELY(ret == -1)) {
201342
throw std::system_error(errno,
202343
std::system_category(),
203344
"lstat " + path.string() + " failed:");
@@ -212,7 +353,7 @@ auto statfs(const file_descriptor &fd) -> struct statfs
212353
{
213354
struct statfs statbuf{ };
214355
auto ret = ::statfs(fd.proc_path().c_str(), &statbuf);
215-
if (ret == -1) {
356+
if (UNLIKELY(ret == -1)) {
216357
throw std::system_error(errno, std::system_category(), "statfs");
217358
}
218359

src/linyaps_box/utils/symlink.cpp

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
#include "linyaps_box/utils/log.h"
88
#include "linyaps_box/utils/platform.h"
9+
#include "linyaps_box/utils/utils.h"
910

1011
#include <linux/limits.h>
1112

@@ -63,18 +64,13 @@ std::filesystem::path linyaps_box::utils::readlink(const std::filesystem::path &
6364
std::filesystem::path linyaps_box::utils::readlinkat(const file_descriptor &dirfd,
6465
const std::filesystem::path &path)
6566
{
66-
auto parent = path.parent_path();
67-
if (path.is_relative()) {
68-
parent = dirfd.current_path() / parent;
69-
}
70-
71-
auto buf_len = get_path_max(parent) + 1;
67+
auto buf_len = get_path_max(dirfd.current_path()) + 1;
7268
std::string buf(buf_len, '\0');
73-
7469
auto ret = ::readlinkat(dirfd.get(), path.c_str(), buf.data(), buf_len - 1);
75-
if (ret == -1) {
70+
if (UNLIKELY(ret == -1)) {
7671
throw std::system_error(errno, std::system_category(), "readlinkat");
7772
}
7873

79-
return std::filesystem::path{ buf.data() };
74+
buf.resize(static_cast<size_t>(ret));
75+
return std::filesystem::path{ std::move(buf) };
8076
}

0 commit comments

Comments
 (0)