Skip to content

Commit bf8827b

Browse files
committed
fs: allow realpath to resolve deep symlinks
realpath(3) would fail if the symbolic link depth was too deep. If ELOOP is encountered then resolve the path in parts until the entire thing is resolved. This excludes if the number of symbolic links is too deep, or if they are recursive. Fixes: nodejs#7175
1 parent 8662974 commit bf8827b

File tree

2 files changed

+145
-11
lines changed

2 files changed

+145
-11
lines changed

lib/fs.js

+57-1
Original file line numberDiff line numberDiff line change
@@ -1590,12 +1590,68 @@ fs.realpath = function realpath(path, options, callback) {
15901590
if (!nullCheck(path, callback))
15911591
return;
15921592
var req = new FSReqWrap();
1593-
req.oncomplete = callback;
1593+
req.oncomplete = function oncomplete(err, resolvedPath) {
1594+
interceptELOOP(err, resolvedPath, path, options, callback);
1595+
};
15941596
binding.realpath(pathModule._makeLong(path), options.encoding, req);
15951597
return;
15961598
};
15971599

15981600

1601+
function interceptELOOP(err, resolvedPath, path, options, callback) {
1602+
if (resolvedPath)
1603+
return callback(err, resolvedPath);
1604+
if (err && err.code !== 'ELOOP')
1605+
return callback(err);
1606+
var retPath = '';
1607+
// Start at -1 since first operation is to + 1.
1608+
var offset = -1;
1609+
var current = 0;
1610+
var slashCount = 0;
1611+
var callDepth = 0;
1612+
1613+
// Can assume '/' because uv_fs_realpath() cannot return UV_ELOOP on win.
1614+
(function pathResolver(err2, resolvedPath2) {
1615+
// No need to handle an error that was returned by a recursive call.
1616+
if (err2) return callback(err2);
1617+
// callDepth is too much. Return original error.
1618+
if (++callDepth > 100) return callback(err);
1619+
// Done iterating over the path.
1620+
if (offset === path.length) return callback(null, resolvedPath2);
1621+
1622+
retPath = resolvedPath2;
1623+
slashCount = countSlashes(retPath);
1624+
offset = get32SlashOffset(path, offset + 1, slashCount);
1625+
1626+
var tmpPath = retPath + path.slice(current, offset);
1627+
current = offset;
1628+
var req = new FSReqWrap();
1629+
req.oncomplete = pathResolver;
1630+
binding.realpath(pathModule._makeLong(tmpPath), options.encoding, req);
1631+
}(null, ''));
1632+
}
1633+
1634+
1635+
function get32SlashOffset(path, offset, slashCounter) {
1636+
// OSX libc bails with ELOOP when encountering more than MAXSYMLINKS, which
1637+
// is hard coded to in the kernel header to 32.
1638+
while ((offset = path.indexOf('/', offset + 1)) !== -1 &&
1639+
++slashCounter < 32);
1640+
if (offset === -1)
1641+
offset = path.length;
1642+
return offset;
1643+
}
1644+
1645+
1646+
function countSlashes(path) {
1647+
var offset = -1;
1648+
var counter = 0;
1649+
while ((offset = path.indexOf('/', offset + 1)) !== -1)
1650+
counter++;
1651+
return counter;
1652+
}
1653+
1654+
15991655
fs.mkdtemp = function(prefix, options, callback) {
16001656
if (!prefix || typeof prefix !== 'string')
16011657
throw new TypeError('filename prefix is required');

src/node_file.cc

+88-10
Original file line numberDiff line numberDiff line change
@@ -361,16 +361,19 @@ class fs_req_wrap {
361361
#define ASYNC_CALL(func, req, encoding, ...) \
362362
ASYNC_DEST_CALL(func, req, nullptr, encoding, __VA_ARGS__) \
363363

364-
#define SYNC_DEST_CALL(func, path, dest, ...) \
365-
fs_req_wrap req_wrap; \
364+
#define SYNC_CALL_NO_THROW(req_wrap, func, dest, ...) \
366365
env->PrintSyncTrace(); \
367366
int err = uv_fs_ ## func(env->event_loop(), \
368-
&req_wrap.req, \
367+
&(req_wrap).req, \
369368
__VA_ARGS__, \
370-
nullptr); \
371-
if (err < 0) { \
372-
return env->ThrowUVException(err, #func, nullptr, path, dest); \
373-
} \
369+
nullptr);
370+
371+
#define SYNC_DEST_CALL(func, path, dest, ...) \
372+
fs_req_wrap req_wrap; \
373+
SYNC_CALL_NO_THROW(req_wrap, func, dest, __VA_ARGS__) \
374+
if (SYNC_RESULT < 0) { \
375+
return env->ThrowUVException(SYNC_RESULT, #func, nullptr, path, dest); \
376+
}
374377

375378
#define SYNC_CALL(func, path, ...) \
376379
SYNC_DEST_CALL(func, path, nullptr, __VA_ARGS__) \
@@ -882,6 +885,77 @@ static void MKDir(const FunctionCallbackInfo<Value>& args) {
882885
}
883886
}
884887

888+
889+
static size_t CountSlashes(const char* str, size_t len) {
890+
size_t cntr = 0;
891+
for (size_t i = 0; i < len; i++) {
892+
if (str[i] == '/') cntr++;
893+
}
894+
return cntr;
895+
}
896+
897+
898+
static int ResolveRealPathSync(Environment* env,
899+
std::string* ret_str,
900+
const char* path,
901+
size_t call_depth) {
902+
fs_req_wrap req_wrap;
903+
SYNC_CALL_NO_THROW(req_wrap, realpath, path, path);
904+
905+
call_depth++;
906+
if (SYNC_RESULT != UV_ELOOP) {
907+
if (SYNC_RESULT) return SYNC_RESULT;
908+
*ret_str = std::string(static_cast<const char*>(SYNC_REQ.ptr));
909+
return SYNC_RESULT;
910+
// TODO(trevnorris): Instead of simply not allowing too many recursive
911+
// calls, would it instead be a viable solution to attempt detection of
912+
// recursive symlinks? Thus preventing false negatives.
913+
} else if (SYNC_RESULT == UV_ELOOP && call_depth > 100) {
914+
return UV_ELOOP;
915+
}
916+
917+
#ifdef _WIN32
918+
const char separator = '\\';
919+
#else
920+
const char separator = '/';
921+
#endif
922+
std::string str_path(path);
923+
size_t offset = 0;
924+
size_t current = 0;
925+
size_t slash_count = 0;
926+
927+
// Can assume '/' because uv_fs_realpath() cannot return UV_ELOOP on win.
928+
while ((offset = str_path.find(separator, offset + 1)) != std::string::npos) {
929+
// OSX libc bails with ELOOP when encountering more than MAXSYMLINKS,
930+
// which is hard coded to in the kernel header to 32.
931+
if (++slash_count < 32) {
932+
continue;
933+
}
934+
935+
std::string partial = *ret_str + str_path.substr(current, offset - current);
936+
int err2 = ResolveRealPathSync(env, ret_str, partial.c_str(), call_depth);
937+
// No need to handle an error that was returned by a recursive call.
938+
if (err2) {
939+
*ret_str = std::string();
940+
return err2;
941+
}
942+
943+
current = offset;
944+
slash_count = CountSlashes(ret_str->c_str(), ret_str->length());
945+
}
946+
947+
if (offset == std::string::npos) {
948+
offset = str_path.length();
949+
}
950+
if (current >= offset) {
951+
return 0;
952+
}
953+
954+
std::string pass(*ret_str + str_path.substr(current, offset - current));
955+
return ResolveRealPathSync(env, ret_str, pass.c_str(), call_depth);
956+
}
957+
958+
885959
static void RealPath(const FunctionCallbackInfo<Value>& args) {
886960
Environment* env = Environment::GetCurrent(args);
887961

@@ -902,10 +976,14 @@ static void RealPath(const FunctionCallbackInfo<Value>& args) {
902976
if (callback->IsObject()) {
903977
ASYNC_CALL(realpath, callback, encoding, *path);
904978
} else {
905-
SYNC_CALL(realpath, *path, *path);
906-
const char* link_path = static_cast<const char*>(SYNC_REQ.ptr);
979+
std::string rc_string;
980+
// Resolve the symlink attempting simple amount of deep path resolution.
981+
int err = ResolveRealPathSync(env, &rc_string, *path, 0);
982+
if (err) {
983+
return env->ThrowUVException(err, "realpath", nullptr, *path, *path);
984+
}
907985
Local<Value> rc = StringBytes::Encode(env->isolate(),
908-
link_path,
986+
rc_string.c_str(),
909987
encoding);
910988
if (rc.IsEmpty()) {
911989
return env->ThrowUVException(UV_EINVAL,

0 commit comments

Comments
 (0)