Skip to content

Instantly share code, notes, and snippets.

@lboulard
Last active October 30, 2024 06:51
Show Gist options
  • Save lboulard/988e3d77ea994a93bb8b18f32ff9a6ba to your computer and use it in GitHub Desktop.
Save lboulard/988e3d77ea994a93bb8b18f32ff9a6ba to your computer and use it in GitHub Desktop.
Symbolic link on Windows 10
# .clang-format
# Base style
BasedOnStyle: Google
# Indentation
IndentWidth: 4 # Use 4 spaces for indentation
TabWidth: 4 # Set tab width to 4 spaces
UseTab: Never # Always use spaces instead of tabs
# Brace Wrapping
BreakBeforeBraces: Attach # Attach opening brace to control statements and function declarations
# Function and Namespace Definitions
AllowShortFunctionsOnASingleLine: None # Force functions to break onto multiple lines
AlwaysBreakAfterDefinitionReturnType: All # Break after return type in function definitions
# Function Parameters and Arguments on Own Line
BinPackParameters: false # Force each parameter to be on a new line
BinPackArguments: false # Force each argument to be on a new line
PenaltyBreakBeforeFirstCallParameter: 1 # Encourage breaking arguments in function calls
PenaltyReturnTypeOnItsOwnLine: 0 # No penalty for placing the return type on its own line
# Force breaking after opening parenthesis if parameters do not fit on one line
BreakConstructorInitializers: BeforeComma # Break constructor initializer lists before each initializer
AlwaysBreakAfterReturnType: TopLevel # Break after return type if function signature exceeds line length
# Pointer and Reference Alignment
PointerAlignment: Left # Align pointers to the left (e.g., `int* ptr`)
# Column Limit
ColumnLimit: 99 # Set maximum column width to 99 characters
# Spaces
SpaceBeforeParens: ControlStatements # Only add space before parens in control statements (e.g., `if (cond)`)
SpaceBeforeCpp11BracedList: false # No space before `{` in C++11 list initialization
SpaceAfterCStyleCast: false # No space after C-style casts
# Alignment
AlignConsecutiveAssignments: true # Align assignments
AlignConsecutiveDeclarations: true # Align variable declarations
AlignAfterOpenBracket: BlockIndent # Indent parameters after open bracket by one block
AlignOperands: true # Align operands in expressions
# Formatting for Includes
SortIncludes: true # Sort include statements
IncludeBlocks: Preserve # Preserve empty lines between include blocks
# Comment Formatting
ReflowComments: false # Don't reflow long comments
# Empty Line Control
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1 # Limit empty lines between code sections
*~
*.bak
*.orig
*.sw[a-z]
\#*\#
.ninja*
*.exe
/t

Understanding how symbolic link work on Windows 10

Symbolic link creation requires usage of NTFS type file system partition and to have a permission from user security scope.

There are 3 behaviors based on computer configuration:

  1. Developer Mode: using a special flag to CreateSymbolicLinkW API
  2. Privileged permission to allow creation of symbolic link
  3. Not allowed at all (unless in group Administrators). Default.
  • Behavior 1 is “opt-in” per program.
  • Behavior 2 is “always permitted” for any programs when user owns privilege.
  • Behavior 3 is “never allowed” for any non-administrator user.

Differences with POSIX symbolic link

POSIX has unified symbolic link object. Symbolic link can target file, directories and any other file system objects. Target type is resolved at runtime. Usually by VFS (Virtual File System) layer in kernel, by enquiry of target file system object. POSIX does not mention VFS, but all UNIX© kernels have VFS.

NTFS, and hence Windows, have separated object for regular file and directory. API user shall declare object kind (file or directory) at creation time. Target type is resolved from symbolic link itself and inside NTFS layer. Contrary to POSIX, no proper VFS in Windows architecture is visible, even if a file system object cache exists since NT area.

A consequence on Windows is for many tools to require target existence to be able to create a symbolic link to this target. Even if Win32 API itself does not requires presence of target at creation time. Those tools can then decide if they create a file or directory symbolic link. And refuses to create symbolic link to other filesystem object types.

Developer Mode with special flag

References:

When user account has Developer Mode activated, add this flags when calling CreateSymbolicLinkW API:

SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE

API signature for CreateSymbolicLinkW:

BOOLEAN CreateSymbolicLinkW(
  [in] LPCWSTR lpSymlinkFileName,
  [in] LPCWSTR lpTargetFileName,
  [in] DWORD   dwFlags
);

Program will then be able to create symbolic link as long as there are running with user security scope.

Windows privileged permission for symbolic link

Named SeCreateSymbolicLinkPrivilege.

Because of Developer Mode in Windows 10 and later, not having this permission do not prevent an user to allow programs to create symbolic link.

Only tentative of symbolic link creation allows discovery of permission.

Reported error when symbolic link creation is forbidden

This error happens in those two cases:

  • No privilege / No Developer Mode
  • No privilege / Developer Mode / No special flag

Windows Win32 error ERROR_PRIVILEGE_NOT_HELD will be reported. For example:

