780 Chapter 20 The Linux System
ever, in the sense that people can copy it, modify it, use it in any manner they
want, and give away (or sell) their own copies.
The main implication of Linux’s licensing terms is that nobody using
Linux, or creating a derivative of Linux (a legitimate exercise), can distribute
the derivative without including the source code. Software released under the
GPL cannot be redistributed as a binary-only product. If you release software
that includes any components covered by the GPL, then, under the GPL, you
must make source code available alongside any binary distributions. (This
restriction does not prohibit making—or even selling—binary software distri-
butions, as long as anybody who receives binaries is also given the opportunity
to get the originating source code for a reasonable distribution charge.)
20.2 Design Principles
In its overall design, Linux resembles other traditional, nonmicrokernel UNIX
implementations. It is a multiuser, preemptively multitasking system with a
full set of UNIX-compatible tools. Linux’s file system adheres to traditional
UNIX semantics, and the standard UNIX networking model is fully imple-
mented. The internal details of Linux’s design have been influenced heavily
by the history of this operating system’s development.
Although Linux runs on a wide variety of platforms, it was originally
developed exclusively on PC architecture. A great deal of that early develop-
ment was carried out by individual enthusiasts rather than by well-funded
development or research facilities, so from the start Linux attempted to squeeze
as much functionality as possible from limited resources. Today, Linux can
run happily on a multiprocessor machine with hundreds of gigabytes of main
memory and many terabytes of disk space, but it is still capable of operating
usefully in under 16-MB of RAM.
As PCs became more powerful and as memory and hard disks became
cheaper, the original, minimalist Linux kernels grew to implement more UNIX
functionality. Speed and efficiency are still important design goals, but much
recent and current work on Linux has concentrated on a third major design
goal: standardization. One of the prices paid for the diversity of UNIX imple-
mentations currently available is that source code written for one may not
necessarily compile or run correctly on another. Even when the same system
calls are present on two different UNIX systems, they do not necessarily behave
in exactly the same way. The POSIX standards comprise a set of specifications
for different aspects of operating-system behavior. There are POSIX documents
for common operating-system functionality and for extensions such as pro-
cess threads and real-time operations. Linux is designed to comply with the
relevant POSIX documents, and at least two Linux distributions have achieved
official POSIX certification.
Because it gives standard interfaces to both the programmer and the user,
Linux presents few surprises to anybody familiar with UNIX. We do not detail
these interfaces here. The sections on the programmer interface (Section C.3)
and user interface (Section C.4) of BSD apply equally well to Linux. By default,
however, the Linux programming interface adheres to SVR4 UNIX semantics,
rather than to BSD behavior. A separate set of libraries is available to implement
BSD semantics in places where the two behaviors differ significantly.
9.5 Swapping 377
9.5.1 Standard Swapping
Standard swapping involves moving entire processes between main memory
and a backing store. The backing store is commonly fast secondary storage.
It must be large enough to accommodate whatever parts of processes need to
be stored and retrieved, and it must provide direct access to these memory
images. When a process or part is swapped to the backing store, the data
structures associated with the process must be written to the backing store.
For a multithreaded process, all per-thread data structures must be swapped
as well. The operating system must also maintain metadata for processes that
have been swapped out, so they can be restored when they are swapped back
in to memory.
The advantage of standard swapping is that it allows physical memory to
be oversubscribed, so that the system can accommodate more processes than
there is actual physical memory to store them. Idle or mostly idle processes
are good candidates for swapping; any memory that has been allocated to
these inactive processes can then be dedicated to active processes. If an inactive
process that has been swapped out becomes active once again, it must then be
swapped back in. This is illustrated in Figure 9.19.
9.5.2 Swapping with Paging
Standard swapping was used in traditional UNIX systems, but it is generally no
longer used in contemporary operating systems, because the amount of time
required to move entire processes between memory and the backing store is
prohibitive. (An exception to this is Solaris, which still uses standard swapping,
however only under dire circumstances when available memory is extremely
low.)
Most systems, including Linux and Windows, now use a variation of swap-
ping in which pages of a process—rather than an entire process—can be
swapped. This strategy still allows physical memory to be oversubscribed, but
does not incur the cost of swapping entire processes, as presumably only a
small number of pages will be involved in swapping. In fact, the term swapping
now generally refers to standard swapping, and paging refers to swapping
with paging. A page out operation moves a page from memory to the backing
store; the reverse process is known as a page in. Swapping with paging is illus-
trated in Figure 9.20 where a subset of pages for processes A and B are being
paged-out and paged-in respectively. As we shall see in Chapter 10, swapping
with paging works well in conjunction with virtual memory.
9.5.3 Swapping on Mobile Systems
Most operating systems for PCs and servers support swapping pages. In con-
trast, mobile systems typically do not support swapping in any form. Mobile
devices generally use flash memory rather than more spacious hard disks for
nonvolatile storage. The resulting space constraint is one reason why mobile
operating-system designers avoid swapping. Other reasons include the limited
number of writes that flash memory can tolerate before it becomes unreliable
and the poor throughput between main memory and flash memory in these
devices.
628 Chapter 16 Security
#include <stdio.h>
#define BUFFER SIZE 0
int main(int argc, char *argv[])
{
int j = 0;
char buffer[BUFFER SIZE];
int k = 0;
if (argc < 2) {return -1;}
strcpy(buffer,argv[1]);
printf("K is %d, J is %d, buffer is %s∖n", j,k,buffer);
return 0;
}
}
Figure 16.2 C program with buffer-overflow condition.
code-scanning tools designed to find flaws, including security flaws, but gen-
erally good programmers are the best code reviewers.
For those not involved in developing the code, code review is useful for
finding and reporting flaws (or for finding and exploiting them). For most
software, source code is not available, making code review much harder for
nondevelopers.
16.2.2 Code Injection
Most software is not malicious, but it can nonetheless pose serious threats to
security due to a code-injection attack, in which executable code is added
or modified. Even otherwise benign software can harbor vulnerabilities that,
if exploited, allow an attacker to take over the program code, subverting its
existing code flow or entirely reprogramming it by supplying new code.
Code-injection attacks are nearly always the result of poor or insecure
programming paradigms, commonly in low-level languages such as C or
C++, which allow direct memory access through pointers. This direct mem-
ory access, coupled with the need to carefully decide on sizes of memory
buffers and take care not to exceed them, can lead to memory corruption when
memory buffers are not properly handled.
As an example, consider the simplest code-injection vector—a buffer over-
flow. The program in Figure 16.2 illustrates such an overflow, which occurs due
to an unbounded copy operation, the call to strcpy(). The function copies
with no regard to the buffer size in question, halting only when a NULL (∖0)
byte is encountered. If such a byte occurs before the BUFFER SIZE is reached,
the program behaves as expected. But the copy could easily exceed the buffer
size —what then?
The answer is that the outcome of an overflow depends largely on the
length of the overflow and the overflowing contents (Figure 16.3). It also varies
greatly with the code generated by the compiler, which may be optimized