Skip to content
academia.sh

Lesson 06 / 09

System Call Tracing

The first measure of observation piercing isolation: of the 14 facts the strict bundle closes, 2 return through system call tracing, the open surface climbs from 10 to 12, and the run slows by 34 percent; three of the five facts the channel shows were already visible.

Contents

The isolation topic closed with a strict eight-mechanism bundle: of a process’s 24 observable facts, 14 had closed, 10 stayed visible, and something was still open on seven of the nine axes. The question there was not “how much did we close,” it was “what stayed open.” This topic asks the same question in reverse: of the 14 closed facts, how many come back because of the tools we use to understand the system.

The reversal is not wordplay. Observation tools were not written to pierce isolation; they were written to show what a process is doing. But showing what a process is doing means showing exactly what isolation is trying to hide. The topic’s two subjects are therefore opposites of one another, and this lesson opens with the most expensive example of that opposition: system call tracing.

Channel Versus Filter

The common definition has two objects. An isolation mechanism is a filter: it closes a given axis and usually leaves one fact open on that axis. An observation channel is a channel: it brings back some of the closed facts. The oracle is all of the process’s facts; because we wrote the mock setup, it is known. What is measured is the open surface — the union of the visible facts and the returned facts.

This lesson’s assumptions are these. The channel runs after isolation is set up and from outside the bundle; the observing side is not bound by the observed side’s restrictions (OD1). The channel shows only the facts on its own list; it produces no new fact (OD2). When a fact returns, it returns completely; there is no partial visibility (OD3). The five facts system call tracing shows are the list in the code (OD4). Channel costs are percentages in the model, not a real measurement; the baseline run is 1000 time units (OD5). The cost is fixed for the run and does not change with the number of calls traced (OD6). Tracing is on while the process runs; it is not a channel that looks after the run has ended (OD7).

What the tool sees, on its side, is the sequence of services the process requests from the kernel: which file was opened, which metadata was read, which clock was queried. The system call itself was built as a mechanism in the Operating System Concepts course and is not repeated here.

# example dump , has not been run
$ strace -f -e trace=file,clock -p <process-no>
openat(AT_FDCWD, "/etc/<config-file>", O_RDONLY)         = 3
newfstatat(3, "", {st_mode=S_IFREG|0644, st_uid=0}, 0)   = 0
openat(AT_FDCWD, "/proc/self/mountinfo", O_RDONLY)       = 4
clock_gettime(CLOCK_REALTIME, {tv_sec=<seconds>})        = 0

This dump has not been run and the values in it are mock. What is measured is not the dump itself but which fact can be read from it; the code below counts that.

# M03/K05 common definition -- only the parts this lesson uses.
# Isolation is a filter: it closes an axis , leaving one thing open on each.
# The observation channel is a channel: it brings BACK some of the closed facts.
FACTS = [
    ("other-process-list", "process"), ("own-process-number", "process"),
    ("ancestor-process-chain", "process"),
    ("root-filesystem-tree", "mount"), ("other-mounts", "mount"),
    ("shared-temp-directory", "mount"),
    ("host-interfaces", "network"), ("host-routing-table", "network"),
    ("listening-ports", "network"),
    ("user-id-mapping", "user"), ("file-ownership", "user"),
    ("hostname", "host"),
    ("cpu-share", "resource"), ("memory-limit", "resource"),
    ("memory-used", "resource"), ("cpu-count", "resource"),
    ("kernel-version", "kernel"), ("kernel-settings", "kernel"),
    ("system-load", "kernel"), ("clock", "kernel"),
    ("capability-set", "privilege"), ("file-permissions", "privilege"),
    ("security-label", "label"), ("policy-rules", "label"),
]
MECHANISM = {
    "pid-namespace":     {"axis": ["process"], "left_open": ["own-process-number"]},
    "mount-namespace":   {"axis": ["mount"], "left_open": ["root-filesystem-tree"]},
    "network-namespace": {"axis": ["network"], "left_open": []},
    "user-namespace":    {"axis": ["user"], "left_open": ["file-ownership"]},
    "uts-namespace":     {"axis": ["host"], "left_open": []},
    "control-group":     {"axis": ["resource"], "left_open": ["cpu-count"]},
    "capability-dropping": {"axis": ["privilege"], "left_open": ["file-permissions"]},
    "mandatory-label":   {"axis": ["label"], "left_open": ["security-label"]},
}
COMMON_BUNDLE = ["pid-namespace", "mount-namespace", "network-namespace",
                 "uts-namespace", "control-group", "capability-dropping"]
