Skip to content

Commit 794bfdf

Browse files
committed
win,fs: Add more complete fs__chmod() implementation
This implementation of `fs__chmod()` maps the familiar `owner`, `group`, `other` triplet of permissions within a POSIX `mode` parameter to ACL entries involving the current user, the current user's primary group as well as any groups the user may belong to that already have ACL entries within the given file object, and the `Everyone` group. We create new ACL entries explicitly allowing and denying the relevant permissions for each of these security entities, and apply the new ACL to the given file object. The `chmod()` method continues to set the readonly bit upon the given file when no write permissions are specified for any user.
1 parent 1dcf324 commit 794bfdf

File tree

1 file changed

+307
-5
lines changed

1 file changed

+307
-5
lines changed

src/win/fs.c

+307-5
Original file line numberDiff line numberDiff line change
@@ -2248,13 +2248,315 @@ static void fs__access(uv_fs_t* req) {
22482248
SET_REQ_SUCCESS(req);
22492249
}
22502250

2251+
static void build_access_struct(EXPLICIT_ACCESS_W* ea, PSID owner,
2252+
TRUSTEE_TYPE user_type, mode_t mode_triplet,
2253+
ACCESS_MODE allow_deny) {
2254+
/*
2255+
* We map the typical POSIX mode bits r/w/x as the Windows
2256+
* FILE_GENERIC_{READ,WRITE,EXECUTE} permissions with a little bit of of extra permissions
2257+
* added on, to deal with directories and win32 idiosyncrasies.
2258+
*/
2259+
ZeroMemory(ea, sizeof(EXPLICIT_ACCESS_W));
2260+
2261+
/*
2262+
* Initialize two EXLPICIT_ACCESS structures; one to explicitly allow things, the
2263+
* other to explicitly deny them. We leave no middle ground for inheritance to mess
2264+
* things up.
2265+
*/
2266+
ea->grfAccessPermissions = 0;
2267+
ea->grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
2268+
ea->Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;
2269+
ea->Trustee.TrusteeForm = TRUSTEE_IS_SID;
2270+
ea->Trustee.TrusteeType = user_type;
2271+
ea->Trustee.ptstrName = owner;
2272+
2273+
ea->grfAccessMode = allow_deny;
2274+
2275+
/*
2276+
* We would like to use FILE_GENERIC_* for everything, but unfortunately:
2277+
*
2278+
* - This does not include the rights for a directory to delete its children,
2279+
* so we include that manually with the "write" permission by including the
2280+
* FILE_ADD_SUBDIRECTORY and FILE_DELETE_CHILD permissions.
2281+
* - All FILE_GENERIC_* defines share the SYNCHRONIZE permission, which means
2282+
* that if we deny FILE_GENERIC_WRITE but allow FILE_GENERIC_READ, that one
2283+
* permission will still be denied. We work around this by only denying the
2284+
* SYNCHRONIZE permission if read is not allowed, allowing it otherwise.
2285+
* - We want to be able to set things as read-only even after the ACL has been
2286+
* set, so we never give up the FILE_WRITE_ATTRIBUTES permission, unless we're
2287+
* actually being set to 0o000.
2288+
*/
2289+
2290+
if (mode_triplet & 0x1) {
2291+
ea->grfAccessPermissions |= STANDARD_RIGHTS_EXECUTE | FILE_READ_ATTRIBUTES | FILE_EXECUTE;
2292+
if (allow_deny == GRANT_ACCESS) {
2293+
ea->grfAccessPermissions |= SYNCHRONIZE | FILE_WRITE_ATTRIBUTES;
2294+
}
2295+
}
2296+
2297+
if (mode_triplet & 0x2) {
2298+
ea->grfAccessPermissions |= STANDARD_RIGHTS_WRITE | FILE_WRITE_DATA | FILE_WRITE_EA | FILE_APPEND_DATA | FILE_ADD_SUBDIRECTORY | FILE_DELETE_CHILD;
2299+
if (allow_deny == GRANT_ACCESS) {
2300+
ea->grfAccessPermissions |= SYNCHRONIZE | FILE_WRITE_ATTRIBUTES;
2301+
}
2302+
}
2303+
2304+
if (mode_triplet & 0x4) {
2305+
ea->grfAccessPermissions |= FILE_GENERIC_READ | FILE_WRITE_ATTRIBUTES;
2306+
}
2307+
}
22512308

