Skip to content

Commit 1e764da

Browse files
authored
Merge pull request #27 from xenu/xenu/win32-symlink
implement a function checking whether symlink creation is permitted
2 parents 33a7cee + 8e18726 commit 1e764da

File tree

4 files changed

+247
-0
lines changed

4 files changed

+247
-0
lines changed

Makefile.PL

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ my %param = (
1111
NAME => 'Win32',
1212
VERSION_FROM => 'Win32.pm',
1313
INSTALLDIRS => ($] >= 5.008004 && $] < 5.012 ? 'perl' : 'site'),
14+
PREREQ_PM => {
15+
strict => 0,
16+
warnings => 0,
17+
vars => 0,
18+
Exporter => 0,
19+
DynaLoader => 0
20+
}
1421
);
1522
$param{NO_META} = 1 if eval "$ExtUtils::MakeMaker::VERSION" >= 6.10_03;
1623

@@ -21,4 +28,11 @@ else {
2128
$param{LIBS} = ['-luserenv']
2229
}
2330

31+
my $test_requires = $ExtUtils::MakeMaker::VERSION >= 6.64
32+
? 'TEST_REQUIRES'
33+
: 'PREREQ_PM';
34+
35+
$param{$test_requires}{'Test'} = 0;
36+
$param{$test_requires}{'File::Temp'} = 0;
37+
2438
WriteMakefile(%param);

Win32.pm

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,6 +713,27 @@ sub _GetOSName {
713713
return ("Win$os", $desc);
714714
}
715715

716+
sub IsSymlinkCreationAllowed {
717+
my(undef, $major, $minor, $build) = GetOSVersion();
718+
719+
# Vista was the first Windows version with symlink support
720+
return !!0 if $major < 6;
721+
722+
# Since Windows 10 1703, enabling the developer mode allows to create
723+
# symlinks regardless of process privileges
724+
if ($major > 10 || ($major == 10 && ($minor > 0 || $build > 15063))) {
725+
return !!1 if IsDeveloperModeEnabled();
726+
}
727+
728+
my $privs = GetProcessPrivileges();
729+
730+
return !!0 unless $privs;
731+
732+
# It doesn't matter if the permission is enabled or not, it just has to
733+
# exist. CreateSymbolicLink() will automatically enable it when needed.
734+
return exists $privs->{SeCreateSymbolicLinkPrivilege};
735+
}
736+
716737
# "no warnings 'redefine';" doesn't work for 5.8.7 and earlier
717738
local $^W = 0;
718739
bootstrap Win32;
@@ -1233,6 +1254,25 @@ information about what you can do with this address has been lost in
12331254
the mist of time. Use the Win32::API module instead of this deprecated
12341255
function.
12351256
1257+
=item Win32::GetProcessPrivileges([PID])
1258+
1259+
Returns a reference to a hash holding the information about the privileges
1260+
held by the specified process. The keys are privilege names, and the values
1261+
are booleans indicating whether a given privilege is currently enabled or not.
1262+
1263+
If the optional PID parameter is omitted, the function queries the current
1264+
process.
1265+
1266+
Example return value:
1267+
1268+
{
1269+
SeTimeZonePrivilege => 0,
1270+
SeShutdownPrivilege => 0,
1271+
SeUndockPrivilege => 0,
1272+
SeIncreaseWorkingSetPrivilege => 0,
1273+
SeChangeNotifyPrivilege => 1
1274+
}
1275+
12361276
=item Win32::GetProductInfo(OSMAJOR, OSMINOR, SPMAJOR, SPMINOR)
12371277
12381278
Retrieves the product type for the operating system on the local
@@ -1285,6 +1325,17 @@ actually running with elevated privileges. Returns C<undef>
12851325
and prints a warning if an error occurred. This function always
12861326
returns 1 on Win9X.
12871327
1328+
=item Win32::IsDeveloperModeEnabled()
1329+
1330+
Returns true if the developer mode is currently enabled. It always returns
1331+
false on Windows versions older than Windows 10.
1332+
1333+
=item Win32::IsSymlinkCreationAllowed()
1334+
1335+
Returns true if the current process is allowed to create symbolic links. This
1336+
function is a convenience wrapper around Win32::GetProcessPrivileges() and
1337+
Win32::IsDeveloperModeEnabled().
1338+
12881339
=item Win32::IsWinNT()
12891340
12901341
[CORE] Returns non zero if the Win32 subsystem is Windows NT.

