POSIX mode 到 Windows ACL 映射方案详解与代码示例
2025-03-19 02:20:28
POSIX mode
到 Windows ACL/FileAttribute 映射方案
项目要在 Windows 上实现 POSIX 层的,碰到了一个麻烦:怎么把 POSIX 的 mode
(创建/创建目录时的权限位)尽可能好地对应到 Windows 的访问控制机制(ACL/FileAttribute)上?毕竟,如果完全不能兼容,就有点难办了。
目前已知的是:
- UCRT (Universal C Runtime) 在
mkdir
里压根不支持mode
参数,这个参数直接从函数原型里拿掉了, 它创建目录时 ACL 参数永远是nullptr
。 - UCRT 的
open
函数,用SetFileAttributesA
处理mode
,但也只处理了READ_ONLY
这一种情况。
想弄清楚有没有现成的转换例子,或者,有没有一个比较合适的方法用 ACL/FileAttributes 来实现这个映射。 下面我们仔细聊聊。
一、 问题分析
POSIX 的 mode
和 Windows 的 ACL/FileAttribute 本质上都是为了控制文件和目录的访问权限,但它们的实现机制有很大区别。
- POSIX
mode
: 简单直接,用几个比特位表示用户、组和其他用户的读、写、执行权限。 - Windows ACL: 更复杂,更强大。它由一系列访问控制条目 (ACE) 组成,每个 ACE 指定一个用户或组的特定权限(读、写、执行、删除等等)。
- Windows FileAttribute: 主要用于控制文件的一些属性, 像只读, 隐藏, 系统文件等. 和 POSIX mode 不是一个概念, 但是某些方面有功能重叠, 可以结合使用.
直接一对一映射几乎不可能。 咱们的目标是“尽力而为”,在保证核心功能的前提下,尽可能模拟 POSIX 的行为。
二、 可行方案
2.1 方案一:简化映射(UCRT 的思路)
UCRT 的做法其实提供了一个思路:只处理最基本的情况。这种方案最简单,但功能也最有限。
-
原理:
mkdir
:直接忽略mode
参数,使用默认的 ACL。这意味着创建的目录会继承父目录的权限。creat
/open
:只处理只读属性。如果mode
包含写权限,则创建的文件可读写;否则,创建的文件只读。利用SetFileAttributesA
实现。
-
代码示例 (C++):
#include <Windows.h>
#include <io.h> // For _open, _mkdir
#include <fcntl.h> // For _O_CREAT, _O_RDWR, etc.
#include <sys/stat.h> // For _S_IREAD, _S_IWRITE
// 模拟 creat (简化版)
int my_creat(const char *pathname, int mode) {
int flags = _O_CREAT | _O_TRUNC | _O_WRONLY;
if ((mode & _S_IWRITE) == 0) {
flags = _O_CREAT | _O_TRUNC | _O_RDONLY;
}
return _open(pathname, flags, mode); // _open 在 Windows 上可以处理创建
}
int my_mkdir(const char *dirname, int /*mode*/) { //直接忽视 mode.
return _mkdir(dirname);
}
//open 函数处理 mode (UCRT SetFileAttributes 思路)
int my_open(const char* path_name, int flags, int mode) {
int fd = _open(path_name, flags, mode);
if (fd != -1 && (flags & _O_CREAT))
{
if ((mode & _S_IWRITE) == 0 ) {
//使用 SetFileAttributesA 设置文件为只读。
SetFileAttributesA(path_name, FILE_ATTRIBUTE_READONLY);
}
}
return fd;
}
-
安全性:
- 简单映射安全性相对可控,因为基本上沿用了 Windows 默认的安全机制。
- 需要仔细考虑是否允许创建可执行文件,防止潜在的安全风险。
-
进阶使用:
此方案适合对于安全性要求较高, 但是功能完整性要求较低的场景. 可以通过白名单方式仅允许创建几种特定 mode 的文件。
2.2 方案二:基于 SID 的 ACL 映射
这个方案尝试更细致地模拟 POSIX 的权限模型。核心思想是利用 Windows 的安全标识符 (SID) 来创建对应的 ACE。
-
原理:
- 获取用户和组 SID: 通过
GetUserName
和LookupAccountName
等 API 获取当前用户的 SID 以及相关组的 SID。 - 根据
mode
构建 ACE: 将 POSIX 的mode
拆解成用户、组和其他用户的权限,然后为每个类别创建对应的 ACE。例如:- 如果
mode
允许用户写,就创建一个允许当前用户 SID 进行写操作的 ACE。 - 如果
mode
允许组读,就创建一个允许当前用户主组 SID 进行读操作的 ACE。 - 如果
mode
允许其他用户执行,就创建一个允许 "Everyone" SID 进行执行操作的 ACE (需谨慎考虑安全性!)。
- 如果
- 创建 ACL: 将构建好的 ACE 组合成一个 ACL。
- 应用 ACL: 在创建文件/目录时,使用
CreateFile
/CreateDirectory
的SECURITY_ATTRIBUTES
参数,或者通过SetNamedSecurityInfo
应用 ACL。
- 获取用户和组 SID: 通过
-
代码示例 (C++):
#include <Windows.h>
#include <iostream>
#include <AclAPI.h>
#include <sddl.h> // 简化使用
bool setPermissions(const char *path, int mode) {
// 获取用户SID, 使用 SDDL 简化表达.
PSID pUserSid = NULL;
ConvertStringSidToSidA("S-1-5-11", &pUserSid); // "Authenticated Users"
// 可换为 "S-1-1-0"(Everyone)
if(!pUserSid){
std::cerr << "Failed to Get User SID,Error:" << GetLastError()<< std::endl;
return false;
}
//创建 DACL(discretionary access control list)
PACL pDacl = NULL;
EXPLICIT_ACCESS_A ea[3] = {0};
// User Permissions
ea[0].grfAccessPermissions = 0; // 先清 0.
if (mode & _S_IREAD) ea[0].grfAccessPermissions |= GENERIC_READ;
if (mode & _S_IWRITE) ea[0].grfAccessPermissions |= GENERIC_WRITE;
if (mode & _S_IEXEC) ea[0].grfAccessPermissions |= GENERIC_EXECUTE;
ea[0].grfAccessMode = SET_ACCESS; //直接设置, 不考虑 DENY 情况。
ea[0].grfInheritance = NO_INHERITANCE; //简化, 不考虑继承情况.
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
ea[0].Trustee.ptstrName = (LPSTR)pUserSid;
// Group Permissions, 简单起见, 和 User 用同一个 SID, 更严格的情况需要区分。
ea[1] = ea[0];
// Other persmissions. 简单处理, 全部不允许.
ea[2].grfAccessPermissions = 0;
ea[2].grfAccessMode = SET_ACCESS;
ea[2].grfInheritance = NO_INHERITANCE;
ea[2].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[2].Trustee.TrusteeType = TRUSTEE_IS_UNKNOWN; // 不设置, 表示不修改.
ea[2].Trustee.ptstrName = nullptr;
// 根据上面的设置生成 ACL.
DWORD dwRes = SetEntriesInAclA(3, ea, NULL, &pDacl);
if (ERROR_SUCCESS != dwRes) {
std::cerr << "SetEntriesInAclA Error: "<< dwRes <<std::endl;
LocalFree(pUserSid);
return false;
}
//应用 ACL
dwRes = SetNamedSecurityInfoA(
(LPSTR)path, // name of the object
SE_FILE_OBJECT, // type of object
DACL_SECURITY_INFORMATION, // change only the object's DACL
NULL, NULL, // do not change owner or group
pDacl, // DACL specified
NULL); // do not change SACL
if (ERROR_SUCCESS != dwRes) {
std::cerr << "SetNamedSecurityInfo Error: "<< dwRes <<std::endl;
LocalFree(pUserSid);
LocalFree(pDacl);
return false;
}
LocalFree(pUserSid);
LocalFree(pDacl);
return true;
}
// 包装 creat.
int my_creat_acl(const char *pathname, int mode)
{
HANDLE hFile = CreateFileA(pathname,
GENERIC_READ | GENERIC_WRITE, //需要有设置权限的权限
0, // no sharing
NULL, // default security attributes
CREATE_ALWAYS, // overwrite existing
FILE_ATTRIBUTE_NORMAL, // normal file
NULL);
if(hFile == INVALID_HANDLE_VALUE){
std::cerr << "Create File Error:"<< GetLastError()<< std::endl;
return -1;
}
// 创建成功, 设置 ACL。
if (!setPermissions(pathname, mode))
{
CloseHandle(hFile);
DeleteFileA(pathname);
return -1;
}
//将 Handle 转换为 fd, 这里仅作示例. 需要处理各种 flag。
int fd = _open_osfhandle((intptr_t)hFile, _O_RDWR);
return fd;
}
-
安全性:
- 这种方案比简化映射更灵活,但也更容易出错。
- 强烈建议不要轻易给 "Everyone" 或其他广泛的用户组授予写权限或执行权限。
- 仔细处理 SID 的获取和使用,防止权限提升漏洞。
-
进阶使用
可以增加 Owner/Group 映射功能, 让权限控制更加精细, 可以自定义安全符定义语言 (SDDL) 字符串,实现更复杂的 ACE 配置。 可以研究
ConvertStringSecurityDescriptorToSecurityDescriptor
等函数
2.3 方案三: 混合使用 FileAttribute 与 ACL
可以在方案二的基础上,进一步结合 FileAttribute
。 例如:
- POSIX 只读位(
S_IREAD
没有S_IWRITE
) , 可以通过设置FILE_ATTRIBUTE_READONLY
来实现, 简化 ACL 设置的复杂性. - 可执行文件属性, 可以根据实际需要通过
SetFileAttributes
或者 ACE 来控制。
这样可以充分利用两种机制, 实现最好的映射效果。
-
代码:
结合上述方案二的代码 和 方案一中SetFileAttributes
的部分. 根据具体情况进行调整。
这种方式更灵活. 需要做好错误处理。 -
安全: 与方案二类似。
-
进阶技巧: 可以进一步探索Windows的安全模型. 结合 mandatory integrity control (MIC) 等更高级的功能来实现安全控制.
三、 总结
上面提出了几种映射 POSIX mode
到 Windows ACL/FileAttribute 的方案,各有优缺点。
- 方案一 (简化映射) 简单, 安全, 但功能受限.
- 方案二 (基于 SID 的 ACL 映射) 更接近 POSIX 模型, 但复杂, 需要仔细考虑安全性.
- 方案三 (混合使用) 最灵活, 但是需要对两种机制都有一定了解。
实际开发中,要根据具体需求, 进行取舍, 选择一个合适的方案, 或者几个方案混合使用。 要记住,安全第一!