Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,13 @@ Implemented against vCenter 8 / ESXi 8. Default transport is `nbdssl`
- `VixDiskLib_Open` (datastore path, read-only or read-write)
- `VixDiskLib_Read` (optional ``skip_decompression`` packs FastLZ extras)
- `VixDiskLib_Write`
- `VixDiskLib_GetInfo` (capacity and physical geometry from the `Open`
reply; `biosGeo`/`adapterType`/`uuid` from `DDB_GET`, matching real
VDDK's cost and behavior)

Not implemented: compression open flags other than FastLZ, CBT /
allocated-block queries, disk geometry (`DDB_GET`), encrypted disks,
and direct ESXi `ha-nfc` without vCenter `vpxa-nfc`.
allocated-block queries, encrypted disks, and direct ESXi `ha-nfc`
without vCenter `vpxa-nfc`.

Requires Python 3.10 or later.

Expand Down
88 changes: 80 additions & 8 deletions docs/nfc_open.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ AIO types used for Open / Read / Close, correlated with the consecutive
| 9 | `SET_SOCK_OPTS` | 12 | |
| 22 | `SET_RES_POOL` | 4 | |
| 4 | `OPEN_FILE` | 60 | path string |
| 11 | `DDB_GET` | 16 | key name (VDDK only) |
| 11 | `DDB_GET` | 16 | key name |
| 7 | `IO` | 44 | sector bytes (read reply / write request) |
| 5 | `CLOSE_FILE` | 8 | |
| 3 | `CLOSE_SESSION` | 4 | |
Expand All @@ -156,6 +156,65 @@ VDDK Open also issues several `DDB_GET` queries (`resumeConsolidateSector`,
(16 zero bytes) on this unencrypted disk. They are not required to
obtain a file handle or to read sector 0.

`VixDiskLib_GetInfo` (Step 14) triggers ~20 more `DDB_GET` calls right
after `OPEN_FILE`, for these keys (captured in request-order, key name
is the extra string after the 16-byte payload, no `ddb.` prefix on the
wire): `resumeConsolidateSector`, `isDigest` (×3), `iofilters` (×2),
`logicalSectorSize` (×2), `physicalSectorSize` (×2),
`isNativeLinkedClone` (×2), `KMFilters`, `sidecars`, `adapterType`,
`uuid`, `geometry.cylinders`, `geometry.heads`, `geometry.sectors`,
`geometry.biosCylinders`, `geometry.biosHeads`, `geometry.biosSectors`.
On this lab disk, `biosGeo` came back all zeros (key not found) and
`logicalSectorSize`/`physicalSectorSize`/the non-bios `geometry.*` keys
duplicate what OPEN_FILE already returned — only `adapterType` and
`uuid` are genuinely new information from this burst.

### `DDB_GET` request/reply layout

Decoded from the same capture (request/reply pairs matched by `opId`
across all ~28 calls seen in one `GetInfo`).

Request: 16-byte fixed payload plus the key name as a raw ASCII extra
(no NUL terminator, not counted in `size` — same convention as
`OPEN_FILE`'s path):

| Offset | Type | Meaning |
| ------ | -------- | --------------------------------------- |
| 0 | `uint64` | File handle (same value as `OPEN_FILE`) |
| 8 | `uint32` | Key name length in bytes |
| 12 | `uint32` | 0 |
| 16 | — | Key name (ASCII, no `ddb.` prefix) |

Reply: 16 bytes plus a value extra, **not** padded (unlike
`QueryAllocatedBlocks`'s bitmap — verified by decoding all 28 replies
in sequence with no desync):

| Offset | Type | Meaning |
| ------ | -------- | --------------------------------------- |
| 0-11 | — | Zero/unused in every capture |
| 12 | `uint32` | Value length in bytes (`0` = not found) |
| 16 | — | Value (ASCII **text**, not binary) |

Values are ASCII text even for keys that sound numeric —
`geometry.cylinders` comes back as the literal bytes `b"2088"`, not a
binary `uint32`. This matches how a VMDK descriptor file's DDB (disk
database) section stores keys as plain-text `ddb.<key> = "<value>"`
lines; `adapterType` comes back as `b"lsilogic"` (a string), not
VDDK's numeric `VIXDISKLIB_ADAPTER_SCSI_LSILOGIC` enum value — VDDK's
own client does that string-to-enum mapping internally, which
OpenVixDiskLib does not reproduce (`DiskInfo.adapter_type` is the raw
DDB string).

Implemented as `openvixdisklib.nfc_open.NfcDisk.ddb_get(key) -> str |
None` and `NfcDisk.query_full_info() -> DiskInfo` (5 round trips:
`geometry.biosCylinders`/`biosHeads`/`biosSectors`, `adapterType`,
`uuid`), wired into `VixDiskLibHandle.get_info`, which now matches
real VDDK's `VixDiskLib_GetInfo` exactly — capacity/physGeo free from
`OPEN_FILE`, the rest costing the same 5 round trips VDDK itself pays.
Validated against the live ESXi lab: matches native VDDK's `GetInfo`
output on the same disk (`adapterType=3` ↔ `"lsilogic"`, same `uuid`
string, same zeroed `biosGeo`).

### OPEN_SESSION / sockopts / resource pool

`OPEN_SESSION` payload is 16 bytes, little-endian:
Expand Down Expand Up @@ -202,12 +261,22 @@ Reply payload (60 bytes), fields that matter:

| Offset | Type | Meaning |
| ------ | -------- | ------------------------------- |
| 8 | `uint64` | File handle (opaque, per open) |
| 16 | `uint32` | File type (`2` = `NFC_DISK`) |
| 20 | `uint32` | Flags echoed (`0x1e` or `0x1a`) |
| 36 | `uint32` | Sector size (`512` on this VM) |

Later AIO messages pass that handle as a `uint64`.
| 8 | `uint64` | File handle (opaque, per open) |
| 16 | `uint32` | File type (`2` = `NFC_DISK`) |
| 20 | `uint32` | Flags echoed (`0x1e` or `0x1a`) |
| 28 | `uint64` | Disk capacity in **bytes** |
| 36 | `uint32` | Sector size (`512` on this VM) |
| 40 | `uint32` | Physical geometry cylinders |
| 44 | `uint32` | Physical geometry heads |
| 48 | `uint32` | Physical geometry sectors |

Later AIO messages pass that handle as a `uint64`. Offset 28 was found
by capturing `VixDiskLib_GetInfo` (Step 14,
`docs/reverse_engineering_procedure.md`): it matches
`VixDiskLibInfo.capacity` converted to bytes, and offsets 40/44/48
match `VixDiskLibInfo.physGeo` exactly — both already arrive with this
reply, no separate `GetInfo` wire call exists. `biosGeo`, `adapterType`,
and `uuid` are **not** here; VDDK gets those from `DDB_GET` (below).

### IO (read / write)

Expand All @@ -232,6 +301,8 @@ classic type 4 `NFC_SESSION_COMPLETE`.
| Handshake + AIO + OPEN_FILE | `openvixdisklib.nfc_open.open_disk` |
| AIO extra size / pool count | `open_disk(..., aio_buffer_size=, aio_buffer_count=)` |
| Sector read / write / close | `openvixdisklib.nfc_open.NfcDisk` |
| Full disk info (`GetInfo`) | `openvixdisklib.openvixdisklib.VixDiskLibHandle.get_info` |
| VMDK descriptor DDB lookup | `openvixdisklib.nfc_open.NfcDisk.ddb_get` |

Run:

Expand All @@ -246,7 +317,8 @@ I/O: `docs/nfc_read.md`, `docs/nfc_write.md`, and

## What is still VDDK-only

- `DDB_GET` / geometry / zlib and skipz compression / encryption keys
- zlib and skipz compression / encryption keys (`DDB_GET` is
implemented for the plain, non-encrypted keys covered above)
- `NFC_DELTA_DISK`, change-block tracking
- Host-switch (`NFC_AIO_SWITCH_HOST_*`)
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
Expand Down
1 change: 0 additions & 1 deletion docs/nfc_read.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,3 @@ buf when skip_decompression=True: extras packed densely from offset 0
- zlib and skipz NBD compression flags
- `VixDiskLib_ReadAsync` (same IO messages, different client threading)
- `VixDiskLib_QueryAllocatedBlocks` / allocation bitmaps
- `VixDiskLib_GetInfo` capacity (not required to read a known range)
62 changes: 56 additions & 6 deletions docs/reverse_engineering_procedure.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ This file is the **sequence of steps**, including dead ends, so later
NFC work can follow the same loop instead of rediscovering it.

Scope so far: `VixDiskLib_ConnectEx` + `VixDiskLib_Open` +
`VixDiskLib_Read` + `VixDiskLib_Write` against lab vCenter 8.0.1 /
ESXi 8, transports `nbd` and `nbdssl`. Validation method:
`VixDiskLib_Read` + `VixDiskLib_Write` + `VixDiskLib_GetInfo` against
lab vCenter 8.0.1 / ESXi 8, transports `nbd` and `nbdssl`. Validation
method:
`tests/integration/` (the session-scoped `lab` fixture creates a temporary
empty VM with a 10 GiB disk and destroys it when the pytest session ends).

Expand Down Expand Up @@ -381,8 +382,58 @@ not an OPEN_FILE bit. Capture VDDK with that flag (NBD + the port-902
Replay: pip `pyfastlz` via `openvixdisklib/fastlz.py` (NFC extra is
raw FastLZ, without the wrapper's 4-byte length prefix) plus `NfcDisk`
compression on each IO. Proof:
`tests/integration/test_nfc_read_write.py` (`fastlz`) and
`tests/perf/test_compare.py`.
## Step 14 — `VixDiskLib_GetInfo` capacity

Extended the ctypes probe from Step 13 to call `VixDiskLib_GetInfo`
after `Open`, under the SSL hook plus a `write`/`read` interceptor on
fd 902 (Step 7), to see what wire traffic `GetInfo` adds.

Result: **no new SOAP or authd traffic** — the same `RetrieveContent`
+ `Login` + `NfcGetVmFiles` + authd sequence as a plain `Open`. All the
extra traffic is inside the already-open NFC/AIO session: ~20 more
`DDB_GET` (type 11) requests right after `OPEN_FILE`, for keys like
`adapterType`, `uuid`, `geometry.cylinders`, `geometry.biosCylinders`,
etc. (full list in `docs/nfc_open.md`).

Dumping every byte of the `OPEN_FILE` reply (not just the fields the
earlier Open-only capture had labeled) found `capacity` (offset 28,
`uint64` bytes) and `physGeo` (offsets 40/44/48) already present —
verified they match `VixDiskLibInfo.capacity`/`physGeo` from the same
`GetInfo` call exactly. Only `biosGeo`, `adapterType`, and `uuid` are
genuinely `DDB_GET`-only; `biosGeo` came back "key not found" (zeros)
on this unencrypted lab disk.

Fix: extended `_parse_open_reply` in `openvixdisklib/nfc_open.py` to
also read those offsets, added `nfc_open.DiskInfo`/`DiskGeometry`, and
exposed `VixDiskLibHandle.get_info()`. No new NFC message type was
needed — `DDB_GET` (`adapterType`/`uuid`/`biosGeo`) is still open work.
Validated against the live host: `capacity_sectors=33554432`
(16 GiB), `phys_geo=(2088, 255, 63)`, matching native VDDK's
`GetInfo` on the same disk.


## Step 16 — `DDB_GET` (AIO type 11)

Already partly captured as a side effect of Step 14 (`VixDiskLib_GetInfo`
triggers ~28 `DDB_GET` calls); no new capture was needed, just decoding
the request/reply pairs from that saved log by matching `opId` across
both directions. Confirmed the request's first 8 bytes equal the
`OPEN_FILE` handle from the same capture, and that a "found" reply's
extra is plain ASCII text (`b"lsilogic"`, `b"2088"`, ...), not binary —
matching how a VMDK descriptor's DDB section stores key/value pairs as
text. No padding on the reply extra (unlike Step 15's bitmap),
confirmed by decoding all 28 request/reply pairs from one capture in
sequence without desync.

Implemented as `NfcDisk.ddb_get(key) -> str | None` and
`NfcDisk.query_full_info() -> DiskInfo` (the 5 keys needed for
`bios_geo`/`adapter_type`/`uuid`), wired into
`VixDiskLibHandle.get_info` in place of the OPEN_FILE-only version
from Step 14 — `get_info` now matches real VDDK's `VixDiskLib_GetInfo`
completely, including paying the same round-trip cost. Full layout:
`docs/nfc_open.md`. Validated against the live ESXi lab: matches
native VDDK's `GetInfo` output on the same disk exactly.


## What to write down

Expand All @@ -404,8 +455,7 @@ OpenVixDiskLib.

Not yet reversed, same loop as above:

- `DDB_GET` / disk geometry, zlib/skipz compression, encrypted disks
- zlib/skipz compression, encrypted disks
- `NFC_DELTA_DISK`, CBT / `QueryAllocatedBlocks`
- `VixDiskLib_GetInfo` capacity
- Host-switch AIO messages
- Direct ESXi `ha-nfc` without vCenter `vpxa-nfc`
Loading
Loading