Skip to content

Berkan Berkan YILDIZ Blog

All posts

Inside hvix64: partition IDs, ownership and lifetime

Following Hyper-⁠V's partition hypercalls to recover the current-partition context, child enumeration, ownership checks and synchronization around teardown.

12 min read Markdown
On this page
  1. The binary and the boundaries
  2. Start with the interface, then find the handlers
  3. A tiny handler identifies the current partition
  4. An ID lookup also carries an access policy
  5. Recover the child list through enumeration
  6. The counter beside the list is synchronization
  7. Creation, finalization and deletion are separate steps
  8. What to carry into the next investigation

A partition ID is a 64-bit integer at the hypercall boundary. Inside hvix64.exe, using that integer involves a current execution context, an object lookup, an ownership decision and protection against concurrent teardown.

This first part of a two-part series follows that path through an Intel Hyper-⁠V binary; the second part follows guest execution on the same build. The useful result is a small, cross-checked map of the partition object, together with the code that explains what those fields mean. There is no vulnerability claim here; the interesting part is how the implementation separates finding an object from being allowed to use it.

The binary and the boundaries

The analysis covers this exact file, examined statically in an existing annotated IDA 9.4 database with Hex-Rays:

Property Value
File hvix64.exe
File version 10.0.26100.9444
Architecture AMD64, Intel Hyper-⁠V
File size 2,057,688 bytes
Preferred image base 0xFFFFF80000000000
SHA-256
cf5af317f300b7da25f37d5dc6ca75bff91b6b868d89f337d23ec1773340ce8a

Every address below is an RVA, relative to that image base. A live hypervisor can load elsewhere. The structure offsets are observations from this build, not an ABI to copy into another Windows version.

The database already contained recovered types and function names, including _HV_PARTITION, _HV_PCR, HvpLookupPartitionById and HvpReleasePartitionReference. Those names are retained as analyst annotations; their presence does not establish original Microsoft symbol names. The hypercall names are cross-checked against documented call numbers and the dispatch table. C snippets are reduced reconstructions, and assembly snippets transcribe the inspected instructions with local labels shortened. Descriptive names in snippets, such as WalkNextChild, are placeholders for helpers that remain unnamed in the database.

Nothing here was validated by issuing hypercalls or racing partition deletion on a running machine. Static control flow can establish the checks and data accesses below; it cannot establish that every configuration reaches the same path.

Start with the interface, then find the handlers

In Hyper-⁠V terminology, a partition is an isolation boundary containing an operating system. The root partition hosts the management stack; child partitions host other guests. A virtual processor belongs to a partition, while a virtual trust level describes a trust context within a partition. Those are different axes. Microsoft’s TLFS glossary and VSM description provide the public definitions.

For this investigation, the useful entry points are the partition-management hypercalls. The database labels the dispatch table HvHypercallDispatchTable, at RVA 0x2000, with a stride of 0x18. Each entry begins with an eight-byte handler address, followed by the two-byte call code. The remaining fields include input and output sizes; their complete flag semantics are outside this pass.

Hypercall Code Handler RVA Fixed input / output bytes
HvCallCreatePartition 0x40 0x2831E0 56 / 8
HvCallInitializePartition 0x41 0x2838F0 8 / 0
HvCallFinalizePartition 0x42 0x283550 8 / 0
HvCallDeletePartition 0x43 0x283480 8 / 0
HvCallGetPartitionId 0x46 0x2836E0 0 / 8
HvCallGetNextChildPartition 0x47 0x2835D0 16 / 8

For example, entry 0x40 is at 0x2000 + 0x40 * 0x18 = 0x2600. Its handler points to 0x2831E0, and its 56 / 8 sizes agree with the documented AMD64 creation interface. The neighboring lifecycle entries and the small ID-query handler provide independent checks on the table interpretation.

This is also a useful way to start a database without symbols. Microsoft’s own Hyper-⁠V research introduction describes using the hypercall table and following accesses from the hypervisor’s GS-based processor context.

A tiny handler identifies the current partition

HvCallGetPartitionId is small enough to inspect in full. At RVA 0x2836E0, the relevant instructions are:

x86-64 asmHvCallGetPartitionId · RVA 0x2836E0
mov     rax, gs:360h
mov     r8, rdx
bt      qword ptr [rax+1B0h], 21h
jb      permitted
mov     eax, 6
ret
permitted:
mov     rcx, gs:360h
xor     eax, eax
mov     rdx, [rcx+4550h]
mov     [r8], rdx
ret

Three observations fall out of this handler. In this hypervisor execution context, gs:[0x360] supplies the object whose ID is returned. Bit 33 of the qword at object offset 0x1B0 gates the operation; without it, the handler returns 6, HV_STATUS_ACCESS_DENIED. On success, the output is the qword at 0x4550.

