System Calls, Copy Boundaries, and User-Kernel ABI

LESSON

Operating Systems Implementation

008 30 min intermediate

System Calls, Copy Boundaries, and User-Kernel ABI

By the end of this lesson, you will be able to...

  • Trace a write request from user registers through dispatch, validation, copy, and return.

  • Explain why a user pointer is input rather than a kernel pointer.

  • Predict the safe result of invalid memory, partial work, and interrupted waiting.

Idea in one sentence: A syscall ABI is a stable request contract, and the kernel must validate and copy every untrusted argument before it lets that request affect privileged state.

calc executes write(1, buffer, 3) to print 42\n. The bytes are in its user address space. The console driver and file table are in kernel space. The processor can transfer control through ecall, but that transfer does not make buffer trustworthy or turn it into a kernel address.

The kernel must answer several small questions in order: Which syscall did calc request? Are the descriptor and length valid? Does the whole user range exist and have the required permission? Should the bytes be copied now, or may the kernel safely use the memory later? What return value tells the program whether all, some, or none of the requested bytes were accepted?

The order matters. Validation before side effects gives the kernel a small, auditable promise: malformed user input becomes a reported error, not accidental damage to a later subsystem.

Those questions form the user–kernel ABI: the binary agreement about syscall numbers, argument locations, result values, and errors. A good ABI is narrow and explicit. A good copy boundary treats user memory as data supplied by a less-trusted party, even when that party is a friendly program.

Core Insight

A syscall has two boundaries:

Boundary What crosses it Kernel responsibility
control boundary U-mode request enters privileged trap handling decode the request and return to the correct PC/state
data boundary integers, pointers, buffers, and handles supplied by user code validate values, safely access memory, define ownership and result semantics

Plain meaning:

Calling the front desk lets a visitor ask for a service. It does not let the visitor walk into the staff room or hand the clerk a key claimed to open any room.

In this scenario:

buffer is a virtual address meaningful in calc's page table. The console driver cannot blindly dereference it as though it were a stable kernel pointer.

Technical name:

The locations and meanings of syscall number, arguments, return value, and error conventions form the application binary interface (ABI). The deliberate transfer or checked access of bytes is a copy boundary.

The Naive Handler Breaks the Protection Boundary

This unsafe sketch looks convenient:

sys_write(int fd, char *user_buffer, int n) {
  return console_write(user_buffer, n);
}

It assumes the pointer is valid, points to readable bytes for n positions, remains valid for the operation, and cannot refer to kernel-only memory. None of those assumptions follows from receiving an integer in a register.

The pointer may be unmapped, point near the end of a page, overflow when combined with n, or be changed by another thread in the same process. A huge n can exceed a resource limit even if the first byte is valid. If an asynchronous device retains the pointer after the syscall returns, the process can unmap or rewrite the memory before completion.

The better model is: a user pointer names a requested range, not an owned kernel buffer. The kernel validates the range in the relevant address space and either copies bytes into kernel-owned storage or uses a documented pinning/lifetime protocol for the short interval in which it accesses them.

Worked Trace: write(1, 0x1000, 3)

Assume calc has mapped readable user bytes 42\n at 0x1000 and executes a RISC-V ecall.

Input:

a0 = 1       descriptor
a1 = 0x1000  user buffer address
a2 = 3       length
a7 = WRITE   syscall number
pc = ecall

1. Trap entry preserves the caller's state

The trap mechanism from lesson 003 saves user registers and records the request PC. The syscall dispatcher reads the syscall number and arguments from the trap frame. It advances the saved PC so a successful return continues after ecall, not through the same request forever.

2. Dispatch validates scalar arguments first

The dispatcher checks that the syscall number is known. write checks that descriptor 1 refers to a writable endpoint and that length is non-negative, within an implementation limit, and cannot overflow its address calculations. It does not start by copying a user-controlled number of bytes into a fixed kernel array.

3. The kernel validates the complete user range

For a three-byte range, the kernel verifies that addresses 0x1000 through 0x1002 are readable in calc's address space. A robust helper checks page boundaries and arithmetic before each access. It must not validate only 0x1000 and then assume the rest of a long range is safe.

