Skip to content

Commit 4f0a823

Browse files
authored
Utils: add access permissions (#26)
Some packages install themselves as readonly, or with other file permissions that conflict with what we're doing with this compiler wrapper. Thus, we need the ability to override (and restore) those settings. For this compiler wrapper to perform it's duties, it needs to be able to read and write the various binaries artifacts from a build. We thus need to be able to: 1. Obtain and store current permissions 2. Grant ourselves the required permissions 3. Do our reading/writing 4. Restore previous permissions On Windows, file access is handled by the read/write bit, as on other platforms, but also by Access Control Lists (ACLs) which are populated by Discretionary Access Control Lists (DACLs) which are the atomic units responsible for granting rights. This PR adds an interface to inspect, store, update, and restore the read/write bits and DACLs of a given file. This functionality is wrapped up in a RAII-based class (ScopedFileAccess) that essentially provides sufficient access permissions to a file for scope of the object. Note: this assumes the user driving this wrapper is the one who created the file, or that they have admin rights, otherwise this will fail (gracefully, but still a failure). --------- Signed-off-by: John Parent <john.parent@kitware.com>
1 parent c565d1d commit 4f0a823

2 files changed

Lines changed: 318 additions & 0 deletions

File tree

src/utils.cxx

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* SPDX-License-Identifier: (Apache-2.0 OR MIT)
55
*/
66
#include "utils.h"
7+
#include <aclapi.h>
8+
#include <accctrl.h>
79
#include <errhandlingapi.h>
810
#include <fileapi.h>
911
#include <cstdio>
@@ -12,6 +14,8 @@
1214
#include <minwinbase.h>
1315
#include <minwindef.h>
1416
#include <processenv.h>
17+
#include <processthreadsapi.h>
18+
#include <securitybaseapi.h>
1519
#include <stringapiset.h>
1620
#include <strsafe.h>
1721
#include <winbase.h>
@@ -926,6 +930,253 @@ char* findstr(char* search_str, const char* substr, size_t size) {
926930
return nullptr;
927931
}
928932

933+
ScopedSid FileSecurity::GetCurrentUserSid() {
934+
HANDLE token_handle = nullptr;
935+
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_QUERY,
936+
&token_handle)) {
937+
return nullptr;
938+
}
939+
std::unique_ptr<void, decltype(&::CloseHandle)> const scoped_token(
940+
token_handle, &::CloseHandle);
941+
942+
DWORD buffer_size = 0;
943+
::GetTokenInformation(token_handle, TokenUser, nullptr, 0, &buffer_size);
944+
945+
std::vector<char> buffer(buffer_size);
946+
auto* token_user = reinterpret_cast<PTOKEN_USER>(buffer.data());
947+
948+
if (!::GetTokenInformation(token_handle, TokenUser, token_user, buffer_size,
949+
&buffer_size)) {
950+
return nullptr;
951+
}
952+
953+
DWORD const sid_len = ::GetLengthSid(token_user->User.Sid);
954+
void* sid_copy = std::malloc(sid_len);
955+
if (sid_copy) {
956+
::CopySid(sid_len, sid_copy, token_user->User.Sid);
957+
return ScopedSid(sid_copy);
958+
}
959+
return nullptr;
960+
}
961+
962+
/**
963+
* Determines whether a given file has a given set of file permissions
964+
* \param file to check permissions for
965+
* \param access_mask type of permissions to check for
966+
* \param sid user identifier to check for permissions in the context of
967+
*
968+
*/
969+
bool FileSecurity::AclHasAccess(const std::wstring& file_path,
970+
DWORD access_mask, PSID sid) {
971+
PACL dacl = nullptr;
972+
PSECURITY_DESCRIPTOR sd_raw = nullptr;
973+
DWORD result = ::GetNamedSecurityInfoW(file_path.c_str(), SE_FILE_OBJECT,
974+
DACL_SECURITY_INFORMATION, nullptr,
975+
nullptr, &dacl, nullptr, &sd_raw);
976+
977+
if (result != ERROR_SUCCESS)
978+
return false;
979+
ScopedLocalInfo const scoped_sd(sd_raw);
980+
981+
TRUSTEE_W trustee = {nullptr};
982+
trustee.TrusteeForm = TRUSTEE_IS_SID;
983+
trustee.TrusteeType = TRUSTEE_IS_USER;
984+
trustee.ptstrName = static_cast<LPWSTR>(sid);
985+
986+
ACCESS_MASK effective_rights = 0;
987+
result = ::GetEffectiveRightsFromAclW(dacl, &trustee, &effective_rights);
988+
989+
if (result != ERROR_SUCCESS)
990+
return false;
991+
992+
if ((access_mask & GENERIC_WRITE) && (effective_rights & FILE_WRITE_DATA))
993+
return true;
994+
if ((access_mask & GENERIC_READ) && (effective_rights & FILE_READ_DATA))
995+
return true;
996+
if ((access_mask & GENERIC_ALL) && (effective_rights & FILE_ALL_ACCESS))
997+
return true;
998+
999+
return (effective_rights & access_mask) == access_mask;
1000+
}
1001+
1002+
/**
1003+
* \param file_path path to the file for which we are granting a permission
1004+
* \param access_mask the bitmask for the ACE permissions being requested
1005+
* \param sid pointer to the security identifier for a given trustee (typically the current user)
1006+
* \param out_old_sid output param, pointer to variable containing pointer to the pre-modified DACL
1007+
* useful to determine whether the security descriptor has been modified and
1008+
* provide a baseline sid
1009+
*/
1010+
bool FileSecurity::AddAccessControlEntry(const std::wstring& file_path,
1011+
DWORD access_mask, PSID sid,
1012+
PSECURITY_DESCRIPTOR* out_old_sd) {
1013+
PACL old_dacl = nullptr;
1014+
PSECURITY_DESCRIPTOR sd_raw = nullptr;
1015+
1016+
DWORD result = ::GetNamedSecurityInfoW(
1017+
file_path.c_str(), SE_FILE_OBJECT, DACL_SECURITY_INFORMATION, nullptr,
1018+
nullptr, &old_dacl, nullptr, &sd_raw);
1019+
1020+
if (result != ERROR_SUCCESS)
1021+
return false;
1022+
1023+
if (out_old_sd)
1024+
*out_old_sd = sd_raw;
1025+
ScopedLocalInfo const temp_sd_wrapper(out_old_sd ? nullptr : sd_raw);
1026+
1027+
EXPLICIT_ACCESS_W ea = {0};
1028+
ea.grfAccessPermissions = access_mask;
1029+
ea.grfAccessMode = GRANT_ACCESS;
1030+
ea.grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
1031+
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
1032+
ea.Trustee.TrusteeType = TRUSTEE_IS_USER;
1033+
ea.Trustee.ptstrName = static_cast<LPWSTR>(sid);
1034+
1035+
PACL new_dacl = nullptr;
1036+
result = ::SetEntriesInAclW(1, &ea, old_dacl, &new_dacl);
1037+
if (result != ERROR_SUCCESS)
1038+
return false;
1039+
1040+
ScopedLocalInfo const scoped_new_dacl(new_dacl);
1041+
result = ::SetNamedSecurityInfoW(const_cast<LPWSTR>(file_path.c_str()),
1042+
SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
1043+
nullptr, nullptr, new_dacl, nullptr);
1044+
return (result == ERROR_SUCCESS);
1045+
}
1046+
1047+
/**
1048+
* Applies security descriptor to file
1049+
* \param file_path file to apply SD to
1050+
* \param sd security descriptor to apply to file
1051+
*/
1052+
bool FileSecurity::SetAclFromDescriptor(const std::wstring& file_path,
1053+
PSECURITY_DESCRIPTOR sd) {
1054+
if (!sd)
1055+
return false;
1056+
BOOL present = FALSE;
1057+
BOOL defaulted = FALSE;
1058+
PACL dacl = nullptr;
1059+
if (!::GetSecurityDescriptorDacl(sd, &present, &dacl, &defaulted) ||
1060+
!present || !dacl)
1061+
return false;
1062+
1063+
return ::SetNamedSecurityInfoW(const_cast<LPWSTR>(file_path.c_str()),
1064+
SE_FILE_OBJECT, DACL_SECURITY_INFORMATION,
1065+
nullptr, nullptr, dacl,
1066+
nullptr) == ERROR_SUCCESS;
1067+
}
1068+
1069+
/**
1070+
* Obtain current file attributes for file indicated by file path
1071+
* \param file_path file to obtain permissions for
1072+
* \param out_attr output parameter to hold permissions
1073+
*/
1074+
bool FileSecurity::GetAttributes(const std::wstring& file_path,
1075+
DWORD* out_attr) {
1076+
DWORD const attr = ::GetFileAttributesW(file_path.c_str());
1077+
if (attr == INVALID_FILE_ATTRIBUTES)
1078+
return false;
1079+
if (out_attr)
1080+
*out_attr = attr;
1081+
return true;
1082+
}
1083+
1084+
/**
1085+
* Set file attribute attr on file indicated by file_path
1086+
* \param file_path file to set attributes for
1087+
* \param attr attributes to obtain
1088+
*/
1089+
bool FileSecurity::SetAttributes(const std::wstring& file_path, DWORD attr) {
1090+
return ::SetFileAttributesW(file_path.c_str(), attr) != 0;
1091+
}
1092+
1093+
/**
1094+
* Construct FileAccess object
1095+
* \param file_path file on which we want to obtain permissions
1096+
* \param desired_acess access control permissions we want to obtain for the file
1097+
*/
1098+
ScopedFileAccess::ScopedFileAccess(std::wstring file_path, DWORD desired_access)
1099+
: file_path_(std::move(file_path)),
1100+
desired_access_(desired_access),
1101+
original_sd_(nullptr),
1102+
current_user_sid_(nullptr),
1103+
acl_needs_revert_(false),
1104+
original_attributes_(0),
1105+
attributes_changed_(false) {
1106+
}
1107+
1108+
/**
1109+
* Obtain permissions established by constructor
1110+
*
1111+
*/
1112+
void ScopedFileAccess::Access() {
1113+
1114+
1115+
// We must ensure we have permissions *first* before we try to
1116+
// change the file attributes in Phase 2.
1117+
1118+
current_user_sid_ = FileSecurity::GetCurrentUserSid();
1119+
if (!current_user_sid_) {
1120+
throw std::system_error(static_cast<int>(::GetLastError()),
1121+
std::system_category(), "Failed to get SID");
1122+
}
1123+
1124+
// Check if we need to modify ACLs
1125+
if (!FileSecurity::AclHasAccess(file_path_, desired_access_,
1126+
current_user_sid_.get())) {
1127+
if (!FileSecurity::AddAccessControlEntry(file_path_, desired_access_,
1128+
current_user_sid_.get(),
1129+
&original_sd_)) {
1130+
throw std::system_error(static_cast<int>(::GetLastError()),
1131+
std::system_category(),
1132+
"Failed to grant ACL");
1133+
}
1134+
acl_needs_revert_ = true;
1135+
}
1136+
1137+
if (FileSecurity::GetAttributes(file_path_, &original_attributes_)) {
1138+
if (original_attributes_ & FILE_ATTRIBUTE_READONLY) {
1139+
// Remove the Read-Only bit
1140+
DWORD const new_attributes =
1141+
original_attributes_ & ~FILE_ATTRIBUTE_READONLY;
1142+
1143+
if (FileSecurity::SetAttributes(file_path_, new_attributes)) {
1144+
attributes_changed_ = true;
1145+
} else {
1146+
// If we fail to remove Read-Only, we might still fail to write later.
1147+
// We throw here to be safe and consistent.
1148+
throw std::system_error(static_cast<int>(::GetLastError()),
1149+
std::system_category(),
1150+
"Failed to remove Read-Only attribute");
1151+
}
1152+
}
1153+
} else {
1154+
throw std::system_error(static_cast<int>(::GetLastError()),
1155+
std::system_category(),
1156+
"Failed to get file attributes");
1157+
}
1158+
}
1159+
1160+
ScopedFileAccess::~ScopedFileAccess() {
1161+
// We must restore attributes *before* we revert ACLs, because reverting ACLs
1162+
// might remove our permission to write attributes.
1163+
if (attributes_changed_) {
1164+
// We ignore errors in destructors to prevent termination
1165+
FileSecurity::SetAttributes(file_path_, original_attributes_);
1166+
}
1167+
1168+
if (acl_needs_revert_ && original_sd_) {
1169+
FileSecurity::SetAclFromDescriptor(file_path_, original_sd_);
1170+
::LocalFree(original_sd_);
1171+
}
1172+
}
1173+
1174+
bool ScopedFileAccess::IsAccessGranted() const {
1175+
1176+
return FileSecurity::AclHasAccess(file_path_, desired_access_,
1177+
current_user_sid_.get());
1178+
}
1179+
9291180
NameTooLongError::NameTooLongError(char const* const message)
9301181
: std::runtime_error(message) {}
9311182