Win32.xs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ typedef int (__stdcall *PFNDllUnregisterServer)(void);
3030
typedef BOOL (__stdcall *PFNIsUserAnAdmin)(void);
3131
typedef BOOL (WINAPI *PFNGetProductInfo)(DWORD, DWORD, DWORD, DWORD, DWORD*);
3232
typedef void (WINAPI *PFNGetNativeSystemInfo)(LPSYSTEM_INFO lpSystemInfo);
33+
typedef LONG (*PFNRegGetValueA)(HKEY, LPCSTR, LPCSTR, DWORD, LPDWORD, PVOID, LPDWORD);
3334

3435
#ifndef CSIDL_MYMUSIC
3536
# define CSIDL_MYMUSIC 0x000D
@@ -1557,6 +1558,130 @@ XS(w32_SetConsoleOutputCP)
15571558
XSRETURN_IV(SetConsoleOutputCP((int)SvIV(ST(0))));
15581559
}
15591560

1561+
XS(w32_GetProcessPrivileges)
1562+
{
1563+
dXSARGS;
1564+
BOOL ret;
1565+
HV *priv_hv;
1566+
HANDLE proc_handle, token;
1567+
char *priv_name = NULL;
1568+
TOKEN_PRIVILEGES *privs = NULL;
1569+
DWORD i, pid, priv_name_len = 100, privs_len = 300;
1570+
1571+
if (items > 1)
1572+
Perl_croak(aTHX_ "usage: Win32::GetProcessPrivileges([$pid])");
1573+
1574+
if (items == 0) {
1575+
EXTEND(SP, 1);
1576+
pid = GetCurrentProcessId();
1577+
}
1578+
else {
1579+
pid = (DWORD)SvUV(ST(0));
1580+
}
1581+
1582+
proc_handle = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, pid);
1583+
1584+
if (!proc_handle)
1585+
XSRETURN_NO;
1586+
1587+
ret = OpenProcessToken(proc_handle, TOKEN_QUERY, &token);
1588+
CloseHandle(proc_handle);
1589+
1590+
if (!ret)
1591+
XSRETURN_NO;
1592+
1593+
do {
1594+
Renewc(privs, privs_len, char, TOKEN_PRIVILEGES);
1595+
ret = GetTokenInformation(
1596+
token, TokenPrivileges, privs, privs_len, &privs_len
1597+
);
1598+
} while (!ret && GetLastError() == ERROR_INSUFFICIENT_BUFFER);
1599+
1600+
CloseHandle(token);
1601+
1602+
if (!ret) {
1603+
Safefree(privs);
1604+
XSRETURN_NO;
1605+
}
1606+
1607+
priv_hv = newHV();
1608+
New(0, priv_name, priv_name_len, char);
1609+
1610+
for (i = 0; i < privs->PrivilegeCount; ++i) {
1611+
DWORD ret_len = 0;
1612+
LUID_AND_ATTRIBUTES *priv = &privs->Privileges[i];
1613+
BOOL is_enabled = !!(priv->Attributes & SE_PRIVILEGE_ENABLED);
1614+
1615+
if (priv->Attributes & SE_PRIVILEGE_REMOVED)
1616+
continue;
1617+
1618+
do {
1619+
ret_len = priv_name_len;
1620+
ret = LookupPrivilegeNameA(
1621+
NULL, &priv->Luid, priv_name, &ret_len
1622+
);
1623+
1624+
if (ret_len > priv_name_len) {
1625+
priv_name_len = ret_len + 1;
1626+
Renew(priv_name, priv_name_len, char);
1627+
}
1628+
} while (!ret && GetLastError() == ERROR_INSUFFICIENT_BUFFER);
1629+
1630+
if (!ret) {
1631+
SvREFCNT_dec((SV*)priv_hv);
1632+
Safefree(privs);
1633+
Safefree(priv_name);
1634+
XSRETURN_NO;
1635+
}
1636+
1637+
hv_store(priv_hv, priv_name, ret_len, newSViv(is_enabled), 0);
1638+
}
1639+
1640+
Safefree(privs);
1641+
Safefree(priv_name);
1642+
1643+
ST(0) = sv_2mortal(newRV_noinc((SV*)priv_hv));
1644+
XSRETURN(1);
1645+
}
1646+
1647+
XS(w32_IsDeveloperModeEnabled)
1648+
{
1649+
dXSARGS;
1650+
LONG status;
1651+
DWORD val, val_size = sizeof(val);
1652+
PFNRegGetValueA pfnRegGetValueA;
1653+
HMODULE module;
1654+
1655+
if (items)
1656+
Perl_croak(aTHX_ "usage: Win32::IsDeveloperModeEnabled()");
1657+
1658+
EXTEND(SP, 1);
1659+
1660+
/* developer mode was introduced in Windows 10 */
1661+
if (g_osver.dwMajorVersion < 10)
1662+
XSRETURN_NO;
1663+
1664+
module = GetModuleHandleA("advapi32.dll");
1665+
GETPROC(RegGetValueA);
1666+
if (!pfnRegGetValueA)
1667+
XSRETURN_NO;
1668+
1669+
status = pfnRegGetValueA(
1670+
HKEY_LOCAL_MACHINE,
1671+
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock",
1672+
"AllowDevelopmentWithoutDevLicense",
1673+
RRF_RT_REG_DWORD | KEY_WOW64_64KEY,
1674+
NULL,
1675+
&val,
1676+
&val_size
1677+
);
1678+
1679+
if (status == ERROR_SUCCESS && val == 1)
1680+
XSRETURN_YES;
1681+
1682+
XSRETURN_NO;
1683+
}
1684+
15601685
MODULE = Win32 PACKAGE = Win32
15611686

