Files
Justin Miller b272427baa [APITESTS][NTOS:PS][SDK][NTUSER] Job UI Restriction support (#9448)
Adds the initial attempt at handling the job ui restriction support. This doesn't protect the OS yet, however it provides everything we need to start implementing that protection.
2026-08-29 04:46:45 +00:00

401 lines
15 KiB
C

/*
* PROJECT: ReactOS API Tests
* LICENSE: MIT (https://spdx.org/licenses/MIT)
* PURPOSE: Tests for the job object UI restrictions
* COPYRIGHT: Copyright 2026 Justin Miller <justin.miller@reactos.org>
*/
#include "precomp.h"
/*
* Chromium 109, sandbox/win/src/job.cc. The JobLevel cases there fall through
* into each other, so each level takes the flags of the ones below it too:
* kLockdown adds the four here to kLimitedUser's, which comes to all eight.
*/
#define JOB_LIMITEDUSER_UI (JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | \
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS| \
JOB_OBJECT_UILIMIT_DESKTOP | \
JOB_OBJECT_UILIMIT_EXITWINDOWS)
#define JOB_LOCKDOWN_UI (JOB_LIMITEDUSER_UI | \
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | \
JOB_OBJECT_UILIMIT_READCLIPBOARD | \
JOB_OBJECT_UILIMIT_HANDLES | \
JOB_OBJECT_UILIMIT_GLOBALATOMS)
static const ULONG SingleRestrictions[] =
{
JOB_OBJECT_UILIMIT_HANDLES,
JOB_OBJECT_UILIMIT_READCLIPBOARD,
JOB_OBJECT_UILIMIT_WRITECLIPBOARD,
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS,
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS,
JOB_OBJECT_UILIMIT_GLOBALATOMS,
JOB_OBJECT_UILIMIT_DESKTOP,
JOB_OBJECT_UILIMIT_EXITWINDOWS,
};
static
BOOL
SetRestrictions(
_In_ HANDLE hJob,
_In_ ULONG Restrictions)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS Info;
Info.UIRestrictionsClass = Restrictions;
return SetInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info));
}
static
void
test_RoundTrip(void)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS Info;
HANDLE hJob;
DWORD Returned;
BOOL Success;
ULONG i;
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
/* A fresh job restricts nothing */
memset(&Info, 0xAA, sizeof(Info));
Returned = 0;
SetLastError(0xDEADBEEF);
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
&Returned);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, 0);
ok_long(Returned, sizeof(Info));
/* Every flag on its own, so one mistake cannot hide behind the others */
for (i = 0; i < _countof(SingleRestrictions); i++)
{
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, SingleRestrictions[i]);
ok(Success != FALSE, "Setting 0x%lx failed with %lu\n",
SingleRestrictions[i], GetLastError());
if (Success == FALSE)
{
/* Everything below only means something once it is set */
skip("0x%lx could not be set\n", SingleRestrictions[i]);
continue;
}
memset(&Info, 0xAA, sizeof(Info));
Returned = 0;
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
&Returned);
ok(Success != FALSE, "Querying 0x%lx failed with %lu\n",
SingleRestrictions[i], GetLastError());
if (Success == FALSE)
{
skip("0x%lx could not be queried\n", SingleRestrictions[i]);
}
else
{
ok_long(Info.UIRestrictionsClass, SingleRestrictions[i]);
ok_long(Returned, sizeof(Info));
}
/* Back to nothing, which drops the per-job state win32k keeps */
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, 0);
ok(Success != FALSE, "Clearing 0x%lx failed with %lu\n",
SingleRestrictions[i], GetLastError());
if (Success == FALSE)
{
/* The job stays restricted, so the round after this one would be
starting from the wrong place */
skip("0x%lx could not be cleared\n", SingleRestrictions[i]);
continue;
}
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
if (Success != FALSE)
ok_long(Info.UIRestrictionsClass, 0);
}
/* All of them at once */
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_OBJECT_UILIMIT_ALL);
ok(Success != FALSE, "Setting JOB_OBJECT_UILIMIT_ALL failed with %lu\n", GetLastError());
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, JOB_OBJECT_UILIMIT_ALL);
/* Setting the same value twice is not an error */
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_OBJECT_UILIMIT_ALL);
ok(Success != FALSE, "Setting the same restrictions again failed with %lu\n",
GetLastError());
/* Close it while still restricted, to exercise the delete path */
CloseHandle(hJob);
}
static
void
test_InvalidParameters(void)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS Info;
HANDLE hJob;
DWORD Returned;
BOOL Success;
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
/* Undefined bits must be rejected, and must not be stored */
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, 0xDEAD0000);
ok(Success == FALSE, "Setting undefined restrictions succeeded\n");
ok_err(ERROR_INVALID_PARAMETER);
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_OBJECT_UILIMIT_ALL | 0x100);
ok(Success == FALSE, "Setting one undefined bit succeeded\n");
ok_err(ERROR_INVALID_PARAMETER);
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, 0);
/* The class is fixed length in both directions */
Info.UIRestrictionsClass = JOB_OBJECT_UILIMIT_HANDLES;
SetLastError(0xDEADBEEF);
Success = SetInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info) - 1);
ok(Success == FALSE, "SetInformationJobObject with a short buffer succeeded\n");
ok_err(ERROR_BAD_LENGTH);
SetLastError(0xDEADBEEF);
Success = SetInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info) + 1);
ok(Success == FALSE, "SetInformationJobObject with a long buffer succeeded\n");
ok_err(ERROR_BAD_LENGTH);
Returned = 0;
SetLastError(0xDEADBEEF);
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info) - 1,
&Returned);
ok(Success == FALSE, "QueryInformationJobObject with a short buffer succeeded\n");
ok_err(ERROR_BAD_LENGTH);
/* An inaccessible buffer must be reported, not raised */
SetLastError(0xDEADBEEF);
Success = SetInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
NULL,
sizeof(Info));
ok(Success == FALSE, "SetInformationJobObject with a NULL buffer succeeded\n");
ok_err(ERROR_NOACCESS);
CloseHandle(hJob);
}
/* A handle without JOB_OBJECT_SET_ATTRIBUTES may not change the restrictions */
static
void
test_Access(void)
{
JOBOBJECT_BASIC_UI_RESTRICTIONS Info;
HANDLE hJob, hQueryOnly;
BOOL Success;
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
hQueryOnly = NULL;
Success = DuplicateHandle(GetCurrentProcess(),
hJob,
GetCurrentProcess(),
&hQueryOnly,
JOB_OBJECT_QUERY,
FALSE,
0);
ok(Success != FALSE, "DuplicateHandle failed with %lu\n", GetLastError());
if (Success)
{
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hQueryOnly, JOB_OBJECT_UILIMIT_HANDLES);
ok(Success == FALSE, "Setting restrictions through a query handle succeeded\n");
ok_err(ERROR_ACCESS_DENIED);
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hQueryOnly,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, 0);
CloseHandle(hQueryOnly);
}
CloseHandle(hJob);
}
/*
* The two policies the Chromium sandbox builds, in the order it builds them.
* The GPU one excepts every UI restriction away again, which has to end up
* clearing them rather than failing.
*/
static
void
test_SandboxPolicies(void)
{
JOBOBJECT_EXTENDED_LIMIT_INFORMATION ExtendedLimit;
JOBOBJECT_BASIC_UI_RESTRICTIONS Info;
HANDLE hJob;
BOOL Success;
/* sandbox::Job::Init(JOB_LOCKDOWN) */
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
memset(&ExtendedLimit, 0, sizeof(ExtendedLimit));
ExtendedLimit.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION |
JOB_OBJECT_LIMIT_ACTIVE_PROCESS |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
ExtendedLimit.BasicLimitInformation.ActiveProcessLimit = 1;
SetLastError(0xDEADBEEF);
Success = SetInformationJobObject(hJob,
JobObjectExtendedLimitInformation,
&ExtendedLimit,
sizeof(ExtendedLimit));
ok(Success != FALSE, "Setting the lockdown limits failed with %lu\n", GetLastError());
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_LOCKDOWN_UI);
ok(Success != FALSE, "Setting the lockdown restrictions failed with %lu\n",
GetLastError());
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, JOB_LOCKDOWN_UI);
CloseHandle(hJob);
/* Job::Init(JobLevel::kLimitedUser), which does not take kLockdown's four */
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
memset(&ExtendedLimit, 0, sizeof(ExtendedLimit));
ExtendedLimit.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_ACTIVE_PROCESS |
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
ExtendedLimit.BasicLimitInformation.ActiveProcessLimit = 1;
SetLastError(0xDEADBEEF);
Success = SetInformationJobObject(hJob,
JobObjectExtendedLimitInformation,
&ExtendedLimit,
sizeof(ExtendedLimit));
ok(Success != FALSE, "Setting the limited user limits failed with %lu\n", GetLastError());
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_LIMITEDUSER_UI);
ok(Success != FALSE, "Setting the limited user restrictions failed with %lu\n",
GetLastError());
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, JOB_LIMITEDUSER_UI);
CloseHandle(hJob);
/*
* job.cc applies the caller's exceptions as
* jbur.UIRestrictionsClass &= ~ui_exceptions before its single
* SetInformationJobObject, so a delegate that excepts everything away asks
* for a mask of zero. That has to be accepted, not refused.
*/
hJob = CreateJobObjectW(NULL, NULL);
ok(hJob != NULL, "CreateJobObject failed with %lu\n", GetLastError());
if (hJob == NULL)
return;
SetLastError(0xDEADBEEF);
Success = SetRestrictions(hJob, JOB_LOCKDOWN_UI & ~JOB_OBJECT_UILIMIT_ALL);
ok(Success != FALSE, "Excepting every restriction away failed with %lu\n",
GetLastError());
memset(&Info, 0xAA, sizeof(Info));
Success = QueryInformationJobObject(hJob,
JobObjectBasicUIRestrictions,
&Info,
sizeof(Info),
NULL);
ok(Success != FALSE, "QueryInformationJobObject failed with %lu\n", GetLastError());
ok_long(Info.UIRestrictionsClass, 0);
CloseHandle(hJob);
}
START_TEST(JobObject)
{
test_RoundTrip();
test_InvalidParameters();
test_Access();
test_SandboxPolicies();
}