Added symlink() Implementation; Revised readlink() (#289)

- Added implementation of symlink() using native CreateSymbolicLink() function.
- Added unit tests for symlink behavior in readlink(), lstat, stat(), and symlink().
- Reworked readlink() to be more inline with POSIX readlink() behavior.
- Reworked symlink handling in stat/lstat due to revised readlink().
- Added additional error handling to readlink().
- Added symlink() Implementation - Memory Leak Fix
- Modified fileio_readlink() to properly free a temporary buffer it creates.
This commit is contained in:
Bryan Berns 2018-03-29 13:54:38 -04:00 committed by Manoj Ampalam
parent 00b869dbd8
commit 993cce0798
6 changed files with 325 additions and 152 deletions

View File

@ -87,6 +87,7 @@ int
errno_from_Win32Error(int win32_error)
{
switch (win32_error) {
case ERROR_PRIVILEGE_NOT_HELD:
case ERROR_ACCESS_DENIED:
return EACCES;
case ERROR_OUTOFMEMORY:
@ -752,124 +753,12 @@ fileio_fstat(struct w32_io* pio, struct _stat64 *buf)
return _fstat64(fd, buf);
}
wchar_t *
fileio_readlink_internal(wchar_t * wpath)
{
/* note: there are two approaches for resolving a symlink in Windows:
*
* 1) Use CreateFile() to obtain a file handle to the reparse point and
* send using the DeviceIoControl() call to retrieve the link data from the
* reparse point.
* 2) Use CreateFile() to obtain a file handle to the target file followed
* by a call to GetFinalPathNameByHandle() to get the real path on the
* file system.
*
* This approach uses the first method because the second method does not
* work on broken link since the target file cannot be opened. It also
* requires additional I/O to read both the symlink and its target.
*/
/* abbreviated REPARSE_DATA_BUFFER data structure for decoding symlinks;
* the full definition can be found in ntifs.h within the Windows DDK.
* we include it here so the DDK does not become prereq to the build.
* for more info: https://msdn.microsoft.com/en-us/library/cc232006.aspx
*/
typedef struct _REPARSE_DATA_BUFFER_SYMLINK {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} REPARSE_DATA_BUFFER_SYMLINK, *PREPARSE_DATA_BUFFER_SYMLINK;
/* early declarations for cleanup */
wchar_t *linkpath = NULL;
HANDLE handle = INVALID_HANDLE_VALUE;
PREPARSE_DATA_BUFFER_SYMLINK reparse_buffer = NULL;
/* obtain a handle to send to deviceioctl */
handle = CreateFileW(wpath, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0);
if (handle == INVALID_HANDLE_VALUE) {
errno = errno_from_Win32LastError();
goto cleanup;
}
/* send a request to the file system to get the real path */
reparse_buffer = (PREPARSE_DATA_BUFFER_SYMLINK) malloc(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
DWORD dwBytesReturned = 0;
if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
(LPVOID)reparse_buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dwBytesReturned, 0) == 0) {
errno = errno_from_Win32LastError();
goto cleanup;
}
/* ensure file is actually symlink */
if (reparse_buffer->ReparseTag != IO_REPARSE_TAG_SYMLINK) {
errno = ENOLINK;
goto cleanup;
}
/* the symlink structure has a 'Print Name' value that is displayed to the
* user which is different from the actual value it uses for redirection
* called the 'Substitute Name'; since the Substitute Name has an odd format
* that begins with \??\ and it appears that CreateSymbolicLink() always
* formats the PrintName value consistently we will just use that
*/
int symlink_nonnull_size = reparse_buffer->PrintNameLength;
wchar_t * symlink_nonnull = &reparse_buffer->PathBuffer[reparse_buffer->PrintNameOffset / sizeof(WCHAR)];
/* since readlink allows a return string larger than MAX_PATH by specifying
* the bufsiz parameter and windows can have paths larger than MAX_PATH,
* dynamically allocate a string to hold the resultant symbolic link path.
* this string could be as large as parent path plus the reparse buffer
* data plus a null terminator.
*/
const int wpath_len = (int)wcslen(wpath);
int linkpath_len = wpath_len + symlink_nonnull_size / sizeof(wchar_t) + 1;
linkpath = calloc(linkpath_len, sizeof(wchar_t));
if (linkpath == NULL) {
errno = ENOMEM;
goto cleanup;
}
/* if symlink is relative, copy the truncated parent as the base path*/
if (reparse_buffer->Flags != 0) {
/* copy the parent path, convert forward slashes to backslashes, and
* trim off the last entry in the path */
wcscpy_s(linkpath, linkpath_len, wpath);
convertToBackslashW(linkpath);
for (int i = wpath_len; i >= 0; i--) {
if (linkpath[i] == L'\\') {
linkpath[i+1] = L'\0';
break;
}
}
}
/* append the symbolic link data to the output string*/
wcsncat_s(linkpath, linkpath_len, symlink_nonnull, symlink_nonnull_size / sizeof(wchar_t));
cleanup:
if (reparse_buffer)
free(reparse_buffer);
if (handle != INVALID_HANDLE_VALUE)
CloseHandle(handle);
return linkpath;
}
int
fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lstat)
{
wchar_t *wpath = NULL;
wchar_t *resolved_link = NULL;
char link_test = L'\0';
HANDLE link_handle = INVALID_HANDLE_VALUE;
WIN32_FILE_ATTRIBUTE_DATA attributes = { 0 };
int ret = -1;
int is_link = 0;
@ -897,15 +786,29 @@ fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lsta
/* try to see if it is a symlink */
is_link = (attributes.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT &&
(resolved_link = fileio_readlink_internal(wpath)) != NULL);
fileio_readlink(path, &link_test, 1) == 1);
/* if doing a stat() on a link, then lookup attributes on the target of the link */
if (!do_lstat && is_link) {
is_link = 0;
if (GetFileAttributesExW(resolved_link, GetFileExInfoStandard, &attributes) == FALSE) {
/* obtain a file handle to the destination file (not the source link) */
BY_HANDLE_FILE_INFORMATION link_attributes;
if ((link_handle = CreateFileW(wpath, 0, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS, NULL)) == INVALID_HANDLE_VALUE
|| GetFileInformationByHandle(link_handle, &link_attributes) == 0)
{
errno = errno_from_Win32LastError();
goto cleanup;
}
/* copy attributes from handle structure to normal structure */
attributes.ftCreationTime = link_attributes.ftCreationTime;
attributes.ftLastAccessTime = link_attributes.ftLastAccessTime;
attributes.ftLastWriteTime = link_attributes.ftLastWriteTime;
attributes.nFileSizeHigh = link_attributes.nFileSizeHigh;
attributes.nFileSizeLow = link_attributes.nFileSizeLow;
attributes.dwFileAttributes = link_attributes.dwFileAttributes;
is_link = 0;
}
buf->st_ino = 0; /* Has no meaning in the FAT, HPFS, or NTFS file systems*/
@ -931,8 +834,8 @@ fileio_stat_or_lstat_internal(const char *path, struct _stat64 *buf, int do_lsta
ret = 0;
cleanup:
if (resolved_link)
free(resolved_link);
if (link_handle != INVALID_HANDLE_VALUE)
CloseHandle(link_handle);
if (wpath)
free(wpath);
return ret;
@ -1085,19 +988,54 @@ fileio_is_io_available(struct w32_io* pio, BOOL rd)
}
}
ssize_t
ssize_t
fileio_readlink(const char *path, char *buf, size_t bufsiz)
{
/* note: there are two approaches for resolving a symlink in Windows:
*
* 1) Use CreateFile() to obtain a file handle to the reparse point and
* send using the DeviceIoControl() call to retrieve the link data from the
* reparse point.
* 2) Use CreateFile() to obtain a file handle to the target file followed
* by a call to GetFinalPathNameByHandle() to get the real path on the
* file system.
*
* This approach uses the first method because the second method does not
* work on broken link since the target file cannot be opened. It also
* requires additional I/O to read both the symlink and its target.
*/
/* abbreviated REPARSE_DATA_BUFFER data structure for decoding symlinks;
* the full definition can be found in ntifs.h within the Windows DDK.
* we include it here so the DDK does not become prereq to the build.
* for more info: https://msdn.microsoft.com/en-us/library/cc232006.aspx
*/
debug4("readlink - io:%p", pio);
wchar_t *wpath = NULL;
wchar_t *resolved_link = NULL;
char* output = NULL;
typedef struct _REPARSE_DATA_BUFFER_SYMLINK {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} REPARSE_DATA_BUFFER_SYMLINK, *PREPARSE_DATA_BUFFER_SYMLINK;
/* early declarations for cleanup */
ssize_t ret = -1;
wchar_t *wpath = NULL;
wchar_t *linkpath = NULL;
char *output = NULL;
HANDLE handle = INVALID_HANDLE_VALUE;
PREPARSE_DATA_BUFFER_SYMLINK reparse_buffer = NULL;
/* sanity check */
if (buf == NULL) {
errno = EFAULT;
if (path == NULL || buf == NULL || bufsiz == 0) {
errno = EINVAL;
goto cleanup;
}
@ -1106,39 +1044,161 @@ fileio_readlink(const char *path, char *buf, size_t bufsiz)
goto cleanup;
}
/* read the link data from the passed symlink */
resolved_link = fileio_readlink_internal(wpath);
if (resolved_link == NULL) {
/* errorno will have been set by called function */
/* obtain a handle to send to deviceioctl */
handle = CreateFileW(wpath, 0, 0, NULL, OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, 0);
if (handle == INVALID_HANDLE_VALUE) {
errno = errno_from_Win32LastError();
goto cleanup;
}
if ((output = utf16_to_utf8(resolved_link)) == NULL) {
/* send a request to the file system to get the real path */
reparse_buffer = (PREPARSE_DATA_BUFFER_SYMLINK) malloc(MAXIMUM_REPARSE_DATA_BUFFER_SIZE);
DWORD dwBytesReturned = 0;
if (DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, NULL, 0,
(LPVOID) reparse_buffer, MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &dwBytesReturned, 0) == 0) {
errno = errno_from_Win32LastError();
goto cleanup;
}
/* ensure file is actually symlink */
if (reparse_buffer->ReparseTag != IO_REPARSE_TAG_SYMLINK) {
errno = EINVAL;
goto cleanup;
}
/* the symlink structure has a 'Print Name' value that is displayed to the
* user which is different from the actual value it uses for redirection
* called the 'Substitute Name'; since the Substitute Name has an odd format
* that begins with \??\ and it appears that CreateSymbolicLink() always
* formats the PrintName value consistently we will just use that
*/
int symlink_nonnull_size = reparse_buffer->PrintNameLength;
wchar_t * symlink_nonnull = &reparse_buffer->PathBuffer[reparse_buffer->PrintNameOffset / sizeof(WCHAR)];
/* allocate area to hold a null terminated version of the string */
if ((linkpath = malloc(symlink_nonnull_size + sizeof(wchar_t))) == NULL) {
goto cleanup;
}
/* copy the data out of the reparse buffer and add null terminator */
memcpy_s(linkpath, symlink_nonnull_size + sizeof(wchar_t), symlink_nonnull, symlink_nonnull_size);
linkpath[symlink_nonnull_size / sizeof(wchar_t)] = L'\0';
/* convert link path to utf8 */
if ((output = utf16_to_utf8(linkpath)) == NULL) {
errno = ENOMEM;
goto cleanup;
}
/* ensure output buffer is large enough forward slash and the string */
ssize_t out_size = (ssize_t) strlen(output);
if (1 + out_size > bufsiz) {
errno = ENAMETOOLONG;
goto cleanup;
}
/* determine if we need to prepend a forward slash to make this look like
* an absolute path C:\Path\Target --> /C:/Path/Target
*/
int abs_chars = is_absolute_path(output) ? 1 : 0;
if (abs_chars)
buf[0] = '/';
/* copy to output buffer in the forward-slash format: /C:/Path/Target */
/* copy link data to output buffer; per specification, truncation is okay */
convertToForwardslash(output);
buf[0] = '/';
memcpy(buf + 1, output, out_size);
ret = out_size + 1;
size_t out_size = strlen(output);
memcpy(buf + abs_chars, output, min(out_size, bufsiz - abs_chars));
ret = (ssize_t) min(out_size + abs_chars, bufsiz);
cleanup:
if (linkpath)
free(linkpath);
if (reparse_buffer)
free(reparse_buffer);
if (handle != INVALID_HANDLE_VALUE)
CloseHandle(handle);
if (wpath)
free(wpath);
if (output)
free(output);
if (resolved_link)
free(resolved_link);
return (ssize_t) ret;
}
return (ssize_t)ret;
}
int
fileio_symlink(const char *target, const char *linkpath)
{
if (target == NULL || linkpath == NULL) {
errno = EFAULT;
return -1;
}
DWORD ret = 0;
wchar_t *target_utf16 = utf8_to_utf16(resolved_path(target));
wchar_t *linkpath_utf16 = utf8_to_utf16(resolved_path(linkpath));
wchar_t *resolved_utf16 = _wcsdup(target_utf16);
if (target_utf16 == NULL || linkpath_utf16 == NULL || resolved_utf16 == NULL) {
errno = ENOMEM;
ret = -1;
goto cleanup;
}
/* Relative targets are relative to the link and not our current directory
* so attempt to calculate a resolvable path by removing the link file name
* leaving only the parent path and then append the relative link:
* C:\Path\Link with Link->SubDir\Target to C:\Path\SubDir\Target
*/
if (!is_absolute_path(target)) {
/* allocate area to hold the total possible path */
free(resolved_utf16);
size_t resolved_len = (wcslen(target_utf16) + wcslen(linkpath_utf16) + 1);
resolved_utf16 = malloc(resolved_len * sizeof(wchar_t));
if (resolved_utf16 == NULL) {
errno = ENOMEM;
ret = -1;
goto cleanup;
}
/* copy the relative target to the end of the link's parent */
wcscpy_s(resolved_utf16, resolved_len, linkpath_utf16);
convertToBackslashW(resolved_utf16);
wchar_t * ptr = wcsrchr(resolved_utf16, L'\\');
if (ptr == NULL) wcscpy_s(resolved_utf16, resolved_len, target_utf16);
else wcscpy_s(ptr + 1, resolved_len - (ptr + 1 - resolved_utf16), target_utf16);
}
/* unlike other platforms, we need to know whether the symbolic link target is
* a file or a directory. the only way we can confidently do this is to
* get the attributes of the target. therefore, our symlink() has the
* limitation of only creating symlink with valid targets
*/
WIN32_FILE_ATTRIBUTE_DATA attributes = { 0 };
if (GetFileAttributesExW(resolved_utf16, GetFileExInfoStandard, &attributes) == FALSE) {
errno = errno_from_Win32LastError();
ret = -1;
goto cleanup;
}
/* use the attribute of the file to determine the proper flag to send */
DWORD create_flags = (attributes.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) ?
SYMBOLIC_LINK_FLAG_DIRECTORY : 0;
/* symlink creation on earlier versions of windows were a privileged op
* and then an option was added to create symlink using from an unprivileged
* context so we try both operations, attempting privileged version first.
* note: 0x2 = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
*/
if (CreateSymbolicLinkW(linkpath_utf16, target_utf16, create_flags) == 0) {
if (CreateSymbolicLinkW(linkpath_utf16, target_utf16, create_flags | 0x2) == 0) {
errno = errno_from_Win32LastError();
ret = -1;
goto cleanup;
}
}
cleanup:
if (target_utf16)
free(target_utf16);
if (linkpath_utf16)
free(linkpath_utf16);
if (resolved_utf16)
free(resolved_utf16);
return ret;
}

View File

@ -20,5 +20,3 @@ FILE* w32_fdopen(int fd, const char *mode);
int w32_rename(const char *old_name, const char *new_name);
#define rename w32_rename
int is_absolute_path(char *);

View File

@ -644,9 +644,7 @@ w32_utimes(const char *filename, struct timeval *tvp)
int
w32_symlink(const char *target, const char *linkpath)
{
/* Not supported in windows */
errno = EOPNOTSUPP;
return -1;
return fileio_symlink(target, linkpath);
}
int
@ -1443,7 +1441,7 @@ get_program_data_path()
/* Windows absolute paths - \abc, /abc, c:\abc, c:/abc, __PROGRAMDATA__\openssh\sshd_config */
int
is_absolute_path(char *path)
is_absolute_path(const char *path)
{
int retVal = 0;
if(*path == '\"') /* skip double quote if path is "c:\abc" */

View File

@ -41,4 +41,5 @@ char* get_program_data_path();
HANDLE get_user_token(char* user);
int load_user_profile(HANDLE user_token, char* user);
int copy_file(char *source, char *destination);
int create_directory_withsddl(char *path, char *sddl);
int create_directory_withsddl(char *path, char *sddl);
int is_absolute_path(const char *);

View File

@ -164,4 +164,5 @@ int fileio_stat(const char *path, struct _stat64 *buf);
int fileio_lstat(const char *path, struct _stat64 *buf);
long fileio_lseek(struct w32_io* pio, unsigned __int64 offset, int origin);
FILE* fileio_fdopen(struct w32_io* pio, const char *mode);
ssize_t fileio_readlink(const char *path, char *buf, size_t bufsiz);
ssize_t fileio_readlink(const char *path, char *buf, size_t bufsiz);
int fileio_symlink(const char *target, const char *linkpath);

View File

@ -1,5 +1,8 @@
/*
* Author: Manoj Ampalam <manoj.ampalam@microsoft.com>
*
* Author: Bryan Berns <berns@uwalumni.com>
* Added tests for symlink(), readlink(), lstat()
*/
#include "includes.h"
@ -487,6 +490,117 @@ file_miscellaneous_tests()
TEST_DONE();
}
void
file_symlink_tests()
{
/* skip these unit tests if we cannot create symbolic links at all
* note: 0x2 = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE
*/
DeleteFileW(L"admin_check");
if (CreateSymbolicLinkW(L"admin_check", L"admin_check", 0) == 0 &&
CreateSymbolicLinkW(L"admin_check", L"admin_check", 0x2 == 0)) {
return;
}
DeleteFileW(L"admin_check");
wchar_t curdir[MAX_PATH];
GetCurrentDirectoryW(MAX_PATH, curdir);
/* perform a variety of symlink tests using unicode, directory targets,
* file targets, absolute/relative links, absolute/relative targets
*/
for (int do_unicode = 0; do_unicode <= 1; do_unicode++)
for (int do_dir = 0; do_dir <= 1; do_dir++)
for (int do_absolute_lnk = 0; do_absolute_lnk <= 1; do_absolute_lnk++)
for (int do_absolute_tgt = 0; do_absolute_tgt <= 1; do_absolute_tgt++)
{
char test_name[128];
sprintf(test_name, "Symlink: %s link, %s %s target, %s",
(do_absolute_lnk) ? "relative" : "absolute",
(do_absolute_tgt) ? "relative" : "absolute",
(do_dir) ? "directory" : "file",
(do_unicode) ? "unicode" : "ansi");
TEST_START(test_name);
/* cleanup / setup basic test structure */
_wsystem(L"RD /S /Q win32compat-tmp >NUL 2>&1");
_wsystem(L"MKDIR win32compat-tmp >NUL 2>&1");
wchar_t tgt_path[MAX_PATH] = L"";
wchar_t lnk_path[MAX_PATH] = L"";
/* prepend absolute path if doing absolute test */
if (do_absolute_tgt) {
wcscat(tgt_path, L"/");
wcscat(tgt_path, curdir);
wcscat(tgt_path, L"/");
}
if (do_absolute_lnk) {
wcscat(lnk_path, L"/");
wcscat(lnk_path, curdir);
wcscat(lnk_path, L"/");
}
/* append the test paths */
wcscat(tgt_path, L"win32compat-tmp/tgt");
wcscat(lnk_path, L"win32compat-tmp/lnk");
/* append unicode char if doing unicode test */
if (do_unicode) {
wcscat(tgt_path, L"Δ");
wcscat(lnk_path, L"Δ");
}
/* ensure target is in forward slash format since this is
* required for the readlink test output later *
*/
for (wchar_t * t = tgt_path; *t; t++) if (*t == '\\') *t = L'/';
/* create directory or file as target --- we have to offset
* the first forward slash so the windows functions operate
*/
if (do_dir)
CreateDirectoryW(&tgt_path[do_absolute_tgt], NULL);
else
CloseHandle(CreateFileW(&tgt_path[do_absolute_tgt],
GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL));
/* convert to utf8 for test */
char * tgt_utf8 = utf16_to_utf8(tgt_path);
char * lnk_utf8 = utf16_to_utf8(lnk_path);
/* for relative link, the target is relative to the link */
char * tgt_name_utf8 = tgt_utf8;
if (!do_absolute_tgt) tgt_name_utf8 = strrchr(tgt_utf8, '/') + 1;
/* create symlink */
int symlink_ret = symlink(tgt_name_utf8, lnk_utf8);
ASSERT_INT_EQ(symlink_ret, 0);
/* verify readlink() output against symlink() input */
char readlink_buf[MAX_PATH] = "";
int readlink_ret = readlink(lnk_utf8, readlink_buf, MAX_PATH);
ASSERT_INT_EQ(readlink_ret, strlen(tgt_name_utf8));
ASSERT_INT_EQ(0, memcmp(readlink_buf, tgt_name_utf8, readlink_ret));
/* verify lstat() gets the reference to the link */
struct w32_stat statbuf;
int lstat_ret = lstat(lnk_utf8, &statbuf);
ASSERT_INT_EQ(lstat_ret, 0);
ASSERT_INT_EQ(1, S_ISLNK(statbuf.st_mode));
/* verify stat() gets a reference to the dir or file */
int stat_ret = stat(lnk_utf8, &statbuf);
ASSERT_INT_EQ(stat_ret, 0);
ASSERT_INT_EQ(0, S_ISLNK(statbuf.st_mode));
ASSERT_INT_EQ(do_dir, S_ISDIR(statbuf.st_mode));
TEST_DONE();
}
_wsystem(L"RD /S /Q win32compat-tmp >NUL 2>&1");
}
void
file_tests()
{
@ -496,4 +610,5 @@ file_tests()
file_nonblocking_io_tests();
file_select_tests();
file_miscellaneous_tests();
file_symlink_tests();
}