# 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.

Author: Berkan YILDIZ
Published: 2026-09-27
Tags: Hyper-V, Reverse engineering, Windows internals, Intel x64
Canonical: <https://blog.berkan.fr/hvix64-partition-lifecycle>


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](/hvix64-execution-engine) 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` |

```text 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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/tlfs) and [VSM description](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/vsm) 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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/hypercalls/hvcallcreatepartition). 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](https://www.microsoft.com/en-us/msrc/blog/2018/12/first-steps-in-hyper-v-research) 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:

```asm HvCallGetPartitionId · RVA 0x2836E0 {3,10-11}
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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/datatypes/hv_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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/datatypes/hv_partition_id).

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:

```c Reduced 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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/hypercalls/hvcallgetnextchildpartition) 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:

```c Reduced 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:

```c Reduced 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.](/images/blog/hvix64-partition-structures.svg)

*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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/virtualization-host) 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](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/tlfs/hypercalls/hvcalldeletepartition). 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](/hvix64-execution-engine), from VMCS ownership and guest entry to host interrupts and VTL switching.