src/utils.h

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,21 @@ int SafeHandleCleanup(HANDLE& handle);
234234
// System Helpers //
235235
std::string reportLastError();
236236

237+
struct LocalFreeDeleter {
238+
void operator()(void* p) const {
239+
if (p)
240+
::LocalFree(p);
241+
}
242+
};
243+
244+
// Custom deleter for Standard C pointers.
245+
struct FreeDeleter {
246+
void operator()(void* p) const {
247+
if (p)
248+
std::free(p);
249+
}
250+
};
251+
237252
// Data helpers //
238253

239254
// Converts big endian data to little endian form
@@ -278,6 +293,58 @@ class LibraryFinder {
278293
void EvalSearchPaths();
279294
};
280295

296+
using ScopedLocalInfo = std::unique_ptr<void, LocalFreeDeleter>;
297+
298+
using ScopedSid = std::unique_ptr<void, FreeDeleter>;
299+
300+
class FileSecurity {
301+
public:
302+
FileSecurity() = delete;
303+
304+
static ScopedSid GetCurrentUserSid();
305+
306+
static bool AclHasAccess(const std::wstring& file_path, DWORD access_mask,
307+
PSID sid);
308+
309+
static bool AddAccessControlEntry(const std::wstring& file_path,
310+
DWORD access_mask, PSID sid,
311+
PSECURITY_DESCRIPTOR* out_old_sd);
312+
313+
static bool SetAclFromDescriptor(const std::wstring& file_path,
314+
PSECURITY_DESCRIPTOR sd);
315+
316+
// Retrieves file attributes (e.g., ReadOnly, Hidden).
317+
// Returns false if the file cannot be accessed.
318+
static bool GetAttributes(const std::wstring& file_path, DWORD* out_attr);
319+
320+
// Sets file attributes.
321+
// Returns false if the operation fails.
322+
static bool SetAttributes(const std::wstring& file_path, DWORD attr);
323+
};
324+
325+
class ScopedFileAccess {
326+
public:
327+
explicit ScopedFileAccess(std::wstring file_path,
328+
DWORD desired_access = GENERIC_WRITE);
329+
~ScopedFileAccess();
330+
void Access();
331+
332+
bool IsAccessGranted() const;
333+
334+
private:
335+
std::wstring file_path_;
336+
DWORD desired_access_;
337+
338+
// ACL State
339+
PSECURITY_DESCRIPTOR original_sd_;
340+
ScopedSid current_user_sid_;
341+
bool acl_needs_revert_;
342+
343+
// Attribute State
344+
DWORD original_attributes_;
345+
bool attributes_changed_;
346+
};
347+
281348
const std::map<char, char> special_character_to_path{{'|', '\\'}, {';', ':'}};
282349

283350
const std::map<char, char> path_to_special_characters{{'\\', '|'},

0 commit comments

Comments
 (0)