The public partition privilege mask places CreatePartitions at bit 32 and AccessPartitionId at bit 33. That matches the ID handler’s test, and the creation and deletion handlers test bit 32 at the same offset. Together, those uses support identifying 0x1B0 as the privilege mask and 0x4550 as the partition ID.

There is a decompiler trap here. Even with recovered hypervisor types, Hex-Rays renders some GS accesses through NtCurrentTeb(). That spelling does not establish a Windows user-mode TEB. The database’s _HV_PCR places CurrentPartition at 0x360 and CurrentVp at 0x358; the instructions independently confirm the partition access. Check the segment access before accepting the decompiler’s structure interpretation.

An ID lookup also carries an access policy

The initialize, finalize and enumeration handlers all call HvpLookupPartitionById, at RVA 0x2A52F0. Its first argument is the requested partition ID. Other arguments select access and protection modes, and it returns both an object pointer and a small cleanup context.

The public HV_PARTITION_ID type defines 0 as invalid and UINT64_MAX as SELF. SELF denotes the calling partition, rather than an actual ID that should be looked up in a table. See the datatype reference.

The helper implements a direct-current-object path for SELF. Its ordinary numeric lookup path can reach the helper at 0x2A5A14, which searches an index using 0x21B9F0. The search is a binary-tree walk: compare the requested 64-bit key with the node’s first qword, then follow one of two child pointers. A successful lookup subtracts an embedded-node offset to recover the enclosing partition object.

Finding the object is only part of the operation. The surrounding resolver checks relationships against the caller, with mode-dependent rules for the caller itself and its children. It also contains VTL-related comparisons and branches for additional execution configurations. Treating it as a simple global FindPartitionById() would hide those decisions.

The initialize wrapper makes the intended sequencing visible:

CReduced reconstruction · handler RVA 0x2838F0
status = HvpLookupPartitionById(input->PartitionId, 3, 1, 1,
                               &partition, &cleanup);
if (status == 0) {
    status = HvpAdvancePartitionLifeState(partition);
    HvpReleasePartitionReference(&cleanup);
}
return status;

HvpLookupPartitionById, HvpAdvancePartitionLifeState and HvpReleasePartitionReference are the database’s recovered labels for RVAs 0x2A52F0, 0x2A2C68 and 0x2A4F2C. The numeric modes are left numeric because this pass did not recover a complete, reliable enum for them.

That cleanup context matters: following only the returned object pointer discards part of the lifetime contract visible at the call site.

Recover the child list through enumeration

The enumeration interface takes a parent ID and a previous-child ID. Zero starts the walk; a successful result of zero marks its end.

At RVA 0x2835D0, the handler first resolves the parent. When a previous child is supplied, it resolves that object too and checks:

CReduced reconstruction · handler RVA 0x2835D0
if (previousChild != NULL && previousChild->ParentPartition != parent) {
    status = 0x0D;
} else {
    WalkNextChild(parent, previousChild, output);
}

0x0D is HV_STATUS_INVALID_PARTITION_ID. The parent pointer read is at previousChild + 0x4540. This is a concrete ownership check: a cursor from a different parent cannot simply be used to enter its list.

The walker at RVA 0x2A5188 then exposes the list layout. It uses a list head at parent + 0x4720 and a link at child + 0x4710. The shortened reconstruction below omits the synchronization around the walk:

CReduced reconstruction · walker RVA 0x2A5188
*nextId = 0;
head = (LIST_ENTRY *)((unsigned char *)parent + 0x4720);

if (previousChild == NULL) {
    next = head->Flink;
} else {
    next = ((LIST_ENTRY *)((unsigned char *)previousChild + 0x4710))->Flink;
}

if (next != head) {
    *nextId = *(UINT64 *)((unsigned char *)next - 0x1C0);
}

The subtraction provides a useful consistency check: 0x4710 - 0x4550 = 0x1C0. Reading next - 0x1C0 therefore retrieves the ID field independently identified by HvCallGetPartitionId.

This is stronger evidence than naming fields from a single decompiled function. Two unrelated interfaces agree on the same ID location, and the enumeration wrapper separately establishes the parent relationship.

The counter beside the list is synchronization

The omitted part of the walker operates on the dword at parent + 0x4730. It masks off bit 31, increments the remaining count with lock cmpxchg, walks the list and finishes with lock dec.

On contention, helper 0x241FA4 waits until the high bit is clear before attempting the increment again. The removal helper at 0x2A613C takes the complementary path: set bit 31, wait for the word to become exactly 0x80000000, modify the protected structures and clear the word.

Those matching paths establish a reader/writer synchronization protocol. The low bits count active readers; the high bit blocks new readers while the writer waits for existing ones to leave.

It would be easy to call this a partition reference count after seeing only the increment and decrement. The writer path shows why that name would be misleading. This word protects the lookup/index and list operations. A pointer surviving those operations still needs the separate object-protection protocol used by the resolver and deletion path.

