Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions src/utils.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* SPDX-License-Identifier: (Apache-2.0 OR MIT)
*/
#include "utils.h"
#include <aclapi.h>
#include <accctrl.h>
#include <errhandlingapi.h>
#include <fileapi.h>
#include <cstdio>
Expand All @@ -12,6 +14,8 @@
#include <minwinbase.h>
#include <minwindef.h>
#include <processenv.h>
#include <processthreadsapi.h>
#include <securitybaseapi.h>
#include <stringapiset.h>
#include <strsafe.h>
#include <winbase.h>
Expand Down Expand Up @@ -926,6 +930,253 @@ char* findstr(char* search_str, const char* substr, size_t size) {
return nullptr;
}

ScopedSid FileSecurity::GetCurrentUserSid() {
HANDLE token_handle = nullptr;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
&token_handle)) {
return nullptr;
Comment thread
scheibelp marked this conversation as resolved.
}
std::unique_ptr<void, decltype(&::CloseHandle)> const scoped_token(
token_handle, &::CloseHandle);

DWORD buffer_size = 0;
::GetTokenInformation(token_handle, TokenUser, nullptr, 0, &buffer_size);

std::vector<char> buffer(buffer_size);
auto* token_user = reinterpret_cast<PTOKEN_USER>(buffer.data());

if (!::GetTokenInformation(token_handle, TokenUser, token_user, buffer_size,
&buffer_size)) {
return nullptr;
}

DWORD const sid_len = ::GetLengthSid(token_user->User.Sid);
void* sid_copy = std::malloc(sid_len);
if (sid_copy) {
::CopySid(sid_len, sid_copy, token_user->User.Sid);
return ScopedSid(sid_copy);
}
return nullptr;
}

/**
* Determines whether a given file has a given set of file permissions
* \param file to check permissions for
* \param access_mask type of permissions to check for
* \param sid user identifier to check for permissions in the context of
*
*/
bool FileSecurity::AclHasAccess(const std::wstring& file_path,
DWORD access_mask, PSID sid) {
PACL dacl = nullptr;
PSECURITY_DESCRIPTOR sd_raw = nullptr;
DWORD result = ::GetNamedSecurityInfoW(file_path.c_str(), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION, nullptr,
nullptr, &dacl, nullptr, &sd_raw);

if (result != ERROR_SUCCESS)
Comment thread
scheibelp marked this conversation as resolved.
return false;
ScopedLocalInfo const scoped_sd(sd_raw);

TRUSTEE_W trustee = {nullptr};
trustee.TrusteeForm = TRUSTEE_IS_SID;
trustee.TrusteeType = TRUSTEE_IS_USER;
trustee.ptstrName = static_cast<LPWSTR>(sid);

ACCESS_MASK effective_rights = 0;
result = ::GetEffectiveRightsFromAclW(dacl, &trustee, &effective_rights);

if (result != ERROR_SUCCESS)
return false;

if ((access_mask & GENERIC_WRITE) && (effective_rights & FILE_WRITE_DATA))
return true;
if ((access_mask & GENERIC_READ) && (effective_rights & FILE_READ_DATA))
return true;
if ((access_mask & GENERIC_ALL) && (effective_rights & FILE_ALL_ACCESS))
return true;

return (effective_rights & access_mask) == access_mask;
}