Failed to create symbolic link.
Error(0x00000522): A required privilege is not held by the client.

Symbolic link creation should success in those cases for user account running program:

  • “Developer Mode” for user, and program use special flag in Windows API
  • Privileged permission SeCreateSymbolicLinkPrivilege

Beware, this is should, computer policies or NTFS partition configuration may still prevent symbolic link creation.

How to detect if symbolic creation is allowed

Currently (2024-10-30), checking if symbolic creation is allowed requires to call CreateSymbolicLinkW API and checks error result.

Note about symbolic link support in NTFS and permission

NTFS partition can be configured to limit symbolic link creation.

"This setting can be used in conjunction a symlink filesystem setting that can be manipulated with the command line utility to control the kinds of symlinks that are allowed on the machine. Type fsutil behavior set symlinkevaluation /? at the command line to get more information about fsutil and symbolic links."

# Compile with MinGW64
rule cc-unicode
command = g++ -Wall -o $out $in -municode -mconsole
build main-iostream.exe: cc-unicode main-iostream.cpp
build main-Win32-Console.exe: cc-unicode main-Win32-Console.cpp
build main.exe: cc-unicode main.cpp
#include <windows.h>
#include <iostream>
#include <string>
// Enable Unicode support in console
void
EnableUnicodeSupport() {
// Set console output code page to UTF-8
SetConsoleOutputCP(CP_UTF8);
// Set console input code page to UTF-8
SetConsoleCP(CP_UTF8);
}
int
wmain() {
EnableUnicodeSupport();
// Wide Unicode strings (UTF-16)
std::wstring hello = L"Hello, 世界!"; // "Hello, World!" in Chinese
// Output to console using wcout
std::wcout << hello << std::endl;
// Prompt user to enter a Unicode string
std::wcout << L"Enter a Unicode string: ";
std::wstring input;
std::getline(std::wcin, input);
// Output the entered string
std::wcout << L"You entered: " << input << std::endl;
return 0;
}
#include <windows.h>
#include <string>
#include <vector>
// Enable Unicode support in console
void
EnableUnicodeSupport() {
SetConsoleOutputCP(CP_UTF8); // Set console output code page to UTF-8
SetConsoleCP(CP_UTF8); // Set console input code page to UTF-8
}
// Function to write a Unicode string to the console
void
WriteUnicodeToConsole(const std::wstring &text) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if (hConsole == INVALID_HANDLE_VALUE) return;
DWORD written;
WriteConsoleW(hConsole, text.c_str(), text.length(), &written, NULL);
}
// Entry point for Unicode arguments
int
wmain(int argc, wchar_t *argv[]) {
EnableUnicodeSupport();
// Greet the user
WriteUnicodeToConsole(L"Hello, 世界!\n");
// Display all program arguments
WriteUnicodeToConsole(L"Program arguments:\n");
for (int i = 0; i < argc; ++i) {
std::wstring arg = L"Argument " + std::to_wstring(i) + L": " + argv[i] + L"\n";
WriteUnicodeToConsole(arg);
}
// Prompt the user to enter a Unicode string
WriteUnicodeToConsole(L"Enter a Unicode string: ");
wchar_t inputBuffer[256];
DWORD charsRead;
HANDLE hConsoleInput = GetStdHandle(STD_INPUT_HANDLE);
ReadConsoleW(hConsoleInput, inputBuffer, 256, &charsRead, NULL);
// Null-terminate the input string
inputBuffer[charsRead - 2] = L'\0'; // -2 to remove newline characters
// Output the entered string
std::wstring output = L"You entered: " + std::wstring(inputBuffer) + L"\n";
WriteUnicodeToConsole(output);
return 0;
}
#include <strsafe.h> // For StringCchCopyN
#include <windows.h>
#include <string>
#include <vector>
// Enable Unicode support in console
void
EnableUnicodeSupport() {
SetConsoleOutputCP(CP_UTF8); // Set console output code page to UTF-8
SetConsoleCP(CP_UTF8); // Set console input code page to UTF-8
}
void
WriteUnicodeToConsole(const std::wstring &text) {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if (hConsole == INVALID_HANDLE_VALUE) return;
DWORD written;
WriteConsoleW(hConsole, text.c_str(), text.length(), &written, NULL);
}
bool
IsDirectory(const std::wstring &path) {
DWORD attributes = GetFileAttributesW(path.c_str());
if (attributes == INVALID_FILE_ATTRIBUTES) {
return false; // Path doesn't exist or there was an error
}
return (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
}
std::wstring
GetErrorMessage(DWORD errorCode) {
wchar_t *errorMessage = nullptr;
FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
errorCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPWSTR)&errorMessage,
0,
NULL
);
std::wstring message = errorMessage ? errorMessage : L"Unknown error";
LocalFree(errorMessage); // Free the buffer allocated by FormatMessageW
return message;
}
#ifndef IO_REPARSE_TAG_SYMLINK
#define IO_REPARSE_TAG_SYMLINK (0xA000000CL)
#endif
#ifndef IO_REPARSE_TAG_MOUNT_POINT
#define IO_REPARSE_TAG_MOUNT_POINT (0xA0000003L)
#endif
typedef struct _REPARSE_DATA_BUFFER {
ULONG ReparseTag;
USHORT ReparseDataLength;
USHORT Reserved;
union {
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
ULONG Flags;
WCHAR PathBuffer[1];
} SymbolicLinkReparseBuffer;
struct {
USHORT SubstituteNameOffset;
USHORT SubstituteNameLength;
USHORT PrintNameOffset;
USHORT PrintNameLength;
WCHAR PathBuffer[1];
} MountPointReparseBuffer;
struct {
UCHAR DataBuffer[1];
} GenericReparseBuffer;
};
} REPARSE_DATA_BUFFER, *PREPARSE_DATA_BUFFER;
DWORD
GetSymlinkTarget(const std::wstring &linkPath, std::wstring &targetPath) {
HANDLE hFile = CreateFileW(
linkPath.c_str(),
GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS,
NULL
);
if (hFile == INVALID_HANDLE_VALUE) {
return GetLastError();
}
BYTE buffer[MAXIMUM_REPARSE_DATA_BUFFER_SIZE];
DWORD bytesReturned;
if (!DeviceIoControl(
hFile,
FSCTL_GET_REPARSE_POINT,
NULL,
0,
buffer,
MAXIMUM_REPARSE_DATA_BUFFER_SIZE,
&bytesReturned,
NULL
)) {
DWORD error = GetLastError();
CloseHandle(hFile);
return error;
}
CloseHandle(hFile);
// Extract the target path from the REPARSE_DATA_BUFFER structure
auto reparseData = (REPARSE_DATA_BUFFER *)buffer;
if (reparseData->ReparseTag == IO_REPARSE_TAG_SYMLINK ||
reparseData->ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) {
wchar_t tempPath[MAX_PATH];
int targetPathLength;
if (reparseData->ReparseTag == IO_REPARSE_TAG_SYMLINK) {
targetPathLength =
reparseData->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(wchar_t);
StringCchCopyN(
tempPath,
MAX_PATH,
reinterpret_cast<const wchar_t *>(
reparseData->SymbolicLinkReparseBuffer.PathBuffer +
reparseData->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)
),
targetPathLength
);
} else { // Junction point
targetPathLength =
reparseData->MountPointReparseBuffer.SubstituteNameLength / sizeof(wchar_t);
StringCchCopyN(
tempPath,
MAX_PATH,
reinterpret_cast<const wchar_t *>(
reparseData->MountPointReparseBuffer.PathBuffer +
reparseData->MountPointReparseBuffer.SubstituteNameOffset / sizeof(wchar_t)
),
targetPathLength
);
}
tempPath[targetPathLength] = L'\0';
targetPath = std::wstring(tempPath);
return ERROR_SUCCESS;
} else {
return ERROR_NOT_A_REPARSE_POINT;
}
}
DWORD
SymLink(std::wstring &linkPath, std::wstring &targetPath) {
DWORD flags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE;
if (IsDirectory(targetPath)) {
flags |= SYMBOLIC_LINK_FLAG_DIRECTORY;
}
if (!CreateSymbolicLinkW(linkPath.c_str(), targetPath.c_str(), flags)) {
return GetLastError();
}
return ERROR_SUCCESS;
}
int
wmain(int argc, wchar_t *argv[]) {
EnableUnicodeSupport();
// Greet the user
std::wstring hello = L"Hello, 世界!\n"; // "Hello, World!" in Chinese
WriteUnicodeToConsole(hello);
if (argc == 3) {
std::wstring linkPath(argv[1]);
std::wstring targetPath(argv[2]);
DWORD error = SymLink(linkPath, targetPath);
if (error == ERROR_SUCCESS) {
WriteUnicodeToConsole(L"Symlink created!\n");
{
std::wstring target;
DWORD result = GetSymlinkTarget(linkPath, target);
if (result == ERROR_SUCCESS) {
WriteUnicodeToConsole(L"Target path: " + targetPath + L"\n");
} else {
wchar_t hexResult[12];
StringCchPrintfW(
hexResult, sizeof(hexResult) / sizeof(hexResult[0]), L"0x%08X", error
);
WriteUnicodeToConsole(
L"Failed to read target.\n"
L"Error(" +
std::wstring(hexResult) + L": " + GetErrorMessage(result) + L"\n"
);
}
}
} else {
std::wstring errorMessage = GetErrorMessage(error);
wchar_t hexError[12];
StringCchPrintfW(hexError, sizeof(hexError) / sizeof(hexError[0]), L"0x%08X", error);
WriteUnicodeToConsole(
L"Failed to create symbolic link.\n"
L"Error(" +
std::wstring(hexError) + L"): " + errorMessage + L"\n"
);
if (error == ERROR_PRIVILEGE_NOT_HELD) {
WriteUnicodeToConsole(L"Ensure Developer Mode is enabled in Windows settings.\n");
}
}
} else {
WriteUnicodeToConsole(L"usage:" + std::wstring(argv[0]) + L" linkPath targetPath\n");
}
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment