After creating a job, you will typically want to set up the sandbox (set restrictions) on what processes within the job can do. You can place several different types of restrictions on a job:
You place restrictions on a job by calling the following:
BOOL SetInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, PVOID pJobObjectInformation, DWORD cbJobObjectInformationLength); |
The first parameter identifies the job you want to restrict. The second parameter is an enumerated type and indicates the type of restriction you want to apply. The third parameter is the address of a data structure containing the restriction settings, and the fourth parameter indicates the size of this structure (used for versioning). The following table summarizes how to set restrictions.
Limit Type | Value of Second Parameter | Structure of Third Parameter |
---|---|---|
Basic limit | JobObjectBasicLimitInformation | JOBOBJECT_BASIC_ LIMIT_INFORMATION |
Extended basic limit | JobObjectExtendedLimitInformation | JOBOBJECT_EXTENDED_LIMIT_INFORMATION |
Basic UI restrictions | JobObjectBasicUIRestrictions | JOBOBJECT_BASIC_UI_RESTRICTIONS |
Security limit | JobObjectSecurityLimitInformation | JOBOBJECT_SECURITY_LIMIT_INFORMATION |
In my StartRestrictedProcess function, I set only some basic restrictions on the job. I allocated a JOB_OBJECT_BASIC_LIMIT_INFORMATION structure, initialized it, and then called SetInformationJobObject. A JOB_OBJECT_ BASIC_LIMIT_INFORMATION structure looks like this:
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION { LARGE_INTEGER PerProcessUserTimeLimit; LARGE_INTEGER PerJobUserTimeLimit; DWORD LimitFlags; DWORD MinimumWorkingSetSize; DWORD MaximumWorkingSetSize; DWORD ActiveProcessLimit; DWORD_PTR Affinity; DWORD PriorityClass; DWORD SchedulingClass; } JOBOBJECT_BASIC_LIMIT_INFORMATION, *PJOBOBJECT_BASIC_LIMIT_INFORMATION; |
Table 5-1 briefly describes the members.
Table 5-1. JOBOBJECT_BASIC_LIMIT_INFORMATION members
Member | Description | Notes |
---|---|---|
PerProcessUser-TimeLimit | Specifies the maximum user mode time allotted to each process (in 100 ns intervals). | The system automatically terminates any process that uses more than its allotted time. To set this limit, specify the JOB_OBJECT_LIMIT_ PROCESS_TIME flag in the LimitFlags member. |
PerJobUser-TimeLimit | Specifies how much more user-mode time the processes in this job can use (in 100 ns intervals). | By default, the system automatically terminates all processes when this time limit is reached. You can change this value periodically as the job runs. To set this limit, specify the JOB_OBJECT_LIMIT_JOB_TIME flag in the LimitFlags member. |
LimitFlags | Indicates which restrictions to apply to the job. | See the section that follows this table for more information. |
MinimumWorkingSetSize/ MaximumWorkingSetSize | Specifies the minimum and maximum working set size for each process (not for all processes within the job). | Normally, a process's working set can grow above its maximum; setting MaximumWorkingSetSize forces a hard limit. Once the process's working set reaches this limit, the process pages against itself. Calls to SetProcessWorkingSetSize by an individual process are ignored unless the process is just trying to empty its working set. To set this limit, specify the JOB_OBJECT_ LIMIT_WORKINGSET flag in the LimitFlags member. |
ActiveProcessLimit | Specifies the maximum number of processes that can run concurrently in the job. | Any attempt to go over this limit causes the new process to be terminated with a "not enough quota" error. To set this limit, specify the JOB_OBJECT_ LIMIT_ACTIVE_PROCESS flag in the LimitFlags member. |
Affinity | Specifies the subset of the CPU(s) that can run the processes. | Individual processes can limit this even further. To set this limit, specify the JOB_OBJECT_ LIMIT_AFFINITY flag in the LimitFlags member. |
PriorityClass | Specifies the priority class used by all processes. | If a process calls SetPriorityClass, the call will return successfully even though it actually fails. If the process calls GetPriorityClass, the function returns what the process has set the priority class to even though this might not be process's actual priority class. In addition, SetThreadPriority fails to raise threads above normal priority but can be used to lower a thread's priority. To set this limit, specify the JOB_OBJECT_LIMIT_PRIORITY_CLASS flag in the LimitFlags member. |
SchedulingClass | Specifies a relative time quantum difference assigned to threads in the job. | Value can be from 0 to 9 inclusive; 5 is the default. See the text after this table for more information. To set this limit, specify theJOB_OBJECT_LIMIT_SCHEDULING_CLASS flag in the LimitFlags member. |
I'd like to explain a few things about this structure that I don't think are clear in the Platform SDK documentation. You set bits in the LimitFlags member to indicate the restrictions you want applied to the job. For example, in my StartRestrictedProcess function, I set the JOB_OBJECT_LIMIT_ PRIORITY_CLASS and JOB_OBJECT_LIMIT_JOB_TIME bits. This means that these are the only two restrictions that I place on the job. I impose no restrictions on CPU affinity, working set size, per-process CPU time, and so on.
As the job runs, it maintains accounting information—such as how much CPU time the processes in the job have used. Each time you set the basic limit using the JOB_OBJECT_LIMIT_JOB_TIME flag, the job subtracts the CPU time accounting information for processes that have terminated. This shows you how much CPU time is used by the currently active processes. But what if you want to change the affinity of the job but not reset the CPU time accounting information? To do this, you have to set a new basic limit using the JOB_ OBJECT_LIMIT_AFFINITY flag, and you have to leave off the JOB_OBJECT_ LIMIT_JOB_TIME flag. But by doing this, you tell the job that you no longer want to enforce a CPU time restriction. This is not what you want.
What you want is to change the affinity restriction and keep the existing CPU time restriction; you just don't want the CPU time accounting information for the terminated processes to be subtracted. To solve this problem, use a special flag: JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME. This flag and the JOB_ OBJECT_LIMIT_JOB_TIME flag are mutually exclusive. The JOB_OBJECT_ LIMIT_PRESERVE_JOB_TIME flag indicates that you want to change the restrictions without subtracting the CPU time accounting information for the terminated processes.
We should also talk about the JOBOBJECT_BASIC_LIMIT_INFORMATION structure's SchedulingClass member. Imagine that you have two jobs running and you set the priority class of both jobs to NORMAL_PRIORITY_CLASS. But you also want processes in one job to get more CPU time than processes in the other job. You can use the SchedulingClass member to change the relative scheduling of jobs that have the same priority class. You can set a value between 0 and 9, inclusive; 5 is the default. On Windows 2000, a higher value tells the system to give a longer time quantum to threads in processes in a particular job; a lower value reduces the threads' time quantum.
For example, let's say that I have two normal priority class jobs. Each job contains one process, and each process has just one (normal priority) thread. Under ordinary circumstances, these two threads would be scheduled in a round-robin fashion and each would get the same time quantum. However, if we set the SchedulingClass member of the first job to 3, when threads in this job are scheduled CPU time, their quantum is shorter than for threads that are in the second job.
If you use the SchedulingClass member, you should avoid using large numbers and hence larger time quantums because larger time quantums reduce the overall responsiveness of the other jobs, processes, and threads in the system. Also, I have just described what happens on Windows 2000. Microsoft plans to make more significant changes to the thread scheduler in future versions of Windows because it recognizes a need for the operating system to offer a wider range of thread scheduling scenarios to jobs, processes, and threads.
One last limit that deserves special mention is the JOB_OBJECT_LIMIT_ DIE_ON_UNHANDLED_EXCEPTION limit flag. This limit causes the system to turn off the "unhandled exception" dialog box for each process associated with the job. The system does this by calling the SetErrorMode function, passing it the SEM_NOGPFAULTERRORBOX flag for each process in the job. A process in a job that raises an unhandled exception is immediately terminated without any user interface being displayed. This is a useful limit flag for services and other batch-oriented jobs. Without it, a process in a job can raise an exception and never terminate, thereby wasting system resources.
In addition to the basic limits, you can set extended limits on a job using the JOBOBJECT_EXTENDED_LIMIT_INFORMATION structure:
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION { JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; IO_COUNTERS IoInfo; SIZE_T ProcessMemoryLimit; SIZE_T JobMemoryLimit; SIZE_T PeakProcessMemoryUsed; SIZE_T PeakJobMemoryUsed; } JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION; |
As you can see, this structure contains a JOBOBJECT_BASIC_LIMIT_ INFORMATION structure, which makes it a superset of the basic limits. This structure is a little strange because it includes members that have nothing to do with setting limits on a job. First, the IoInfo member is reserved; you should not access it in any way. I'll discuss how you can query I/O counter information later in the chapter. In addition, the PeakProcessMemoryUsed and PeakJobMemoryUsed members are read-only and tell you the maximum amount of committed storage that has been required for any one process and for all processes within the job, respectively.
The two remaining members, ProcessMemoryLimit and JobMemoryLimit, restrict the amount of committed storage used by any one process or by all processes in the job, respectively. To set either of these limits, you specify the JOB_OBJECT_LIMIT_JOB_MEMORY and the JOB_OBJECT_LIMIT_PROCESS_MEMORY flags in the LimitFlags member, respectively.
Now let's turn our attention back to other restrictions that you can place on a job. A JOBOBJECT_BASIC_UI_RESTRICTIONS structure looks like this:
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS { DWORD UIRestrictionsClass; } JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS; |
This structure has only one data member, UIRestrictionsClass, which holds a set of bit flags briefly described in Table 5-2.
Table 5-2. Bit flags for basic user-interface restrictions for a job object
Flag | Description |
---|---|
JOB_OBJECT_UILIMIT_EXITWINDOWS | Prevents processes from logging off, shutting down, rebooting, or powering off the system via the ExitWindowsEx function |
JOB_OBJECT_UILIMIT_READCLIPBOARD | Prevents processes from reading the clipboard |
JOB_OBJECT_UILIMIT_WRITECLIPBOARD | Prevents processes from erasing the clipboard |
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS | Prevents processes from changing system parameters via the SystemParametersInfo function |
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS | Prevents processes from changing the display settings via the ChangeDisplaySettings function |
JOB_OBJECT_UILIMIT_GLOBALATOMS | Gives the job its own global atom table and restricts processes in the job to accessing only the job's table |
JOB_OBJECT_UILIMIT_DESKTOP | Prevents processes from creating or switching desktops using the CreateDesktop or SwitchDesktop function |
JOB_OBJECT_UILIMIT_HANDLES | Prevents processes in a job from using USER objects (such as HWNDs) created by processes outside the same job |
The last flag, JOB_OBJECT_UILIMIT_HANDLES, is particularly interesting. This restriction means that no processes in the job can access USER objects created by processes outside the job. So if you try to run Microsoft Spy++ inside a job, you won't see any windows except the windows that Spy++ itself creates. Figure 5-2 shows Spy++ with two MDI child windows open. Notice that the Threads 1 window contains a list of threads in the system. Only one of those threads, 000006AC SPYXX, seems to have created any windows. This is because I ran Spy++ in its own job and restricted its use of UI handles. In the same window, you can see the MSDEV and EXPLORER threads, but it appears that they have not created any windows. I assure you that these threads have definitely created windows, but Spy++ cannot access them. On the right side, you see the Windows 3 window, in which Spy++ shows the hierarchy of all windows existing on the desktop. Notice that there is only one entry, 00000000. Spy++ must just put this here as a placeholder.
Note that this UI restriction is only one-way. That is, processes outside of a job can see USER objects created by processes within a job. For example, if I run Notepad in a job and Spy++ outside of a job, Spy++ can see Notepad's window even if the job that Notepad is in specifies the JOB_OBJECT_ UILIMIT_HANDLES flag. Also, if Spy++ is in its own job, it can also see Notepad's window unless the job has the JOB_OBJECT_UILIMIT_HANDLES flag specified.
The restricting of UI handles is awesome if you want to create a really secure sandbox for your job's processes to play in. However, it is useful to have a process that is part of a job communicate with a process outside of the job.
Figure 5-2. Microsoft Spy++ running in a job that restricts access to UI handles
One easy way to accomplish this is to use window messages, but if the job's processes can't access UI handles, a process in the job can't send or post a window message to a window created by a process outside the job. Fortunately, you can solve this problem using a new function:
BOOL UserHandleGrantAccess( HANDLE hUserObj, HANDLE hjob, BOOL fGrant); |
The hUserObj parameter indicates a single USER object whose access you want to grant or deny to processes within the job. This is almost always a window handle, but it can be another USER object, such as a desktop, hook, icon, or menu. The last two parameters, hjob and fGrant, indicate which job you are granting or denying access to. Note that this function fails if it is called from a process within the job identified by hjob—this prevents a process within a job from simply granting itself access to an object.
The last type of restriction that you place on a job is related to security. (Note that once applied, security restrictions cannot be revoked.) A JOBOBJECT_SECURITY_LIMIT_INFORMATION structure looks like this:
typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION { DWORD SecurityLimitFlags; HANDLE JobToken; PTOKEN_GROUPS SidsToDisable; PTOKEN_PRIVILEGES PrivilegesToDelete; PTOKEN_GROUPS RestrictedSids; } JOBOBJECT_SECURITY_LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION; |
The following table briefly describes the members.
Member | Description |
---|---|
SecurityLimitFlags | Indicates whether to disallow administrator access, disallow unrestricted token access, force a specific access token, or disable certain security identifiers (SIDs) and privileges |
JobToken | Access token to be used by all processes in the job |
SidsToDisable | Indicates which SIDs to disable for access checking |
PrivilegesToDelete | Indicates which privileges to delete from the access token |
RestrictedSids | Indicates a set of deny-only SIDs that should be added to the access token |
Naturally, once you have placed restrictions on a job, you might want to query those restrictions. You can do so easily by calling
BOOL QueryInformationJobObject( HANDLE hJob, JOBOBJECTINFOCLASS JobObjectInformationClass, PVOID pvJobObjectInformation, DWORD cbJobObjectInformationLength, PDWORD pdwReturnLength); |
You pass this function the handle of the job (like you do with SetInformationJobObject)—an enumerated type that indicates what restriction information you want, the address of the data structure to be initialized by the function, and the length of the data block containing that structure. The last parameter, pdwReturnLength, points to a DWORD that is filled in by the function, which tells you how many bytes were placed in the buffer. You can (and usually will) pass NULL for this parameter if you don't care.
NOTE
A process in a job can call QueryInformationJobObject to obtain information about the job to which it belongs by passing NULL for the job handle parameter. This can be very useful because it allows a process to see what restrictions have been placed on it. However, the SetInformationJobObject function fails if you pass NULL for the job handle parameter because this would allow a process to remove restrictions placed on it.