/**
* \param file_path path to the file for which we are granting a permission
* \param access_mask the bitmask for the ACE permissions being requested
* \param sid pointer to the security identifier for a given trustee (typically the current user)
* \param out_old_sid output param, pointer to variable containing pointer to the pre-modified DACL
* useful to determine whether the security descriptor has been modified and
* provide a baseline sid
*/
bool FileSecurity::AddAccessControlEntry(const std::wstring& file_path,
DWORD access_mask, PSID sid,
PSECURITY_DESCRIPTOR* out_old_sd) {
Comment thread
scheibelp marked this conversation as resolved.
PACL old_dacl = nullptr;
PSECURITY_DESCRIPTOR sd_raw = nullptr;

DWORD result = ::GetNamedSecurityInfoW(
file_path.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr,
nullptr, &old_dacl, nullptr, &sd_raw);

if (result != ERROR_SUCCESS)
return false;

if (out_old_sd)
*out_old_sd = sd_raw;
Comment thread
scheibelp marked this conversation as resolved.
ScopedLocalInfo const temp_sd_wrapper(out_old_sd ? nullptr : sd_raw);

EXPLICIT_ACCESS_W ea = {0};
ea.grfAccessPermissions = access_mask;
ea.grfAccessMode = GRANT_ACCESS;
ea.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
ea.Trustee.ptstrName = static_cast<LPWSTR>(sid);

PACL new_dacl = nullptr;
result = ::SetEntriesInAclW(1, &ea, old_dacl, &new_dacl);
if (result != ERROR_SUCCESS)
return false;

ScopedLocalInfo const scoped_new_dacl(new_dacl);
result = ::SetNamedSecurityInfoW(const_cast<LPWSTR>(file_path.c_str()),
SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, new_dacl, nullptr);
return (result == ERROR_SUCCESS);
}

/**
* Applies security descriptor to file
* \param file_path file to apply SD to
* \param sd security descriptor to apply to file
*/
bool FileSecurity::SetAclFromDescriptor(const std::wstring& file_path,
PSECURITY_DESCRIPTOR sd) {
if (!sd)
return false;
BOOL present = FALSE;
BOOL defaulted = FALSE;
PACL dacl = nullptr;
if (!::GetSecurityDescriptorDacl(sd, &present, &dacl, &defaulted) ||
!present || !dacl)
return false;

return ::SetNamedSecurityInfoW(const_cast<LPWSTR>(file_path.c_str()),
Comment thread
scheibelp marked this conversation as resolved.
SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
nullptr, nullptr, dacl,
nullptr) == ERROR_SUCCESS;
}

/**
* Obtain current file attributes for file indicated by file path
* \param file_path file to obtain permissions for
* \param out_attr output parameter to hold permissions
*/
bool FileSecurity::GetAttributes(const std::wstring& file_path,
DWORD* out_attr) {
DWORD const attr = ::GetFileAttributesW(file_path.c_str());
if (attr == INVALID_FILE_ATTRIBUTES)
return false;
if (out_attr)
*out_attr = attr;
return true;
}

/**
* Set file attribute attr on file indicated by file_path
* \param file_path file to set attributes for
* \param attr attributes to obtain
*/
bool FileSecurity::SetAttributes(const std::wstring& file_path, DWORD attr) {
return ::SetFileAttributesW(file_path.c_str(), attr) != 0;
}

/**
* Construct FileAccess object
* \param file_path file on which we want to obtain permissions
* \param desired_acess access control permissions we want to obtain for the file
*/
ScopedFileAccess::ScopedFileAccess(std::wstring file_path, DWORD desired_access)
: file_path_(std::move(file_path)),
desired_access_(desired_access),
original_sd_(nullptr),
current_user_sid_(nullptr),
acl_needs_revert_(false),
original_attributes_(0),
attributes_changed_(false) {
}

