Skip to content

Commit

Permalink
fs: allow realpath to resolve deep symlinks
Browse files Browse the repository at this point in the history
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
  • Loading branch information
trevnorris committed Jul 14, 2016
1 parent 8662974 commit bf8827b
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 11 deletions.
58 changes: 57 additions & 1 deletion lib/fs.js
Original file line number Diff line number Diff line change
Expand Up @@ -1590,12 +1590,68 @@ fs.realpath = function realpath(path, options, callback) {
if (!nullCheck(path, callback))
return;
var req = new FSReqWrap();
req.oncomplete = callback;
req.oncomplete = function oncomplete(err, resolvedPath) {
interceptELOOP(err, resolvedPath, path, options, callback);
};
binding.realpath(pathModule._makeLong(path), options.encoding, req);
return;
};


function interceptELOOP(err, resolvedPath, path, options, callback) {
if (resolvedPath)
return callback(err, resolvedPath);
if (err && err.code !== 'ELOOP')
return callback(err);
var retPath = '';
// Start at -1 since first operation is to + 1.
var offset = -1;
var current = 0;
var slashCount = 0;
var callDepth = 0;

// Can assume '/' because uv_fs_realpath() cannot return UV_ELOOP on win.
(function pathResolver(err2, resolvedPath2) {
// No need to handle an error that was returned by a recursive call.
if (err2) return callback(err2);
// callDepth is too much. Return original error.
if (++callDepth > 100) return callback(err);
// Done iterating over the path.
if (offset === path.length) return callback(null, resolvedPath2);

retPath = resolvedPath2;
slashCount = countSlashes(retPath);
offset = get32SlashOffset(path, offset + 1, slashCount);

var tmpPath = retPath + path.slice(current, offset);
current = offset;
var req = new FSReqWrap();
req.oncomplete = pathResolver;
binding.realpath(pathModule._makeLong(tmpPath), options.encoding, req);
}(null, ''));
}


function get32SlashOffset(path, offset, slashCounter) {
// OSX libc bails with ELOOP when encountering more than MAXSYMLINKS, which
// is hard coded to in the kernel header to 32.
while ((offset = path.indexOf('/', offset + 1)) !== -1 &&
++slashCounter < 32);
if (offset === -1)
offset = path.length;
return offset;
}


function countSlashes(path) {
var offset = -1;
var counter = 0;
while ((offset = path.indexOf('/', offset + 1)) !== -1)
counter++;
return counter;
}


fs.mkdtemp = function(prefix, options, callback) {
if (!prefix || typeof prefix !== 'string')
throw new TypeError('filename prefix is required');
Expand Down
98 changes: 88 additions & 10 deletions src/node_file.cc
Original file line number Diff line number Diff line change
Expand Up @@ -361,16 +361,19 @@ class fs_req_wrap {
#define ASYNC_CALL(func, req, encoding, ...) \
ASYNC_DEST_CALL(func, req, nullptr, encoding, __VA_ARGS__) \

#define SYNC_DEST_CALL(func, path, dest, ...) \
fs_req_wrap req_wrap; \
#define SYNC_CALL_NO_THROW(req_wrap, func, dest, ...) \
env->PrintSyncTrace(); \
int err = uv_fs_ ## func(env->event_loop(), \
&req_wrap.req, \
&(req_wrap).req, \
__VA_ARGS__, \
nullptr); \
if (err < 0) { \
return env->ThrowUVException(err, #func, nullptr, path, dest); \
} \
nullptr);

#define SYNC_DEST_CALL(func, path, dest, ...) \
fs_req_wrap req_wrap; \
SYNC_CALL_NO_THROW(req_wrap, func, dest, __VA_ARGS__) \
if (SYNC_RESULT < 0) { \
return env->ThrowUVException(SYNC_RESULT, #func, nullptr, path, dest); \
}

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


static size_t CountSlashes(const char* str, size_t len) {
size_t cntr = 0;
for (size_t i = 0; i < len; i++) {
if (str[i] == '/') cntr++;
}
return cntr;
}


static int ResolveRealPathSync(Environment* env,
std::string* ret_str,
const char* path,
size_t call_depth) {
fs_req_wrap req_wrap;
SYNC_CALL_NO_THROW(req_wrap, realpath, path, path);

call_depth++;
if (SYNC_RESULT != UV_ELOOP) {
if (SYNC_RESULT) return SYNC_RESULT;
*ret_str = std::string(static_cast<const char*>(SYNC_REQ.ptr));
return SYNC_RESULT;
// TODO(trevnorris): Instead of simply not allowing too many recursive
// calls, would it instead be a viable solution to attempt detection of
// recursive symlinks? Thus preventing false negatives.
} else if (SYNC_RESULT == UV_ELOOP && call_depth > 100) {
return UV_ELOOP;
}

#ifdef _WIN32
const char separator = '\\';
#else
const char separator = '/';
#endif
std::string str_path(path);
size_t offset = 0;
size_t current = 0;
size_t slash_count = 0;

// Can assume '/' because uv_fs_realpath() cannot return UV_ELOOP on win.
while ((offset = str_path.find(separator, offset + 1)) != std::string::npos) {
// OSX libc bails with ELOOP when encountering more than MAXSYMLINKS,
// which is hard coded to in the kernel header to 32.
if (++slash_count < 32) {
continue;
}

std::string partial = *ret_str + str_path.substr(current, offset - current);
int err2 = ResolveRealPathSync(env, ret_str, partial.c_str(), call_depth);
// No need to handle an error that was returned by a recursive call.
if (err2) {
*ret_str = std::string();
return err2;
}

current = offset;
slash_count = CountSlashes(ret_str->c_str(), ret_str->length());
}

if (offset == std::string::npos) {
offset = str_path.length();
}
if (current >= offset) {
return 0;
}

std::string pass(*ret_str + str_path.substr(current, offset - current));
return ResolveRealPathSync(env, ret_str, pass.c_str(), call_depth);
}


static void RealPath(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);

Expand All @@ -902,10 +976,14 @@ static void RealPath(const FunctionCallbackInfo<Value>& args) {
if (callback->IsObject()) {
ASYNC_CALL(realpath, callback, encoding, *path);
} else {
SYNC_CALL(realpath, *path, *path);
const char* link_path = static_cast<const char*>(SYNC_REQ.ptr);
std::string rc_string;
// Resolve the symlink attempting simple amount of deep path resolution.
int err = ResolveRealPathSync(env, &rc_string, *path, 0);
if (err) {
return env->ThrowUVException(err, "realpath", nullptr, *path, *path);
}
Local<Value> rc = StringBytes::Encode(env->isolate(),
link_path,
rc_string.c_str(),
encoding);
if (rc.IsEmpty()) {
return env->ThrowUVException(UV_EINVAL,
Expand Down

0 comments on commit bf8827b

Please sign in to comment.