STRICT_BUNDLE = COMMON_BUNDLE + ["user-namespace", "mandatory-label"]

# OD4: the facts the system-call-tracing channel shows
TRACE = ["root-filesystem-tree", "other-mounts", "file-ownership", "capability-set", "clock"]
# OD5: channel costs are percentages IN THE MODEL , not a real measurement
CHANNEL_COST = {"system-call-tracing": 34, "sampling-profile": 3,
                "in-kernel-tracing": 6, "core-dump": 0}
BASELINE_TIME = 1000


def visible(mechanisms, facts=FACTS):
    closed = set()
    for d in mechanisms:
        v = MECHANISM[d]
        for e in v["axis"]:
            closed |= {o for o, x in facts if x == e}
        closed -= set(v["left_open"])
    return [o for o, _ in facts if o not in closed]


def returned(mechanisms, channel, facts=FACTS):
    g = set(visible(mechanisms, facts))
    closed = {o for o, _ in facts} - g
    return sorted(set(channel) & closed)


def surface(mechanisms, channel=(), facts=FACTS):
    g = set(visible(mechanisms, facts))
    back = set(returned(mechanisms, channel, facts))
    return {"visible": len(g), "returned": len(back), "open_surface": len(g | back),
            "open_axes": len({e for o, e in facts if o in (g | back)})}


print("facts:", len(FACTS), "| axes:", len({e for _, e in FACTS}))
g = set(visible(STRICT_BUNDLE))
print("strict bundle: mechanisms", len(STRICT_BUNDLE), "| visible", len(g),
      "| closed", len(FACTS) - len(g))
print()
print("fact shown by channel       axis       status under strict bundle")
for o in TRACE:
    e = dict(FACTS)[o]
    print(f"  {o:26s}{e:11s}{'already visible' if o in g else 'RETURNS'}")
print()
print("bundle          visible  returned  open surface  open axes")
for ad, d in (("plain", []), ("common", COMMON_BUNDLE), ("strict", STRICT_BUNDLE)):
    y = surface(d, TRACE)
    print(f"  {ad:12s} {y['visible']:7d}  {y['returned']:10d}"
          f"  {y['open_surface']:10d}  {y['open_axes']:10d}")
print()
pct = CHANNEL_COST["system-call-tracing"]
ys = surface(STRICT_BUNDLE, TRACE)
print("cost: baseline", BASELINE_TIME, "-> run", BASELINE_TIME * (100 + pct) // 100,
      "| percent", pct)
print("cost per returned fact:", round(pct / ys["returned"], 2), "percentage points")
print("channel costs (model percentages , not a measurement):", CHANNEL_COST)
print()
print("FACT SET SWEEP -- removal (strict bundle + tracing channel)")
returned_falls, visible_falls = [], []
for o, _ in FACTS:
    y = surface(STRICT_BUNDLE, TRACE, [x for x in FACTS if x[0] != o])
    if y["returned"] < ys["returned"]:
        returned_falls.append(o)
    elif y["visible"] < ys["visible"]:
        visible_falls.append(o)
print("  baseline open surface", ys["open_surface"],
      "| changes it", len(returned_falls) + len(visible_falls),
      "| does not", len(FACTS) - len(returned_falls) - len(visible_falls))
print("    lowers returned:", ", ".join(returned_falls), "-> returned 1 , open surface 11")
print("    lowers visible :", len(visible_falls), "facts -> visible 9 , open surface 11")
print()
print("FACT SET SWEEP -- addition (one new fact per axis)")
for e in sorted({e for _, e in FACTS}):
    y = surface(STRICT_BUNDLE, TRACE, FACTS + [("new-fact", e)])
    print(f"  +{e:10s} facts 25  visible {y['visible']:2d}  returned {y['returned']}"
          f"  open surface {y['open_surface']:2d}")