15621687
PROTOTYPES: DISABLE
@@ -1626,6 +1751,8 @@ BOOT:
16261751
newXS("Win32::GetOEMCP", w32_GetOEMCP, file);
16271752
newXS("Win32::SetConsoleCP", w32_SetConsoleCP, file);
16281753
newXS("Win32::SetConsoleOutputCP", w32_SetConsoleOutputCP, file);
1754+
newXS("Win32::GetProcessPrivileges", w32_GetProcessPrivileges, file);
1755+
newXS("Win32::IsDeveloperModeEnabled", w32_IsDeveloperModeEnabled, file);
16291756
#ifdef __CYGWIN__
16301757
newXS("Win32::SetChildShowWindow", w32_SetChildShowWindow, file);
16311758
#endif

t/Privileges.t

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use strict;
2+
use warnings;
3+
4+
use Test;
5+
use Win32;
6+
use Config;
7+
use File::Temp;
8+
9+
plan tests => 7;
10+
11+
ok(ref(Win32::GetProcessPrivileges()) eq 'HASH');
12+
ok(ref(Win32::GetProcessPrivileges(Win32::GetCurrentProcessId())) eq 'HASH');
13+
14+
# All Windows PIDs are divisible by 4. It's an undocumented implementation
15+
# detail, but it means it's extremely unlikely that the PID below is valid.
16+
ok(!Win32::GetProcessPrivileges(3423237));
17+
18+
my $whoami = `whoami /priv 2>&1`;
19+
my $skip = ($? == -1 || $? >> 8) ? '"whoami" command is missing' : 0;
20+
21+
skip($skip, sub{
22+
my $privs = Win32::GetProcessPrivileges();
23+
24+
while ($whoami =~ /^(Se\w+)/mg) {
25+
return 0 unless exists $privs->{$1};
26+
}
27+
28+
return 1;
29+
});
30+
31+
# there isn't really anything to test, we just want to make sure that the
32+
# function doesn't segfault
33+
Win32::IsDeveloperModeEnabled();
34+
ok(1);
35+
36+
Win32::IsSymlinkCreationAllowed();
37+
ok(1);
38+
39+
$skip = $^O ne 'MSWin32' ? 'MSWin32-only test' : 0;
40+
$skip ||= !$Config{d_symlink} ? 'this perl doesn\'t have symlink()' : 0;
41+
42+
skip($skip, sub {
43+
my $tmpdir = File::Temp->newdir;
44+
my $dirname = $tmpdir->dirname;
45+
46+
if (Win32::IsSymlinkCreationAllowed()) {
47+
# we expect success
48+
return symlink("foo", $tmpdir->dirname . "/new_symlink") == 1;
49+
}
50+
else {
51+
# we expect failure
52+
return symlink("foo", $tmpdir->dirname . "/new_symlink") == 0;
53+
}
54+
});
55+

0 commit comments

Comments
 (0)