Loading content...
Every buffer overflow, every array out-of-bounds access, every wild pointer dereference represents unauthorized memory access. In the relentless battle between software bugs and system integrity, the segment limit stands as a hardware-enforced line of defense.
The segment limit defines the maximum extent of a segment—the boundary beyond which access is forbidden. Unlike software-based bounds checking that can be bypassed or forgotten, limit checking happens in silicon, on every memory access, without exception.
This is not a suggestion; it is a law of physics for that CPU.
When a program attempts to access an offset beyond the segment limit, the CPU stops the access in its tracks, generates a processor exception, and hands control to the operating system's exception handler. The rogue access never reaches memory. Data integrity is preserved. The system survives.
Understanding segment limits at a deep level reveals how hardware and software cooperate to create protected execution environments—and why modern systems still employ similar concepts through paging.
By the end of this page, you will understand segment limit encoding, granularity flags, expand-up vs. expand-down semantics, hardware bounds checking, limit exceptions and their handling, segment growth, and how limits provide memory protection.
The segment limit field exists to answer a fundamental question: How large is this segment?
But the implications of this simple question are profound. By defining segment size, the limit enables:
1. Memory Protection:
Each segment has well-defined boundaries. A data segment cannot accidentally (or maliciously) access code, stack, or kernel memory. Even if a program has a buffer overflow bug, the corruption is contained within the segment's bounds.
2. Resource Allocation:
The limit represents committed memory. The OS knows exactly how much physical memory is allocated to each segment, enabling accurate resource accounting and enforcement of memory quotas.
3. Sparse Address Spaces:
Not all logical addresses need to be valid. By setting segment limits appropriately, large address space ranges can be marked as inaccessible, catching null pointer dereferences and uninitialized pointer usage.
4. Growth Control:
Dynamic segments like heaps and stacks need room to grow. The limit defines the current size; the OS can increase it as the segment grows, maintaining control over memory consumption.
Segment limits are just one layer of protection. Modern systems add page-level protection, NX (no-execute) bits, ASLR, stack canaries, and more. But segment limits represent the oldest and most fundamental form of hardware memory protection, dating back to the 1960s Multics system.
The limit field occupies a fixed number of bits in the segment descriptor. In x86 protected mode, the limit is a 20-bit value, split across two locations in the descriptor (just like the base address).
x86 Limit Field Layout:
Segment Descriptor (8 bytes):
Bytes 0-1: Limit[15:0] (lower 16 bits of limit)
Byte 6: Limit[19:16] | Flags (upper 4 bits + G, D/B, L, AVL flags)
Extracting the Limit:
uint32_t limit = (descriptor[0] | (descriptor[1] << 8)) | // Limit[15:0]
((descriptor[6] & 0x0F) << 16); // Limit[19:16]
Limit Interpretation:
The raw limit value represents the last valid offset within the segment, not the size. For a segment containing bytes 0 through N, the limit is N.
The 1MB Limitation Problem:
A 20-bit limit can represent at most 1,048,575—meaning segments cannot exceed 1 MB with byte granularity. For 32-bit systems where segments up to 4 GB are needed, this is unacceptable. The solution is the granularity flag.
| Property | Value Range | Notes |
|---|---|---|
| Bit Width | 20 bits | Split across bytes 0-1 and byte 6 |
| Minimum Limit | 0 | 1 byte segment (offset 0 only) |
| Maximum Limit (G=0) | 0xFFFFF | 1 MB - 1 byte |
| Maximum Limit (G=1) | 0xFFFFF × 4096 | 4 GB - 1 byte (4294967295) |
| Semantics | Last valid offset | Not size, but highest valid address - base |
A common confusion: the limit is NOT the segment size. If limit = 0xFFF, the segment has 0x1000 (4096) valid bytes (offsets 0 through 0xFFF). Size = Limit + 1 (for byte granularity). This off-by-one relationship is crucial when setting up descriptors.
The granularity (G) flag in the segment descriptor determines how the limit value is interpreted. This single bit expands the range of possible segment sizes from 1 MB to 4 GB.
Granularity Flag Semantics:
G = 0 (Byte Granularity): The limit is interpreted in bytes. The maximum segment size is 2²⁰ = 1,048,576 bytes (1 MB).
G = 1 (Page Granularity): The limit is interpreted in 4 KB pages. The actual byte limit = (limit + 1) × 4096 - 1. Maximum segment size is 2²⁰ × 4096 = 4,294,967,296 bytes (4 GB).
Mathematical Transformation:
Effective Limit (G=0): raw_limit
Effective Limit (G=1): (raw_limit + 1) × 4096 - 1
= raw_limit × 4096 + 4095
= (raw_limit << 12) | 0xFFF
Example Calculations:
| Raw Limit | G=0 (Bytes) | Segment Size (G=0) | G=1 (Pages) | Segment Size (G=1) |
|---|---|---|---|---|
| 0x000 | 0-0 | 1 byte | 0-4095 | 4 KB |
| 0x0FF | 0-255 | 256 bytes | 0-1048575 | 1 MB |
| 0xFFF | 0-4095 | 4 KB | 0-16777215 | 16 MB |
| 0xFFFF | 0-65535 | 64 KB | 0-268435455 | 256 MB |
| 0xFFFFF | 0-1048575 | 1 MB | 0-4294967295 | 4 GB |
Granularity and Precision Trade-off:
With page granularity, you lose fine-grained control over segment size. You cannot create a segment of exactly 5000 bytes with G=1; the closest you get is either 4 KB (4096 bytes) or 8 KB (8192 bytes).
This trade-off is acceptable for most use cases:
Common Configurations:
// Flat 4GB data segment (typical for modern OS)
Base = 0x00000000
Limit = 0xFFFFF
G = 1
Effective range: 0 to 4,294,967,295 (entire 32-bit space)
// 64KB code segment (embedded system)
Base = 0x00010000
Limit = 0xFFFF
G = 0
Effective range: 0 to 65535 (64 KB)
// 128KB data segment
Base = 0x00020000
Limit = 0x1FFFF
G = 0
Effective range: 0 to 131071 (128 KB with byte precision)
// 1MB stack segment
Base = 0x00100000
Limit = 0x0FF
G = 1
Effective range: 0 to 1048575 (1 MB with page granularity)
When G=1, even a limit of 0 allows access to bytes 0-4095 (the first page). This means page-granular segments always include at least 4 KB. If you need finer control for small segments, you must use G=0 (byte granularity), limiting maximum size to 1 MB.
Most segments are expand-up—valid offsets start at 0 and extend upward to the limit. But stacks present a unique challenge: they grow downward from high addresses to low addresses. Expand-down segments address this by inverting how the limit is interpreted.
Expand-Up Semantics (E=0):
Valid offset range: 0 to limit (inclusive)
Segment Start (Base) Segment End (Base + Limit)
│ │
▼ ▼
┌──────┴──────────────────────────────┴──────┐
│ Valid Address Range │
│ Offsets 0 to Limit are accessible │
└────────────────────────────────────────────┘
▲ ▲
│ │
Lowest Valid Highest Valid
Offset = 0 Offset = Limit
Expand-Down Semantics (E=1):
Valid offset range: (limit + 1) to (max_offset) where max_offset depends on D/B flag
┌────────────────────────────────────────────────┐
│ Invalid Range (0 to Limit) │
├────────────────────────────────────────────────┤
│ Valid Range (Limit+1 to MaxOffset) │
│ Stack pointer starts high, grows down │
└────────────────────────────────────────────────┘
▲ ▲
│ │
Stack can grow Stack top
down to Limit+1 (high address)
| D/B Flag | Max Offset | Valid Range for Limit=0x7FFFF |
|---|---|---|
| 0 (16-bit) | 0xFFFF | 0x80000 to 0xFFFF (wraps in 16-bit) |
| 1 (32-bit) | 0xFFFFFFFF | 0x80000 to 0xFFFFFFFF (~4GB - 512KB valid) |
Why Expand-Down for Stacks?
Stacks traditionally grow from high memory addresses toward low addresses. When a function is called, the stack pointer decreases to allocate stack frame space. When the function returns, the stack pointer increases.
With an expand-down segment:
Stack Growth Scenario:
Initial State:
Limit = 0xFFFF0000 (with G=1, allowing ~64KB stack)
Valid range: 0xFFFF0001 to 0xFFFFFFFF
Stack pointer (ESP) = 0xFFFFFFF0
After Heavy Function Calls:
Stack pointer (ESP) = 0xFFFF1000
Still within valid range—OK
Stack Overflow Attempt:
Stack pointer tries to go to 0xFFFEFFFF
Offset 0xFFFEFFFF < limit (0xFFFF0000) + 1
FAULT! General Protection Exception triggered
OS Grows Stack:
OS allocates more memory, decreases limit to 0xFFFE0000
Valid range now: 0xFFFE0001 to 0xFFFFFFFF
Fault handler returns, instruction retries successfully
Modern systems using flat memory models don't typically use expand-down segments. Instead, the stack is a region within the flat 4GB address space, and guard pages detect stack overflow at the page level. However, understanding expand-down segments is essential for comprehending historical systems and concepts like segment-based protection.
The CPU performs bounds checking on every memory access through a segment. This checking happens in hardware, adds negligible overhead (it's part of the address translation pipeline), and cannot be bypassed by software.
Bounds Check Logic:
For expand-up segments (most common):
if (offset > effective_limit)
raise GeneralProtectionFault(#GP)
else
proceed with memory access
For expand-down segments (stacks):
if (offset <= effective_limit || offset > max_offset)
raise GeneralProtectionFault(#GP)
else
proceed with memory access
Operand Size Considerations:
The check must account for the size of the data being accessed. A 4-byte read at offset 0xFFFC in a segment with limit 0xFFFF would access bytes 0xFFFC, 0xFFFD, 0xFFFE, 0xFFFF—all valid. But a 4-byte read at offset 0xFFFD would try to access 0xFFFD through 0x10000, with 0x10000 exceeding the limit.
The Check:
if (offset + operand_size - 1 > effective_limit)
raise #GP
1234567891011121314151617181920212223242526272829303132
// Hardware bounds checking algorithm function check_bounds(segment_descriptor, offset, access_size): // Calculate effective limit based on granularity if segment_descriptor.granularity == PAGE: effective_limit = (segment_descriptor.limit << 12) | 0xFFF else: effective_limit = segment_descriptor.limit // Determine max offset based on segment size flag if segment_descriptor.D_B_flag: max_offset = 0xFFFFFFFF // 32-bit segment else: max_offset = 0xFFFF // 16-bit segment // Calculate last byte accessed last_byte_offset = offset + access_size - 1 // Check based on expand direction if segment_descriptor.expand_down: // Expand-down: valid range is (limit+1) to max_offset if offset <= effective_limit: raise GeneralProtectionFault("Offset below stack limit") if last_byte_offset > max_offset: raise GeneralProtectionFault("Offset exceeds max") else: // Expand-up: valid range is 0 to limit if last_byte_offset > effective_limit: raise GeneralProtectionFault("Offset exceeds segment limit") // Bounds check passed return OKChecking at Multiple Levels:
Modern x86 systems often have both segmentation and paging enabled. Bounds checking happens at both levels:
Both checks must pass. A segment might allow access to linear address 0x40001000, but if that page is marked not-present, a page fault occurs—and vice versa.
Performance of Bounds Checking:
Bounds checking is pipelined into address translation. The comparison between offset and limit happens in parallel with other address calculation steps. On modern CPUs, it adds zero additional cycles to memory access latency—the check is truly free.
This is a fundamental advantage of hardware protection: unlike software bounds checking (which adds instructions), hardware checking is invisible to performance.
The x86 BOUND instruction performs explicit bounds checking: BOUND reg, mem compares a register against low/high bounds in memory and raises #BR (Bound Range Exceeded) if out of bounds. While useful for software bounds checking (array indices), it's rarely used in practice. The segment limit mechanism is both automatic and hardware-enforced, making manual bounds checking largely unnecessary for segment-based protection.
When a bound check fails, the CPU raises an exception. The specific exception depends on the context, but most commonly it's a General Protection Fault (#GP, Interrupt 13) or a Stack Fault (#SS, Interrupt 12).
Exception Classifications:
| Situation | Exception | Error Code | Description |
|---|---|---|---|
| Data segment out of bounds | #GP (13) | Selector | Access beyond data segment limit |
| Code segment out of bounds | #GP (13) | Selector | Instruction fetch beyond code limit |
| Stack segment out of bounds | #SS (12) | 0 | Stack access beyond stack limit |
| Limit of 0 on stack | #SS (12) | 0 | Minimum valid stack (special case) |
Exception Frame Information:
When a limit exception occurs, the CPU pushes the following onto the kernel stack:
┌─────────────────────────────────────┐ Higher Addresses
│ SS (if privilege change) │
├─────────────────────────────────────┤
│ ESP (if privilege change) │
├─────────────────────────────────────┤
│ EFLAGS │
├─────────────────────────────────────┤
│ CS │
├─────────────────────────────────────┤
│ EIP (return address) │
├─────────────────────────────────────┤
│ Error Code (selector or 0) │ ← Pushed for #GP, #SS
└─────────────────────────────────────┘ Stack Pointer (ESP) after push
The EIP points to the instruction that caused the fault, allowing the handler to analyze or retry.
Handler Responsibilities:
12345678910111213141516171819202122232425262728293031323334353637383940
// General Protection Fault handler for limit violations void general_protection_handler(interrupt_frame_t* frame, uint32_t error_code) { segment_selector_t selector = { .raw = error_code & 0xFFFC }; // Get the faulting instruction address void* fault_addr = (void*)frame->eip; // Decode the faulting instruction to get accessed offset uint32_t offset = decode_memory_operand(fault_addr, frame); // Look up the segment descriptor segment_descriptor_t* desc = get_descriptor_from_selector(selector); // Is this a recoverable fault? if (is_stack_growth_fault(selector, offset, desc)) { // Stack needs to grow if (grow_stack(current_process(), offset)) { // Stack grown successfully, retry instruction return; } // Cannot grow stack further - fall through to kill } if (is_heap_growth_fault(selector, offset, desc)) { // Heap brk() or mmap() region needs extending if (extend_heap(current_process(), offset)) { return; // Retry instruction } } // Unrecoverable limit violation log_fault_info(current_process(), frame, selector, offset, "GP_LIMIT"); // Deliver SIGSEGV to the process deliver_signal(current_process(), SIGSEGV, make_siginfo_gp(frame, offset)); // If we return here (signal handler installed), retry instruction // Usually the process is terminated by default SIGSEGV handler}Not all limit violations are recoverable. A buffer overflow corrupting code pointers, or a wild pointer accessing unmapped segments, typically indicate bugs that cannot be safely fixed at runtime. The safest response is usually process termination, preserving system integrity over the flawed process.
Unlike code segments which are typically fixed-size, data segments (heaps) and stack segments often need to grow during program execution. The limit field enables this growth—the OS simply increases (or for expand-down, decreases) the limit value.
Heap Growth (Expand-Up):
When a program calls malloc() and the current heap is full, the runtime requests more memory from the OS (via sbrk() or mmap()):
sbrk(increment) or mmap() for more heapnew_limit = old_limit + incrementmalloc() now has more spaceStack Growth (Expand-Down):
Stack growth is often demand-driven—the stack faults when it needs to grow:
Before Stack Growth:
Expand-Down Stack Segment:
Limit = 0xFFFF0000
Valid: 0xFFFF0001 - 0xFFFFFFFF
0xFFFFFFF0 │ SP here
0xFFFFFFF4 │ Local var
0xFFFFFFF8 │ Return addr
0xFFFFFFFC │ Frame ptr
─────────── │ ───────────
0xFFFF0001 │ Stack bottom
0xFFFF0000 ┼ ← LIMIT (invalid)
0xFFFEFFFF │ INACCESSIBLE
▼
After Stack Growth:
Expand-Down Stack Segment:
Limit = 0xFFFE0000 (decreased!)
Valid: 0xFFFE0001 - 0xFFFFFFFF
0xFFFFFFF0 │ SP now here
0xFFFFFFF4 │
0xFFFFFFF8 │
0xFFFFFFFC │
─────────── │ ───────────
0xFFFF0004 │ New stack space
0xFFFF0000 │ ←(was limit)
0xFFFE0001 │ New stack bottom
0xFFFE0000 ┼ ← NEW LIMIT
▼
Protection During Growth:
The OS must carefully validate growth requests:
Automatic vs. Explicit Growth:
Modern systems often use guard pages (unmapped pages) instead of segment limits to detect stack overflow. When the stack tries to grow into the guard page, a page fault occurs. The handler then maps a new page and places a new guard page below it. This achieves the same result with paging mechanisms rather than segmentation.
Segment limits are a powerful security mechanism, providing containment of memory errors and preventing certain classes of exploits. Understanding their security implications is crucial for systems security.
Buffer Overflow Containment:
In a properly segmented system, a buffer overflow in a data segment cannot:
The damage is contained within the single segment, greatly limiting exploit potential.
Attack Prevention:
| Attack Type | Without Limits | With Segment Limits |
|---|---|---|
| Buffer Overflow | Corrupt adjacent memory, hijack control flow | Fault at segment boundary, attacker blocked |
| Stack Smashing | Overwrite return address | Stack isolated in own segment (if separate) |
| Heap Overflow | Corrupt heap metadata, arbitrary write | Contained within heap segment |
| Format String | Read/write anywhere via %n | Writes outside data segment fault |
| Integer Overflow (leading to OOB) | Access arbitrary memory | Limit check catches oversized offset |
Limitations of Segment-Based Protection:
While segment limits are powerful, they have limitations:
Intra-Segment Corruption: Limits don't prevent corrupting other data within the same segment. A heap buffer overflow can corrupt other heap objects.
Coarse Granularity: Segments are large. Fine-grained protection (per-object) requires additional mechanisms.
Flat Model Bypass: Modern systems using flat memory models (all segments base=0, limit=4GB) get minimal protection from segment limits—paging provides the actual protection.
No Use-After-Free Protection: Limits don't detect accessing memory that's been freed but is still within the segment.
Modern Security Layers:
Segment limits are now just one layer in a defense-in-depth stack:
The concept of segment-based protection with limits originated in the Multics operating system (1965). Multics used segments extensively for isolation, with hardware-enforced limits. Many ideas from Multics influenced Unix and modern OS design. The segment limit concept lives on conceptually in page table protection bits and memory region permissions.
When the kernel needs to access user-space memory (e.g., reading syscall arguments), it must verify that user-provided pointers are within valid segment bounds. This prevents confused deputy attacks where user code tricks the kernel into accessing its own protected memory.
The Confused Deputy Problem:
A user provides a pointer to read(fd, buf, count). If the kernel blindly accesses that pointer:
The solution: verify the pointer is within user segment limits before access.
User Pointer Verification:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
// Kernel functions to safely access user memory // Check if a user pointer range is validbool access_ok(int type, const void __user *addr, size_t size) { // Get user data segment descriptor segment_descriptor_t* user_ds = get_user_data_segment(); // Calculate user segment bounds uint32_t user_base = get_segment_base(user_ds); uint32_t user_limit = get_effective_limit(user_ds); // Convert user pointer to offset within segment uint32_t offset = (uint32_t)addr - user_base; uint32_t end_offset = offset + size - 1; // Verify within bounds if (offset > user_limit || end_offset > user_limit) { return false; // Invalid: outside user segment } // Also check that pointer doesn't wrap around if ((uint32_t)addr + size < (uint32_t)addr) { return false; // Integer overflow in pointer arithmetic } return true; // Valid user address} // Safe copy from user space to kernellong copy_from_user(void *to, const void __user *from, unsigned long n) { // First, verify user pointer is within valid segment if (!access_ok(VERIFY_READ, from, n)) { return -EFAULT; } // Even after verification, access might fault (swapped out, etc.) // The actual copy must handle page faults return __copy_from_user_nocheck(to, from, n);} // Safe copy to user space from kernellong copy_to_user(void __user *to, const void *from, unsigned long n) { if (!access_ok(VERIFY_WRITE, to, n)) { return -EFAULT; } // Verify destination is in a writable segment segment_descriptor_t* user_ds = get_user_data_segment(); if (!segment_is_writable(user_ds)) { return -EFAULT; } return __copy_to_user_nocheck(to, from, n);}Kernel/User Split:
Historically, on x86 32-bit systems, the address space was often split:
The user data segment would have:
Any attempt by user code to access kernel addresses would exceed the limit and fault. The kernel could access anywhere (flat 4GB segment with Ring 0).
SMAP/SMEP (Modern x86):
On modern x86 systems, hardware features like SMAP (Supervisor Mode Access Prevention) and SMEP (Supervisor Mode Execution Prevention) add another layer: they prevent the kernel from accidentally accessing user memory unless explicit override is set. This protects against confused deputy attacks even in flat memory models.
Failure to verify user pointers is a critical security vulnerability. Kernel bugs allowing arbitrary read/write via unverified pointers have led to countless privilege escalation exploits. Every interaction with user-provided addresses must be validated against segment bounds and permissions.
The segment limit field is a cornerstone of memory protection. Let's consolidate the key concepts we've explored:
What's Next:
With base and limit covered, we'll now explore protection bits—the segment descriptor fields that control read, write, and execute permissions. Protection bits determine not just where you can access, but what operations you can perform, completing the trifecta of segment-based memory protection.
You now have comprehensive knowledge of segment limits—encoding, interpretation, granularity, expand-up/down semantics, hardware bounds checking, fault handling, dynamic growth, and security implications. This understanding is essential for low-level systems programming and security analysis.