Lesson 08 / 11
Guest and Host System
Measuring escape risk by the number of shared items: the eleven items the guest and host share, the path each item opens and its classification by transit, data, or information effect, the paths closed by turning off optional shares along with their cost in work steps and host memory, and the six paths that cannot be closed while the guest is running.
Published Updated
In this lesson
- Counts the items the guest and host share and writes how many paths each item opens
- Splits paths into transit, data, and information effect and measures how many paths each class loses when closed
- States the cost of closing in two units: changed legitimate work steps and additional host memory
Contents
The previous lesson counted layers stacked on top of one another: guest, hypervisor, host, hardware. In that ordering, each layer carried the one before it. But there is not just a single stack between guest and host; while both run, they use a set of items at the same time. The same scheduler queues both sides’ threads, the same physical memory pages get merged, the disk and network card the guest sees are emulation code running on the host, and on top of that there is a shared folder, a clipboard, and an agent channel opened for convenience.
Escape is the guest boundary being crossed once and something on the host side being reached. This lesson measures escape not as a probability but as the number of shared items: how many items the guest and host share, how many paths each shared item opens, and how many of those paths can be closed. The escape step itself is not written; the path’s name, its effect, and its cost are written. Trying an escape path on a real system, without written authorization and a defined scope, is both illegal and unprofessional.
VR7 — A shared item is a component used jointly with the host while the guest runs; the list on the fictional branch server is a model, the count is real. VR8 — A path is an outcome reached on the host side through a single shared item. VR9 — A path’s effect is one of three classes: transit (running code in the host process), data (touching host data), information (inference only). VR10 — A required item is an item that cannot be closed while the guest is running. VR11 — Cost is written in two units: changed legitimate work steps and additional host memory. VR12 — The branch server has four guests, split into two trust levels.
Shared Items
Shared items are of two kinds. One part exists as long as the guest runs: the guest must obtain processor time, see a disk, and write to a network card, and that disk and network card are also emulation code running on the host. The other part is opened for convenience and can be closed with a single configuration item.
// sharing.mjs — items the guest and host share, and the paths each item opens (MODEL). // Paths are named on the defense axis; not exploit code, only the path's name and effect are written. // effect: transit = moving into the host process, data = touching host data, information = inference. // [item, required?, [[path, effect], ...], [changed legitimate work steps, extra host memory MiB]] export const SHARING = [ ['scheduler', true, [["neighboring guest's activity timing", 'information']], [0, 0]], ['processor cache', true, [['access pattern from a shared cache line', 'information']], [0, 0]], ['clock source', true, [["inferring the host's maintenance window", 'information']], [0, 0]], ['disk controller emulation', true, [['transit into the host process via an emulation bug', 'transit']], [0, 0]], ['network interface emulation', true, [['transit into the host process via an emulation bug', 'transit'], ["guest packet into the host's network stack", 'data']], [0, 0]], ['memory page merging', false, [['measuring whether the same page exists', 'information'], ['page sharing inferred from write latency', 'information']], [0, 1856]], ['display controller emulation', false, [['transit into the host process via an emulation bug', 'transit']], [2, 0]], ['shared folder', false, [["writing to the host's file system", 'data'], ['a file from the host running inside the guest', 'data']], [2, 0]], ['clipboard', false, [["reading the host's clipboard", 'data'], ['passing data from host to guest', 'data']], [1, 0]], ['guest agent channel', false, [['running a command in the guest from the host', 'data'], ["reading the guest's file system from the host", 'information']], [4, 0]], ['USB passthrough', false, [["a plugged-in device reaching the host's driver", 'transit']], [1, 0]], ]; export const EFFECT = ['transit', 'data', 'information']; const WIDTH = { transit: 9, data: 6, information: 13 }; export const pathCount = (list) => list.reduce((t, [, , y]) => t + y.length, 0); export const effectCount = (list, e) => list.reduce((t, [, , y]) => t + y.filter(([, k]) => k === e).length, 0); if (import.meta.url === `file://${process.argv[1]}`) { const s = (x, n) => String(x).padStart(n); const T = pathCount(SHARING); console.log(`${SHARING.length} shared items, ${T} paths; average per item ` + `${(T / SHARING.length).toFixed(2)} paths`); console.log(`\n${'item'.padEnd(30)}${'type'.padEnd(14)}${s('paths', 7)}${s('transit', 9)}${s('data', 6)}` + `${s('information', 13)}${s('extra steps', 13)}${s('extra memory', 14)}`); for (const k of SHARING) { const [ad, required, y, [adim, mib]] = k; console.log(`${ad.padEnd(30)}${(required ? 'required' : 'optional').padEnd(14)}${s(y.length, 7)}` + EFFECT.map((e) => s(effectCount([k], e), WIDTH[e])).join('') + `${s(required ? '-' : adim, 13)}${s(required ? '-' : mib, 14)}`); } for (const [ad, f] of [['required total', true], ['optional total', false]]) { const g = SHARING.filter(([, z]) => z === f); const [adim, mib] = g.reduce(([a, m], [, , , [x, y]]) => [a + x, m + y], [0, 0]); console.log(`${ad.padEnd(44)}${s(pathCount(g), 7)}` + EFFECT.map((e) => s(effectCount(g, e), WIDTH[e])).join('') + `${s(f ? '-' : adim, 13)}${s(f ? '-' : mib, 14)}`); } }
11 shared items, 16 paths; average per item 1.45 paths item type paths transit data information extra steps extra memory scheduler required 1 0 0 1 - - processor cache required 1 0 0 1 - - clock source required 1 0 0 1 - - disk controller emulation required 1 1 0 0 - - network interface emulation required 2 1 1 0 - - memory page merging optional 2 0 0 2 0 1856 display controller emulation optional 1 1 0 0 2 0 shared folder optional 2 0 2 0 2 0 clipboard optional 2 0 2 0 1 0 guest agent channel optional 2 0 1 1 4 0 USB passthrough optional 1 1 0 0 1 0 required total 6 2 1 3 - - optional total 10 2 5 3 10 1856
Eleven items open sixteen paths; the average per item is 1.45. That average alone is a warning: closing a shared item does not always close one path — five of the eleven carry two, as does leaving one open.
The effect columns split the list in two. The five required items open six paths, and three of those are pure information inference: when the neighbor runs, which memory page is shared, the host’s maintenance window. Two are transit, and both come from a piece of emulation code. Since the guest must see a disk and a network card, those two pieces of emulation code run on the host in every run; these two transit paths are not a closable item.
The six optional items open ten paths and the distribution flips: five of six data paths are here. The clipboard, the shared folder, and the agent channel — all three items opened to make one person’s work easier — carry five of the six host-data paths. The right two columns write the cost of that: ten work steps and 1,856 MiB of host memory.
What Closing Closes, and What It Leaves
The run below closes the optional items one by one — first the one that opens the most paths, and on a tie the cheaper one — and writes the closed path together with its cost at each step.
// closing.mjs — optional shares are closed one by one; the closed path and its cost are counted together. import { SHARING, EFFECT, pathCount, effectCount } from './sharing.mjs'; const T = pathCount(SHARING); const optional = SHARING.filter(([, z]) => !z) .sort((a, b) => b[2].length - a[2].length || a[3][0] - b[3][0]); // most paths first, then the cheaper one const s = (x, n) => String(x).padStart(n); console.log(`${'closed item'.padEnd(30)}${s('closed', 8)}${s('total', 7)}${s('remaining', 11)}` + `${s('extra steps', 13)}${s('extra memory', 14)}`); let closed = 0, steps = 0, mem = 0; for (const [ad, , y, [a, m]] of optional) { closed += y.length; steps += a; mem += m; console.log(`${ad.padEnd(30)}${s(y.length, 8)}${s(closed, 7)}${s(T - closed, 11)}${s(steps, 13)}${s(mem, 14)}`); } const remaining = SHARING.filter(([, z]) => z); console.log(`\nclosable ${optional.length}/${SHARING.length} items, closed ${closed}/${T} paths ` + `(${((100 * closed) / T).toFixed(1)}%); cost ${steps} work steps and ${mem} MiB of host memory`); console.log(`${'effect'.padEnd(13)}${s('initial paths', 15)}${s('remaining paths', 17)}`); for (const e of EFFECT) console.log(`${e.padEnd(13)}${s(effectCount(SHARING, e), 15)}${s(effectCount(remaining, e), 17)}`); console.log(`while the guest runs, ${remaining.length} items cannot be closed, leaving ${pathCount(remaining)} paths`); // Layout: four fictional guests on the same host, two trust levels. const GUESTS = [['aggregator', 'high'], ['validator', 'high'], ['field portal', 'low'], ['test', 'low']]; const pairs = GUESTS.flatMap((a, i) => GUESTS.slice(i + 1).map((b) => [a[1], b[1]])); const mixed = pairs.filter(([a, b]) => a !== b).length; console.log(`\nsingle host: ${GUESTS.length} guests, ${pairs.length} neighbor pairs, ${mixed} pairs from different levels`); console.log(`separate host per level: ${pairs.length - mixed} pairs, 0 from different levels; ` + `remaining paths still ${pathCount(remaining)}, cost: 1 more machine`);
closed item closed total remaining extra steps extra memory memory page merging 2 2 14 0 1856 clipboard 2 4 12 1 1856 shared folder 2 6 10 3 1856 guest agent channel 2 8 8 7 1856 USB passthrough 1 9 7 8 1856 display controller emulation 1 10 6 10 1856 closable 6/11 items, closed 10/16 paths (62.5%); cost 10 work steps and 1856 MiB of host memory effect initial paths remaining paths transit 4 2 data 6 1 information 6 3 while the guest runs, 5 items cannot be closed, leaving 6 paths single host: 4 guests, 6 neighbor pairs, 4 pairs from different levels separate host per level: 2 pairs, 0 from different levels; remaining paths still 6, cost: 1 more machine
When six items are closed, ten of the sixteen paths close and six remain. The first row looks free at first glance: memory page merging closes two information paths and the changed work step count is zero. The cost is not in the workflow but in memory — 1,856 MiB of host memory, because merged pages are now kept separately. An item whose cost looks like zero usually has its cost in another column.
The effect table shows the real gain. Data paths drop from six to one; the single remaining data path is the one carried by the network emulation. So five out of six paths touching host data were inside the closable convenience items. Transit paths drop from four to two, information paths from six to three. The total cost is ten work steps: file transfer is done over the network, closing and reopening procedures are run by hand, and reading field devices moves to the branch client.
The remaining six paths are this lesson’s boundary. As long as the guest runs, a scheduler, a processor cache, a clock source, a disk, and a network interface are shared. These cannot be closed, because closing them means the guest does not run either. What remains is to ask how readily those six paths operate.
Items Required per Path
Up to this point, every path was matched with a single shared item. Read that way, it looks as if the share’s existence is the path’s only condition; for most paths that is not true. The run below gives each path the extra conditions required on top of the share and works out how many items must be true at the same time to cross the boundary.
// conditions.mjs — number of items that must be true at the same time, for each path (MODEL). // The shared item's existence is one item; the list below gives it EXTRA conditions. import { SHARING } from './sharing.mjs'; const EXTRA = { "neighboring guest's activity timing": [], 'access pattern from a shared cache line': ['being scheduled onto the same core as the neighbor'], "inferring the host's maintenance window": [], 'transit into the host process via an emulation bug': ['an unpatched emulation bug'], "guest packet into the host's network stack": ['the host interface being bridged to the guest network'], 'measuring whether the same page exists': ['identical page content in the neighbor'], 'page sharing inferred from write latency': ['identical page content in the neighbor'], "writing to the host's file system": ['the mount being writable'], 'a file from the host running inside the guest': ['execute permission in the guest', 'the file being opened'], "reading the host's clipboard": [], 'passing data from host to guest': [], 'running a command in the guest from the host': ['agent privilege on the host side'], "reading the guest's file system from the host": ['agent privilege on the host side'], "a plugged-in device reaching the host's driver": ['physical access', 'an unpatched driver bug'], }; // [required items, path name, from a required item?] const PATHS = SHARING.flatMap(([, z, y]) => y.map(([ad]) => [1 + EXTRA[ad].length, ad, z])); const s = (x, n) => String(x).padStart(n); console.log(`${'required items'.padEnd(16)}${s('paths', 6)} example`); for (const g of [...new Set(PATHS.map(([n]) => n))].sort()) { const k = PATHS.filter(([n]) => n === g); console.log(`${String(g).padEnd(16)}${s(k.length, 6)} ${k[0][1]}`); } const required = PATHS.filter(([, , z]) => z); console.log(`\nrequired items in the ${required.length} paths that cannot be closed: ` + [...new Set(required.map(([n]) => n))].sort() .map((g) => `${g} -> ${required.filter(([n]) => n === g).length} paths`).join(', ')); const secondary = PATHS.flatMap(([, ad]) => EXTRA[ad]); const counts = [...new Set(secondary)].map((k) => [k, secondary.filter((x) => x === k).length]) .sort((a, b) => b[1] - a[1]); console.log(`most frequent extra condition: ${counts[0][0]} (${counts[0][1]} paths); total ${secondary.length} extra conditions, ` + `${counts.length} distinct items`);
required items paths example 1 4 neighboring guest's activity timing 2 10 access pattern from a shared cache line 3 2 a file from the host running inside the guest required items in the 6 paths that cannot be closed: 1 -> 2 paths, 2 -> 4 paths most frequent extra condition: an unpatched emulation bug (3 paths); total 14 extra conditions, 10 distinct items
Four of the sixteen paths work with a single item: if the share is open, the path is open, no other condition is required. Those four are the scheduler, the clock source, and the clipboard’s two directions; none of the three depends on a bug or a placement — each exists merely by the share’s definition. Ten paths need two items, two paths need three items.
Two of the six paths that cannot be closed work with a single item, and both are information paths. The remaining four want a second item, and the most frequently required second item is an unpatched emulation bug — the same condition in three paths. This ties the measure of crossing the boundary to patching: on a host where the emulation code is patched, those three paths require two items and the second is not in place. The patch program itself is not this course’s subject, but the number here writes what it changes: it closes the second item of three transit paths.
The Six Paths That Cannot Be Closed
What can be done for a path that cannot be closed is changing where it reaches. The last two rows measure this. While four guests run on the same host, six neighbor pairs form, and four of them are between different trust levels: the guest that aggregates meter readings and the test guest, the validator and the field portal. The remaining six paths work between these four pairs.
Distributing the guests across two separate hosts by trust level closes none of the paths — each host still has six paths — but it drops different-level pairs from four to zero. In the remaining two pairs, the neighbors are already systems that access the same data; the information one neighbor extracts from the other is information it already has within its own boundary. The cost is one machine.
This measurement gives an ordering, not a preference. First the six optional items are closed: ten paths go, at a cost of ten work steps and 1,856 MiB. Then the layout is corrected: no path goes, but the ends of the remaining six paths are tied to the same trust level, at a cost of one machine. There is no third step; as long as the guest runs, those six paths are open, and the defense’s answer there is not closing but watching — which piece of emulation code returns an unexpected error, which guest shows activity during its neighbor’s maintenance window.
Summary
- The guest and host share eleven items, and these items open sixteen paths; the average per item is 1.45 paths.
- The five required items open six paths: two transit, one data, three information. The two transit paths come from disk and network emulation and cannot be closed while the guest runs.
- The six optional items open ten paths and carry five of the six data paths; closing them, data paths drop from six to one, transit from four to two, information from six to three.
- Cost is written in two columns: ten changed work steps and 1,856 MiB of host memory. Memory page merging’s work-step cost is zero; its cost is in memory.
- Four of the sixteen paths work with a single item; of the six remaining paths, two need one item and four need two, and the most frequently required second item is an unpatched emulation bug.
- Splitting the layout by trust level does not change the path count; it drops the different-level neighbor pair count from four to zero, at a cost of one machine.
Next Step
This lesson’s and the previous lesson’s measurement were both about a running guest: which layer it stands beneath, which items it shares with the host. But a virtual machine’s real distinguishing feature does not show up while it runs, but while it is stopped and copied. A guest snapshot can be taken and stored, an image can be cloned thirty times and distributed to thirty machines, a state from a week ago can be returned to in a few seconds. The next lesson measures these three operations in boundary items: how many secret items a clone carries to how many copies, how many sound links a rollback needs as the snapshot chain grows longer, and how many closed vulnerabilities returning to a week ago reopens.
To keep your progress and take notes, Log in
My notes
Log in to take notes.