facts: 24 | axes: 9
strict bundle: mechanisms 8 | visible 10 | closed 14

fact shown by channel       axis       status under strict bundle
  root-filesystem-tree      mount      already visible
  other-mounts              mount      RETURNS
  file-ownership            user       already visible
  capability-set            privilege  RETURNS
  clock                     kernel     already visible

bundle          visible  returned  open surface  open axes
  plain             24           0          24           9
  common            12           2          14           7
  strict            10           2          12           7

cost: baseline 1000 -> run 1340 | percent 34
cost per returned fact: 17.0 percentage points
channel costs (model percentages , not a measurement): {'system-call-tracing': 34, 'sampling-profile': 3, 'in-kernel-tracing': 6, 'core-dump': 0}

FACT SET SWEEP -- removal (strict bundle + tracing channel)
  baseline open surface 12 | changes it 12 | does not 12
    lowers returned: other-mounts, capability-set -> returned 1 , open surface 11
    lowers visible : 10 facts -> visible 9 , open surface 11

FACT SET SWEEP -- addition (one new fact per axis)
  +host       facts 25  visible 10  returned 2  open surface 12
  +kernel     facts 25  visible 11  returned 2  open surface 13
  +label      facts 25  visible 10  returned 2  open surface 12
  +mount      facts 25  visible 10  returned 2  open surface 12
  +network    facts 25  visible 10  returned 2  open surface 12
  +privilege  facts 25  visible 10  returned 2  open surface 12
  +process    facts 25  visible 10  returned 2  open surface 12
  +resource   facts 25  visible 10  returned 2  open surface 12
  +user       facts 25  visible 10  returned 2  open surface 12

Oracle, Channel, Returning Fact

Three numbers sit side by side. Oracle: the process’s 24 facts, over nine axes. Channel: system call tracing shows five facts. Returning fact and open surface: 2 of these five facts are ones the strict bundle closed, and they return; the open surface climbs from 10 to 12.

The returning pair is other-mounts and capability-set. The first is a fact the mount namespace closes: even if the root filesystem tree has been changed, which mount points the process reads still show up in the call sequence. The second is a fact capability dropping closes: which privilege the process is trying to use is readable even from a call’s failed return. Both are facts the mechanisms call “closed,” and the channel reopens both.

The axis count, meanwhile, stays at 7. The reason is that the two returning facts’ axes already had something open: root-filesystem-tree on the mount axis, file-permissions on the privilege axis were already visible under the strict bundle too. So this channel opens no new axis, it thickens the open axes. The distinction matters: the axis is the subject of an isolation decision, the fact is the measure of that decision’s quality.

Five Facts Shown, Two New

Three of the five facts the channel shows — root-filesystem-tree, file-ownership, clock — were already visible under the strict bundle. Tracing them opens nothing new; it only repeats existing visibility.

The reading that follows is this topic’s counterpart of the course’s second claim: a tool showing a lot does not mean it brings back a lot. System call tracing is the most detailed of the four channels; it gives every call, its arguments, and its return value. Yet the number of facts it brings back is 2, and that is not the highest number among the four channels. There is no direct proportion between the number of lines a tool produces and the gap it opens in isolation.

The bundle table says the same thing from another angle. With no isolation, the channel brings back 0 facts — there is no closed fact to bring back, all 24 are already visible. In the common six-mechanism bundle, the return is 2; in the strict eight-mechanism bundle, still 2. The two mechanisms added narrow this channel’s gap not at all: the user namespace leaves file-ownership open, and mandatory labeling closes a fact not on this channel’s list. Tightening isolation does not close the surface a channel opens on its own.

Cost: The Run Slows by 34 Percent

The channel’s cost is that it slows the run. The model’s baseline run is 1000 time units; with tracing on it becomes 1340, that is, 34 percent. This number and the other three channels’ numbers are percentages in the model, not a real measurement; the real value would come out differently depending on the machine, the load, and the kind of call traced. The reason they stand as they do in the model is to keep the order of magnitude fixed.