22522309
static void fs__chmod(uv_fs_t* req) {
2253-
int result = _wchmod(req->file.pathw, req->fs.info.mode);
2254-
if (result == -1)
2255-
SET_REQ_WIN32_ERROR(req, _doserrno);
2256-
else
2257-
SET_REQ_RESULT(req, 0);
2310+
PACL pOldDACL = NULL, pNewDACL = NULL;
2311+
PSID psidOwner = NULL, psidGroup = NULL, psidEveryone = NULL,
2312+
psidNull = NULL, psidCreatorGroup = NULL;
2313+
PSECURITY_DESCRIPTOR pSD = NULL;
2314+
PEXPLICIT_ACCESS_W ea = NULL, pOldEAs = NULL;
2315+
SECURITY_INFORMATION si = NULL;
2316+
DWORD numGroups = 0, tokenAccess = 0, u_mode = 0, g_mode = 0, o_mode = 0,
2317+
u_deny_mode = 0, g_deny_mode = 0, attr = 0, new_attr = 0;
2318+
HANDLE hToken = NULL, hImpersonatedToken = NULL;
2319+
ULONG numOldEAs = 0, numNewEAs = 0, numOtherGroups = 0,
2320+
ea_idx = 0, ea_write_idx = 0;
2321+
2322+
/* Create well-known SIDs for various global groups */
2323+
SID_IDENTIFIER_AUTHORITY SIDAuthWorld = SECURITY_WORLD_SID_AUTHORITY;
2324+
SID_IDENTIFIER_AUTHORITY SIDAuthNull = SECURITY_NULL_SID_AUTHORITY;
2325+
SID_IDENTIFIER_AUTHORITY SIDAuthCreator = SECURITY_CREATOR_SID_AUTHORITY;
2326+
2327+
if (!AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID,
2328+
0, 0, 0, 0, 0, 0, 0, &psidEveryone) ||
2329+
!AllocateAndInitializeSid(&SIDAuthNull, 1, SECURITY_NULL_RID,
2330+
0, 0, 0, 0, 0, 0, 0, &psidNull) ||
2331+
!AllocateAndInitializeSid(&SIDAuthCreator, 1, SECURITY_CREATOR_GROUP_RID,
2332+
0, 0, 0, 0, 0, 0, 0, &psidCreatorGroup) ||
2333+
!AllocateAndInitializeSid(&SIDAuthWorld, 1, SECURITY_WORLD_RID,
2334+
0, 0, 0, 0, 0, 0, 0, &psidEveryone)) {
2335+
SET_REQ_WIN32_ERROR(req, GetLastError());
2336+
goto chmod_cleanup;
2337+
}
2338+
2339+
/* Get the old DACL so that we can merge into it */
2340+
si = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION |
2341+
DACL_SECURITY_INFORMATION;
2342+
if (ERROR_SUCCESS != GetNamedSecurityInfoW(req->file.pathw, SE_FILE_OBJECT,
2343+
si, &psidOwner, &psidGroup,
2344+
&pOldDACL, NULL, &pSD)) {
2345+
SET_REQ_WIN32_ERROR(req, GetLastError());
2346+
goto chmod_cleanup;
2347+
}
2348+
2349+
/* Extract EAs from old DACL */
2350+
if (ERROR_SUCCESS != GetExplicitEntriesFromAclW(pOldDACL, &numOldEAs,
2351+
&pOldEAs)) {
2352+
SET_REQ_WIN32_ERROR(req, GetLastError());
2353+
goto chmod_cleanup;
2354+
}
2355+
2356+
/*
2357+
* Work around Win32 bug where GetExplicitEntriesFromAclW() fails on newly-created files;
2358+
* We fix it by forcibly clearing some kind of cache by setting the security info with the
2359+
* old DACL, then attempting to read it in again.
2360+
*/
2361+
if (numOldEAs != pOldDACL->AceCount) {
2362+
if (ERROR_SUCCESS != SetNamedSecurityInfoW(
2363+
req->file.pathw,
2364+
SE_FILE_OBJECT,
2365+
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
2366+
NULL, NULL, pOldDACL, NULL)) {
2367+
SET_REQ_WIN32_ERROR(req, GetLastError());
2368+
goto chmod_cleanup;
2369+
}
2370+
if (pSD != NULL) {
2371+
LocalFree(pSD);
2372+
pSD = NULL;
2373+
}
2374+
if (ERROR_SUCCESS != GetNamedSecurityInfoW(req->file.pathw, SE_FILE_OBJECT,
2375+
si, &psidOwner, &psidGroup,
2376+
&pOldDACL, NULL, &pSD)) {
2377+
SET_REQ_WIN32_ERROR(req, GetLastError());
2378+
goto chmod_cleanup;
2379+
}
2380+
if (pOldEAs != NULL) {
2381+
LocalFree(pOldEAs);
2382+
pOldEAs = NULL;
2383+
}
2384+
if (ERROR_SUCCESS != GetExplicitEntriesFromAclW(pOldDACL, &numOldEAs,
2385+
&pOldEAs)) {
2386+
SET_REQ_WIN32_ERROR(req, GetLastError());
2387+
goto chmod_cleanup;
2388+
}
2389+
}
2390+
2391+
/* If the file does not contain a group owner, we will use the user's 'Creator Group ID' instead */
2392+
if (EqualSid(psidGroup, psidNull)) {
2393+
psidGroup = psidCreatorGroup;
2394+
}
2395+
2396+
/*
2397+
* We next need to scan all groups that the current user "belongs" to, in order to
2398+
* set the permissions for those groups to be the same as the "group" bit; so first
2399+
* we collect a list of group PSIDs:
2400+
*/
2401+
tokenAccess = TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_DUPLICATE |
2402+
STANDARD_RIGHTS_READ;
2403+
if (!OpenProcessToken(GetCurrentProcess(), tokenAccess, &hToken )) {
2404+
SET_REQ_WIN32_ERROR(req, GetLastError());
2405+
goto chmod_cleanup;
2406+
}
2407+
if (!DuplicateToken(hToken, SecurityImpersonation, &hImpersonatedToken)) {
2408+
SET_REQ_WIN32_ERROR(req, GetLastError());
2409+
goto chmod_cleanup;
2410+
}
2411+
2412+
/* Iterate over all old ACEs, looking for groups that we belong to */
2413+
for (ea_idx=0; ea_idx<numOldEAs; ++ea_idx) {
2414+
BOOL isMember = FALSE;
2415+
PSID pEASid = (PSID)pOldEAs[ea_idx].Trustee.ptstrName;
2416+
/* Skip this EA if it isn't an SID, or it is "Everyone" or our actual group */
2417+
if (pOldEAs[ea_idx].Trustee.TrusteeForm != TRUSTEE_IS_SID ||
2418+
EqualSid(pEASid, psidEveryone) ||
2419+
EqualSid(pEASid, psidGroup)) {
2420+
continue;
2421+
}
2422+
2423+
/* Check to see if our user is a member of this group */
2424+
if (!CheckTokenMembership(hImpersonatedToken, pEASid, &isMember)) {
2425+
SET_REQ_WIN32_ERROR(req, GetLastError());
2426+
goto chmod_cleanup;
2427+
}
2428+
2429+
/* If we're a member, then count it */
2430+
if (isMember) {
2431+
numOtherGroups++;
2432+
}
2433+
}
2434+
2435+
/* Create an ACE for each triplet (user, group, other) */
2436+
numNewEAs = 8 + 3*numOtherGroups;
2437+
ea = (PEXPLICIT_ACCESS_W) malloc(sizeof(EXPLICIT_ACCESS_W)*numNewEAs);
2438+
u_mode = ((req->fs.info.mode & S_IRWXU) >> 6);
2439+
g_mode = ((req->fs.info.mode & S_IRWXG) >> 3);
2440+
o_mode = ((req->fs.info.mode & S_IRWXO) >> 0);
2441+
2442+
/* We start by revoking previous permissions for trustees we care about */
2443+
build_access_struct(&ea[0], psidOwner, TRUSTEE_IS_USER, 0, REVOKE_ACCESS);
2444+
build_access_struct(&ea[1], psidGroup, TRUSTEE_IS_GROUP, 0, REVOKE_ACCESS);
2445+
build_access_struct(&ea[2], psidEveryone, TRUSTEE_IS_GROUP, 0, REVOKE_ACCESS);
2446+
2447+
/*
2448+
* We also add explicit denies to user and group if the user shouldn't have
2449+
* a permission but the group or everyone can, for instance.
2450+
*/
2451+
u_deny_mode = (~u_mode) & (g_mode | o_mode);
2452+
g_deny_mode = (~g_mode) & o_mode;
2453+
build_access_struct(&ea[3], psidOwner, TRUSTEE_IS_USER, u_deny_mode, DENY_ACCESS);
2454+
build_access_struct(&ea[4], psidGroup, TRUSTEE_IS_GROUP, g_deny_mode, DENY_ACCESS);
2455+
2456+
/* Next, add explicit allows for (owner, group, other) */
2457+
build_access_struct(&ea[5], psidOwner, TRUSTEE_IS_USER, u_mode, SET_ACCESS);
2458+
build_access_struct(&ea[6], psidGroup, TRUSTEE_IS_GROUP, g_mode, SET_ACCESS);
2459+
build_access_struct(&ea[7], psidEveryone, TRUSTEE_IS_GROUP, o_mode, SET_ACCESS);
2460+
2461+
/*
2462+
* Iterate over all old ACEs, looking for groups that we belong to, and setting
2463+
* the appropriate access bits for those groups (as g_mode)
2464+
*/
2465+
ea_write_idx = 8;
2466+
for (ea_idx=0; ea_idx<numOldEAs; ++ea_idx) {
2467+
BOOL isMember = FALSE;
2468+
PSID pEASid = (PSID)pOldEAs[ea_idx].Trustee.ptstrName;
2469+
/* Skip this EA if it isn't an SID, or it is "Everyone" or our actual group */
2470+
if (pOldEAs[ea_idx].Trustee.TrusteeForm != TRUSTEE_IS_SID ||
2471+
EqualSid(pEASid, psidEveryone) ||
2472+
EqualSid(pEASid, psidGroup)) {
2473+
continue;
2474+
}
2475+
2476+
/* Check to see if our user is a member of this group */
2477+
if (!CheckTokenMembership(hImpersonatedToken, pEASid, &isMember)) {
2478+
SET_REQ_WIN32_ERROR(req, GetLastError());
2479+
goto chmod_cleanup;
2480+
}
2481+
2482+
/*
2483+
* If we're a member, then count it. We limit our `ea_write_idx` to avoid
2484+
* the unlikely event that we have been added to a group since we first
2485+
* calculated `numOtherGroups`.
2486+
*/
2487+
if (isMember && ea_write_idx < numNewEAs) {
2488+
build_access_struct(&ea[ea_write_idx], pEASid, TRUSTEE_IS_GROUP, 0, REVOKE_ACCESS);
2489+
build_access_struct(&ea[ea_write_idx + 1], pEASid, TRUSTEE_IS_GROUP, g_deny_mode, DENY_ACCESS);
2490+
build_access_struct(&ea[ea_write_idx + 2], pEASid, TRUSTEE_IS_GROUP, g_mode, SET_ACCESS);
2491+
ea_write_idx += 3;
2492+
}
2493+
}
2494+
2495+
/* Set entries in the ACL object */
2496+
if (ERROR_SUCCESS != SetEntriesInAclW(numNewEAs, &ea[0], pOldDACL, &pNewDACL)) {
2497+
SET_REQ_WIN32_ERROR(req, GetLastError());
2498+
goto chmod_cleanup;
2499+
}
2500+
2501+
/* If none of the write bits are set, we want to mark the file as read-only.
2502+
* Alternatively, if it was marked as read-only, unmark it if we have at least
2503+
* one writable group set. */
2504+
attr = GetFileAttributesW(req->file.pathw);
2505+
if (attr == INVALID_FILE_ATTRIBUTES) {
2506+
SET_REQ_WIN32_ERROR(req, GetLastError());
2507+
goto chmod_cleanup;
2508+
}
2509+
new_attr = attr;
2510+
if ((req->fs.info.mode & (S_IWUSR | S_IWGRP | S_IWOTH)) == 0) {
2511+
new_attr |= FILE_ATTRIBUTE_READONLY;
2512+
}
2513+
if ((req->fs.info.mode & (S_IWUSR | S_IWGRP | S_IWOTH)) != 0) {
2514+
new_attr &= ~FILE_ATTRIBUTE_READONLY;
2515+
}
2516+
2517+
/*
2518+
* Now we actually do the setting. We only call SetFileAttributes() if the
2519+
* attributes have actually changed.
2520+
*/
2521+
if (new_attr != attr) {
2522+
if (!SetFileAttributesW(req->file.pathw, new_attr)) {
2523+
SET_REQ_WIN32_ERROR(req, GetLastError());
2524+
goto chmod_cleanup;
2525+
}
2526+
}
2527+
if (ERROR_SUCCESS != SetNamedSecurityInfoW(
2528+
req->file.pathw,
2529+
SE_FILE_OBJECT,
2530+
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
2531+
NULL, NULL, pNewDACL, NULL)) {
2532+
SET_REQ_WIN32_ERROR(req, GetLastError());
2533+
goto chmod_cleanup;
2534+
}
2535+
2536+
SET_REQ_SUCCESS(req);
2537+
2538+
chmod_cleanup:
2539+
if (pSD != NULL) {
2540+
LocalFree(pSD);
2541+
}
2542+
if (pNewDACL != NULL) {
2543+
LocalFree(pNewDACL);
2544+
}
2545+
if (psidEveryone != NULL) {
2546+
FreeSid(psidEveryone);
2547+
}
2548+
if (psidNull != NULL) {
2549+
FreeSid(psidNull);
2550+
}
2551+
if (psidCreatorGroup != NULL) {
2552+
FreeSid(psidCreatorGroup);
2553+
}
2554+
if (pOldEAs != NULL) {
2555+
LocalFree(pOldEAs);
2556+
}
2557+
if (ea != NULL) {
2558+
free(ea);
2559+
}
22582560
}
22592561

22602562

0 commit comments

Comments
 (0)