The recovered fields can now be kept deliberately small:

Partition-relative offset Observed role Evidence RVA
0x1B0 Privilege mask 0x2836E0, 0x2831E0
0x4540 Parent partition pointer 0x2835D0
0x4550 Partition ID 0x2836E0, 0x2A5188
0x46C0 Embedded node used in the parent’s lookup index 0x2A5ACC, 0x2A4544
0x4710 Link in the parent’s child list 0x2A5188, 0x2A4544
0x4720 Head of this partition’s child list 0x2A5188
0x4730 Reader/writer word protecting child structures 0x2A5188, 0x2A613C
0x4738 Child lookup-tree descriptor 0x2A5ACC

This is a field map, not a declaration of the complete partition structure. In particular, it makes no assumption about total object size or undocumented padding.

Partition P holds a privilege mask, its ID, a reader/writer word, a child lookup tree keyed by ID and a child list head. The tree reaches each child’s embedded node at 0x46C0; the list head links child A’s list link at 0x4710 to child B’s, and the last link returns to P. Each child also holds a parent pointer, its ID and a rundown word at offset 8.

The same fields, placed. Two children are shown; the tree’s shape and the list order are not significant.

Creation, finalization and deletion are separate steps

The documented host lifecycle separates creating a partition, configuring it, initializing it, creating VPs and eventually finalizing and deleting it. The implementation also keeps those concerns separate.

The creation handler checks CreatePartitions before reaching the construction helpers. At RVA 0x2A4184, HvpCreatePartition writes the parent pointer into the new object. In the counter-generated-ID path, bit zero of the current partition’s flags at 0x1A0 selects the counter. When the bit is set, the global qword HvpPartitionIdCounter, at RVA 0xAA250, supplies the ID; when it is clear, ChildIdCounter at current-partition offset 0x4798 does. Both counters use interlocked increments; the global-counter observation should not be extended to every hosting configuration.

Initialization reaches HvpAdvancePartitionLifeState, at RVA 0x2A2C68. It serializes through the qword at object offset zero and branches on the state dword at 0x4C. The observed body performs setup and changes that state. Its several intermediate values are a reason to resist mapping every internal number directly onto a simplified public lifecycle diagram.

Finalization reaches HvpFinalizePartitionWorker, at 0x2A28DC, performs staged cleanup and can write state 8. Deletion reaches HvpDeletePartitionWorker, at 0x2A4544, whose initial state check accepts 0 or 8. That is consistent with removing a newly created object or one that completed finalization, rather than accepting an arbitrary active partition.

The delete handler also explicitly rejects SELF. That agrees with the public deletion contract. Its lookup helper, HvpLookupPartitionByIdForDelete at 0x2A5ACC, obtains protection through a field at object offset 8; the deletion body releases that acquisition, invokes the corresponding drain operation and then advances toward removal.

The protection helpers expose a second protocol. Acquisition at 0x21C3E0 adds 2 to a dword only while bit zero is clear. Release at 0x20E374 subtracts 2. Deletion marks bit zero through 0x391EC4 and follows with the wait helper at 0x391EE4, which checks for outstanding acquisitions. This is rundown-style object protection, separate from the high-bit reader/writer word around the parent’s child structures.

Later, deletion calls 0x2A613C for an image-wide index and, when a parent exists, for the parent’s index and child-list link. The latter call supplies the embedded node at 0x46C0, the link at 0x4710, the synchronization word at 0x4730 and the tree descriptor at 0x4738. Those are the same structures observed from the lookup and enumeration side. Before unlinking, the removal helper checks the neighboring list pointers for consistency.

This explains why a partition cannot be understood as just an allocation with an integer attached. It participates in multiple indexes, belongs to a parent, has execution state and supports operations that must finish before teardown proceeds. This pass establishes that sequence around the inspected removal path; it does not establish every allocator or deferred-reclamation detail behind the final cleanup helper.

What to carry into the next investigation

For a new build, repeat the cross-checks instead of carrying these offsets forward: identify the dispatch table, inspect the ID handler, follow enumeration, and compare insertion and removal against the same fields. A plausible-looking structure in the decompiler is a starting hypothesis.

For security analysis, the useful questions follow the implementation: which resolver mode does a handler request, which caller/parent relationship does it permit, what protection keeps the returned object usable, and does every exit release that protection? The list synchronization and the object-lifetime protocol answer different questions, even when both look like counters in a small snippet.

The next step would be to observe creation, enumeration and teardown in a matching nested Hyper-⁠V test machine, with breakpoints at these RVAs. That would test the inferred execution-context and lifetime model against actual scheduling and configuration. Until then, the claims here remain the static, build-specific ones supported by the handlers and their shared helpers.

Next in this series: the guest execution engine, from VMCS ownership and guest entry to host interrupts and VTL switching.