Skip to the content.

Kernel Driver Integration

Purpose

AcTelemetry.sys provides kernel-originated process and image-load telemetry for one target PID. It exports a versioned IOCTL interface to one privileged user-mode collector.

The driver does not perform process-memory scanning or enforcement. The user-mode collector remains responsible for module inventory, virtual-memory classification, JSON serialization, and remote integration.

Components

File Purpose
include/ac_driver_protocol.h Shared ABI, IOCTL values, structures, constants.
driver/src/driver.c WDM driver implementation.
driver/AcTelemetry.vcxproj x64 WDK build project.
driver/AcTelemetry.inf Driver package metadata.
src/kernel_client.c Reference user-mode protocol client.

Device contract

Property Value
NT device name \Device\AcTelemetry
DOS symbolic link \DosDevices\AcTelemetry
Win32 path \\.\AcTelemetry
Device type 0x8337
Transfer method METHOD_BUFFERED
Handle mode Exclusive
Device characteristic FILE_DEVICE_SECURE_OPEN
Device ACL SYSTEM: full access

Security descriptor:

D:P(A;;GA;;;SY)

The device is created through IoCreateDeviceSecure with a project-specific class GUID.

Protocol compatibility

Current protocol:

#define AC_DRIVER_PROTOCOL_VERSION 2u

The following structure sizes are part of the ABI:

Structure Size
AcDriverVersion 16 bytes
AcDriverTargetRequest 24 bytes
AcDriverStats 56 bytes
AcDriverEvent 584 bytes

The shared header contains compile-time size assertions. All structures use fixed-width integer fields and 8-byte packing.

A client must call IOCTL_AC_GET_VERSION before any other request and reject the device when:

IOCTL reference

IOCTL_AC_GET_VERSION

Access: FILE_READ_DATA.

Input: none.

Output:

typedef struct AcDriverVersion {
    uint32_t size;
    uint32_t protocol_version;
    uint32_t event_size;
    uint32_t queue_capacity;
} AcDriverVersion;

Returns:

IOCTL_AC_SET_TARGET

Access: FILE_WRITE_DATA.

Input:

typedef struct AcDriverTargetRequest {
    uint32_t size;
    uint32_t protocol_version;
    uint32_t target_pid;
    uint32_t reserved;
    uint64_t session_id;
} AcDriverTargetRequest;

Rules:

Only one target is active at a time.

IOCTL_AC_READ_EVENTS

Access: FILE_READ_DATA.

Input: none.

Output: contiguous array of AcDriverEvent.

Rules:

IOCTL_AC_GET_STATS

Access: FILE_READ_DATA.

Input: none.

Output:

typedef struct AcDriverStats {
    uint32_t size;
    uint32_t protocol_version;
    uint64_t events_generated;
    uint64_t events_dropped;
    uint64_t next_sequence;
    uint32_t target_pid;
    uint32_t queue_depth;
    uint32_t queue_capacity;
    uint32_t callbacks_active;
    uint64_t session_id;
} AcDriverStats;

callbacks_active bit assignments:

Bit Meaning
0x1 Process callback registered.
0x2 Image-load callback registered.

The client must verify session_id and monitor events_dropped. Any increase indicates that the oldest queued event was overwritten.

Event structure

typedef struct AcDriverEvent {
    uint32_t size;
    uint32_t protocol_version;
    uint64_t sequence;
    uint64_t timestamp_100ns;
    uint32_t type;
    uint32_t flags;
    uint32_t process_id;
    uint32_t parent_process_id;
    int32_t status;
    uint32_t reserved;
    uint64_t image_base;
    uint64_t image_size;
    uint16_t image_path[260];
} AcDriverEvent;

Field contract:

Field Contract
size sizeof(AcDriverEvent).
protocol_version Current driver protocol.
sequence Monotonic driver-local sequence.
timestamp_100ns Kernel system time in 100-nanosecond intervals.
type AcDriverEventType.
flags Path and image classification flags.
process_id Target PID.
parent_process_id Parent PID for process creation.
status NTSTATUS where applicable.
reserved Zero.
image_base 64-bit image base representation.
image_size Mapped image size.
image_path Null-terminated UTF-16 path, bounded to 259 characters.

Event types:

Value Name Source
1 TARGET_CHANGED Successful target update.
2 PROCESS_CREATED Process callback.
3 PROCESS_EXITED Process callback.
4 IMAGE_LOADED Image-load callback.

Flags:

Value Name
0x00000001 PATH_TRUNCATED
0x00000002 SYSTEM_IMAGE
0x00000004 PATH_UNAVAILABLE

Queue behavior

The device extension contains a fixed ring of 512 events in nonpaged memory. Callbacks do not allocate memory.

When the queue is full:

  1. the oldest event is removed;
  2. events_dropped is incremented;
  3. the new event is inserted.

Queue operations and target changes use a spin lock. Callback code copies only bounded metadata and does not inspect target address space.

Callback behavior

Process callback

Registered with:

PsSetCreateProcessNotifyRoutineEx(AcProcessNotify, FALSE);

The callback emits:

It does not modify CreationStatus.

Image callback

Registered with:

PsSetLoadImageNotifyRoutine(AcImageNotify);

The callback records:

The callback does not map, read, or modify user memory.

Unload

Unload order:

  1. remove image-load callback;
  2. remove process callback;
  3. delete symbolic link;
  4. delete device object.

The driver must not be unloaded while an integration continues to require kernel telemetry.

Reference client sequence

HANDLE device = CreateFileW(
    L"\\\\.\\AcTelemetry",
    GENERIC_READ | GENERIC_WRITE,
    0,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);

DeviceIoControl(device, IOCTL_AC_GET_VERSION, ...);
DeviceIoControl(device, IOCTL_AC_SET_TARGET, ...);

for (;;) {
    DeviceIoControl(device, IOCTL_AC_READ_EVENTS, ...);
    DeviceIoControl(device, IOCTL_AC_GET_STATS, ...);
}

DeviceIoControl(device, IOCTL_AC_SET_TARGET, target_pid_zero, ...);
CloseHandle(device);

Use src/kernel_client.c as the canonical implementation. It:

Launcher integration

Recommended launch sequence:

  1. verify the installed driver and collector version matrix;
  2. start the driver service;
  3. create the protected process suspended;
  4. obtain its PID and process creation time;
  5. start the collector with --pid <pid> --require-kernel;
  6. wait for kernel_driver_connected;
  7. resume the protected process;
  8. forward JSONL records to the remote collector;
  9. on process exit, wait for agent_stopped;
  10. stop or retain the driver according to the product lifecycle.

Target registration occurs after process creation. Therefore the driver cannot emit the original process-creation callback for that PID in this sequence. Creating the process suspended minimizes missed post-creation image loads. The user-mode scanner inventories the current modules on its first scan.

Error handling

Client policy:

Condition --kernel --require-kernel
Device not found Continue user-mode only. Exit nonzero.
Access denied Continue user-mode only. Exit nonzero.
Protocol mismatch Continue user-mode only. Exit nonzero.
Target rejected Continue user-mode only. Exit nonzero.
Read failure Close driver, continue. Close driver, exit nonzero.
Queue overflow Emit health event, continue. Emit health event, continue.

An integrator may apply a stricter policy outside the collector.

Build, signing, and packaging

Build:

msbuild driver\AcTelemetry.vcxproj `
  /p:Configuration=Release `
  /p:Platform=x64

Package contents:

Production requirements:

Driver API exclusions

The protocol intentionally has no operation for:

These operations are not required for the telemetry integration described by this document.