/**
* Obtain permissions established by constructor
*
*/
void ScopedFileAccess::Access() {


// We must ensure we have permissions *first* before we try to
// change the file attributes in Phase 2.

current_user_sid_ = FileSecurity::GetCurrentUserSid();
if (!current_user_sid_) {
throw std::system_error(static_cast<int>(::GetLastError()),
std::system_category(), "Failed to get SID");
}

// Check if we need to modify ACLs
if (!FileSecurity::AclHasAccess(file_path_, desired_access_,
current_user_sid_.get())) {
if (!FileSecurity::AddAccessControlEntry(file_path_, desired_access_,
current_user_sid_.get(),
&original_sd_)) {
throw std::system_error(static_cast<int>(::GetLastError()),
std::system_category(),
"Failed to grant ACL");
}
acl_needs_revert_ = true;
}

if (FileSecurity::GetAttributes(file_path_, &original_attributes_)) {
if (original_attributes_ & FILE_ATTRIBUTE_READONLY) {
// Remove the Read-Only bit
DWORD const new_attributes =
original_attributes_ & ~FILE_ATTRIBUTE_READONLY;

if (FileSecurity::SetAttributes(file_path_, new_attributes)) {
attributes_changed_ = true;
} else {
// If we fail to remove Read-Only, we might still fail to write later.
// We throw here to be safe and consistent.
throw std::system_error(static_cast<int>(::GetLastError()),
std::system_category(),
"Failed to remove Read-Only attribute");
}
}
} else {
throw std::system_error(static_cast<int>(::GetLastError()),
std::system_category(),
"Failed to get file attributes");
}
}
Comment thread
scheibelp marked this conversation as resolved.

ScopedFileAccess::~ScopedFileAccess() {
// We must restore attributes *before* we revert ACLs, because reverting ACLs
// might remove our permission to write attributes.
if (attributes_changed_) {
// We ignore errors in destructors to prevent termination
FileSecurity::SetAttributes(file_path_, original_attributes_);
}

if (acl_needs_revert_ && original_sd_) {
FileSecurity::SetAclFromDescriptor(file_path_, original_sd_);
::LocalFree(original_sd_);
}
}

bool ScopedFileAccess::IsAccessGranted() const {

return FileSecurity::AclHasAccess(file_path_, desired_access_,
current_user_sid_.get());
Comment thread
scheibelp marked this conversation as resolved.
}

NameTooLongError::NameTooLongError(char const* const message)
: std::runtime_error(message) {}

Expand Down
67 changes: 67 additions & 0 deletions src/utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,21 @@ int SafeHandleCleanup(HANDLE& handle);
// System Helpers //
std::string reportLastError();

struct LocalFreeDeleter {
void operator()(void* p) const {
if (p)
::LocalFree(p);
}
};

// Custom deleter for Standard C pointers.
struct FreeDeleter {
void operator()(void* p) const {
if (p)
std::free(p);
}
};

// Data helpers //

// Converts big endian data to little endian form
Expand Down Expand Up @@ -278,6 +293,58 @@ class LibraryFinder {
void EvalSearchPaths();
};

using ScopedLocalInfo = std::unique_ptr<void, LocalFreeDeleter>;

using ScopedSid = std::unique_ptr<void, FreeDeleter>;

class FileSecurity {
public:
FileSecurity() = delete;

static ScopedSid GetCurrentUserSid();

static bool AclHasAccess(const std::wstring& file_path, DWORD access_mask,
PSID sid);

static bool AddAccessControlEntry(const std::wstring& file_path,
DWORD access_mask, PSID sid,
PSECURITY_DESCRIPTOR* out_old_sd);

static bool SetAclFromDescriptor(const std::wstring& file_path,
PSECURITY_DESCRIPTOR sd);

// Retrieves file attributes (e.g., ReadOnly, Hidden).
// Returns false if the file cannot be accessed.
static bool GetAttributes(const std::wstring& file_path, DWORD* out_attr);

// Sets file attributes.
// Returns false if the operation fails.
static bool SetAttributes(const std::wstring& file_path, DWORD attr);
};

class ScopedFileAccess {
public:
explicit ScopedFileAccess(std::wstring file_path,
DWORD desired_access = GENERIC_WRITE);
~ScopedFileAccess();
void Access();

bool IsAccessGranted() const;

private:
std::wstring file_path_;
DWORD desired_access_;

// ACL State
PSECURITY_DESCRIPTOR original_sd_;
ScopedSid current_user_sid_;
bool acl_needs_revert_;

// Attribute State
DWORD original_attributes_;
bool attributes_changed_;
};

const std::map<char, char> special_character_to_path{{'|', '\\'}, {';', ':'}};

const std::map<char, char> path_to_special_characters{{'\\', '|'},
Expand Down
Loading