The cost’s source is structural. Tracing hands control to the observing side on every system call the observed process makes; one call turns into several context switches. So the cost grows with call density: a compute-heavy run is affected little, a file- and network-heavy run a lot. The model takes the cost as fixed (OD6), and on a real system this assumption is a rough approximation.

Cost per returning fact is 17.0 percentage points. This ratio will be the comparison unit for the rest of the topic; for now, on its own, it means tracing is an expensive and narrow channel. The cost’s second face falls outside the measurement: a slowed process changes its time-sensitive behavior. Timeouts fill at different points, race conditions land in a different order. Tracing does not just observe, it changes what it observes; an intermittent fault can disappear while tracing is on.

Fact Set Sweep

There is no second seed in this course; instead the fact set is swept. In the removal sweep, 24 facts are dropped one at a time from the list. The result: the open surface changes with 12 facts, does not change with 12. Of those that change it, only 2other-mounts and capability-set — lower the returned count; the remaining 10 are already-visible facts, and removing them lowers the visible count.

The addition sweep says something sharper. When one new fact is added to each of the nine axes, the open surface grows on only one axis: kernel. The reason sits directly in the mechanism table — none of the eight mechanisms closes the kernel axis. Kernel version, kernel settings, system load, and the clock stay visible as is, even under the strict bundle. In the model, every new fact added to this axis is written straight to the open surface; facts added to the other eight axes stay closed.

This result also shows where the measurement depends on the mock setup. The open surface’s size depends on the chosen 24 facts, and a different list would give a different number. The open surface’s sensitivity, though, depends not on the list but on the mechanism table: on whichever axis has no closer, the surface grows on that axis.

Narrowing the Surface

This lesson counts a gap, not how to use it. The course’s boundary is binding: no escape, privilege escalation, or evasion procedure is written in any lesson. Every counted surface is written up together with its narrowing path, and this channel has four.

The first is the tracing capability itself. Tracing a process requires a separate privilege; adding that privilege to the capability-dropping list cuts off tracing started from inside the bundle. The second is the tracer’s position: if the observing side sits outside the bundle (OD1), the counted gap is open; if the tracer is brought inside the same bundle, the channel becomes bound by the bundle’s restrictions too. The third is the tracing output itself; if the dump is written to a file, that file carries every path and privilege attempt the process saw, and its access has to be restricted. The fourth is scope: tracing is turned on for the duration of the diagnosis and with a narrow call filter, not left on continuously.

All four say one thing in common. The channel is not a flaw in isolation; it is a separate access-control question. When the isolation decision and the observation decision are not made in the same place, the second decision silently undoes the first. The principle of least privilege was itself established in the Cybersecurity curriculum and is not repeated here; its counterpart here is one sentence: whoever can trace a bundle is whoever can see some of the facts that bundle closes, and that visibility is written not in the bundle’s configuration but in the tracer’s privilege.

Summary

  • The strict bundle closes 14 of 24 facts and leaves 10 visible; system call tracing brings back 2 of the closed ones and the open surface becomes 12.
  • Three of the five facts the channel shows were already visible: a tool showing a lot is not a tool that brings back a lot.
  • The returning pair is other-mounts and capability-set; open axes stay at 7, meaning the channel opens no new axis, it thickens open ones.
  • The cost is 34 percent (baseline 1000, run 1340) and 17.0 percentage points per returning fact; these percentages are model values, not a real measurement.
  • Sweep: the open surface changes with 12 of 24 facts; a newly added fact grows the surface only on the kernel axis, because no mechanism closes that axis.
  • Narrowing: dropping the tracing capability, bringing the tracer inside the bundle, access control on the dump file, and a narrow scope.

Next Step

This channel was expensive and narrow. The next channel sits at the opposite end: a sampling-based CPU profile slows the run by only 3 percent, that is, it costs less than a tenth of tracing. The next lesson counts how many facts this cheap channel brings back — the answer is 1, and it is a fact the control group closes. The table will show that cheapness alone is not a virtue, and that growing the sample count never changes the returning fact at all.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close