If the range crosses into an unmapped page, the operation fails safely or returns the documented partial result. The kernel should not panic because ordinary user input is malformed.

4. Copy establishes kernel ownership

For a small console write, the kernel copies the three bytes into a kernel buffer or consumes them under a short, checked access routine. The console code now receives a kernel-owned byte sequence, not a live untrusted pointer. If a later I/O request must outlive the syscall, it owns a copied buffer or another explicit lifetime claim, following lesson 005's rules.

5. The handler returns a precise result

If all bytes were accepted, the handler writes 3 into the saved return-value register. If no byte could be accepted because the descriptor is invalid or the first address faults, it returns a defined error. If the interface permits partial progress, it returns the number successfully accepted and leaves the caller to retry the remainder. Return values are part of the ABI, not an afterthought.

Output: trap return restores calc's context. It sees either 3, a documented partial count, or an error. The console driver never received an unchecked user pointer.

Naive failure contrast: Passing 0x1000 to the driver directly can work in a simple test, then fail when the pointer crosses a page boundary, is unmapped after queueing, or names a protected range. Copying turns a moving user-memory claim into an owned kernel resource.

Check: A user buffer starts in a valid page but its requested range crosses into an unmapped page. Is validating only the first byte sufficient?

Think first, then reveal.

Answer: No. The kernel must validate the complete range it intends to read or copy, including page transitions and overflow. A valid starting address does not authorize later addresses.

Blocking, Errors, and ABI Stability

Some syscalls complete immediately; others wait for I/O, a lock, or another process. If read blocks, the thread moves to the sleeping state from lesson 007. The syscall's request state must remain owned while it waits, and a wakeup makes the thread runnable to finish the request. A signal, cancellation rule, or device error can produce a defined error instead of an invented success.

The trade-off is between a small stable ABI and rich kernel features. Each syscall number, argument convention, and return rule becomes a contract that user programs, libraries, debuggers, and tests may rely on. Adding a convenient raw kernel pointer or ambiguous “maybe complete” result can simplify one implementation while making every caller harder to reason about. Copying is also a cost: it consumes CPU and memory bandwidth. It is often worth that cost because it makes ownership and isolation clear.

An ABI does not solve semantic errors in the service itself. A valid pointer can contain nonsensical bytes. A successful write count does not guarantee permanent storage; durability depends on later filesystem and device rules. The ABI only makes the boundary and observable result honest.

Common Confusions

Confusion: ecall makes a user pointer safe

Why it is tempting:

After the trap, the kernel runs with greater privilege.

Better model:

Greater privilege lets the kernel inspect memory; it increases its duty to check it. The pointer still originated outside the trusted boundary.

Confusion: Copying removes all validation

Why it is tempting:

The target buffer is now kernel-owned.

Better model:

The source range and length must be valid before and during the copy. Copying solves later lifetime and mutation problems, not arithmetic, permission, or resource-limit checks.

Confusion: An error means no work happened

Why it is tempting:

Many tiny examples either fully succeed or fully fail.

Better model:

An ABI must specify partial-result behavior. A caller may need to handle a short write or an interrupted blocking operation without guessing what was accepted.

Practice: Review a write Contract

Review this proposed syscall rule: “write(fd, ptr, n) checks that ptr is non-null, queues the pointer to the device, and returns zero when the request is queued.”

List four repairs. A strong answer validates fd, n, and the complete readable user range; copies or pins memory with a defined lifetime; gives the queued request ownership of its data; and returns a documented byte count or error. It also states whether queueing, device completion, and durable storage are distinct outcomes.

Check: A syscall handler copies bytes successfully but later discovers that fd is invalid. What should its externally visible result be?

Think first, then reveal.

Answer: The handler should return the ABI's invalid-descriptor error and avoid claiming a successful write. Internal copying is not the requested service; resource validation must precede the externally visible success decision.

Resources

Key Takeaways

PREVIOUS Scheduler Policy, Timers, and Blocking NEXT Synchronization Inside the Kernel