diff --git a/backends/ze/Makefile.am b/backends/ze/Makefile.am index 942c0947..595855e2 100644 --- a/backends/ze/Makefile.am +++ b/backends/ze/Makefile.am @@ -107,7 +107,12 @@ btx_ze_model.yaml: $(srcdir)/gen_babeltrace_ze_model.rb $(ZE_LIB_GEN) $(ZE_MODEL EXTRA_DIST += \ ze_model.rb \ - gen_babeltrace_ze_model.rb + gen_babeltrace_ze_model.rb \ + ze_validator_zemodel.rb \ + ze_validator_function_entry_exit_callbacks.rb \ + ze_validator_entry_exit_helpers.rb \ + ze_validator_state_object.rb \ + ze_deprecated.json ZE_PROBES = $(ZE_NAMESPACES:=_tracepoints) $(ZE_STRUCTS_NAMESPACES:=_tracepoints) @@ -173,7 +178,35 @@ EXTRA_DIST += \ CLEANFILES += tracer_ze.c bin_SCRIPTS = \ - tracer_ze.sh + tracer_ze.sh \ + ze_validator + +# Standalone Level Zero utility that dumps per-device command-queue-group +# topology (ordinal -> engine type + numQueues) to a JSON file. Meant to be +# invoked by a separate program. Per the requirement, it is compiled with icpx +# rather than the project's default $(CXX). +ICPX = icpx +ZE_DEVICE_PROPERTY_CXXFLAGS = -std=c++17 -Wall -Wextra -O2 +ZE_DEVICE_PROPERTY_CPPFLAGS = -I$(srcdir)/include +ZE_DEVICE_PROPERTY_LIBS = -lze_loader + +ze_device_property$(EXEEXT): $(srcdir)/ze_device_property.cpp + $(ICPX) $(ZE_DEVICE_PROPERTY_CXXFLAGS) $(ZE_DEVICE_PROPERTY_CPPFLAGS) \ + $< -o $@ $(ZE_DEVICE_PROPERTY_LIBS) + +all-local: ze_device_property$(EXEEXT) + +install-exec-local: ze_device_property$(EXEEXT) + $(MKDIR_P) $(DESTDIR)$(bindir) + $(INSTALL_PROGRAM) ze_device_property$(EXEEXT) \ + $(DESTDIR)$(bindir)/ze_device_property$(EXEEXT) + +uninstall-local: + $(RM) $(DESTDIR)$(bindir)/ze_device_property$(EXEEXT) + +CLEANFILES += ze_device_property$(EXEEXT) + +EXTRA_DIST += ze_device_property.cpp noinst_LTLIBRARIES = libzetracepoints.la @@ -242,14 +275,21 @@ EXTRA_DIST += \ gen_ze_library.rb \ gen_babeltrace_ze_lib.rb \ gen_ze_refinements.rb \ - gen_ze_bindings.rb + gen_ze_bindings.rb \ + ze_thread_safety.yaml CLEANFILES += $(ZE_BINDINGS) data_DATA = \ $(ZE_BINDINGS) \ ze_bindings_base.rb \ - babeltrace_zeprofiling_apis.txt + babeltrace_zeprofiling_apis.txt \ + ze_thread_safety.yaml \ + ze_deprecated.json \ + ze_validator_zemodel.rb \ + ze_validator_function_entry_exit_callbacks.rb \ + ze_validator_entry_exit_helpers.rb \ + ze_validator_state_object.rb xprof_utils.hpp: $(top_srcdir)/utils/xprof_utils.hpp cp $< $@ diff --git a/backends/ze/ze_deprecated.json b/backends/ze/ze_deprecated.json new file mode 100644 index 00000000..c2ccb34c --- /dev/null +++ b/backends/ze/ze_deprecated.json @@ -0,0 +1,8 @@ +{ + "zeInit": ["1.10", "zeInitDrivers"], + "zeDriverGet": ["1.10", "zeInitDrivers"], + "zeCommandListImmediateAppendCommandListsExp": ["1.16", "zeCommandListImmediateAppendCommandListsWithParameters"], + "zeImageViewCreateExp": ["", "zeImageViewCreateExt"], + "zesRasGetConfig": ["1.16", "zesRasGetConfigExp"], + "zesRasSetConfig": ["1.16", "zesRasSetConfigExp"] +} \ No newline at end of file diff --git a/backends/ze/ze_device_property.cpp b/backends/ze/ze_device_property.cpp new file mode 100644 index 00000000..edaa0baa --- /dev/null +++ b/backends/ze/ze_device_property.cpp @@ -0,0 +1,147 @@ +// ze_device_property.cpp +// +// Standalone Level Zero utility that queries every device on the system and +// dumps its command-queue-group topology to `ze_device_property.json`. +// +// For each command queue group it reports: +// - the group ordinal (the value passed as `ordinal` in ze_command_queue_desc_t +// or `commandQueueGroupOrdinal` in ze_command_list_desc_t) +// - the engine type derived from the group flags: +// "compute", "copy", or "compute-and-copy" +// - numQueues: the number of physical engines (queue indices) in the group, +// i.e. the valid range for ze_command_queue_desc_t::index is [0, numQueues-1] +// +// This binary is meant to be invoked by a separate program; it performs no +// argument parsing beyond an optional output path. +// +// Build: compiled with icpx (see Makefile.am). + +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +const char *result_to_string(ze_result_t r) { + switch (r) { + case ZE_RESULT_SUCCESS: + return "ZE_RESULT_SUCCESS"; + case ZE_RESULT_ERROR_UNINITIALIZED: + return "ZE_RESULT_ERROR_UNINITIALIZED"; + case ZE_RESULT_ERROR_DEVICE_LOST: + return "ZE_RESULT_ERROR_DEVICE_LOST"; + case ZE_RESULT_ERROR_INVALID_NULL_HANDLE: + return "ZE_RESULT_ERROR_INVALID_NULL_HANDLE"; + case ZE_RESULT_ERROR_INVALID_NULL_POINTER: + return "ZE_RESULT_ERROR_INVALID_NULL_POINTER"; + case ZE_RESULT_ERROR_UNSUPPORTED_FEATURE: + return "ZE_RESULT_ERROR_UNSUPPORTED_FEATURE"; + default: + return "ZE_RESULT_ERROR (unlisted)"; + } +} + +// Fatal-checks a Level Zero call; on failure prints the site and exits(1). +#define ZE_CHECK(call) \ + do { \ + ze_result_t _res = (call); \ + if (_res != ZE_RESULT_SUCCESS) { \ + std::fprintf(stderr, "%s failed: %s (0x%x)\n", #call, \ + result_to_string(_res), (unsigned)_res); \ + std::exit(1); \ + } \ + } while (0) + +// Classify a command queue group by its flags. +const char *engine_type(ze_command_queue_group_property_flags_t flags) { + const bool compute = flags & ZE_COMMAND_QUEUE_GROUP_PROPERTY_FLAG_COMPUTE; + const bool copy = flags & ZE_COMMAND_QUEUE_GROUP_PROPERTY_FLAG_COPY; + if (compute && copy) + return "compute-and-copy"; + if (compute) + return "compute"; + if (copy) + return "copy"; + return "other"; +} + +} // namespace + +int main(int argc, char **argv) { + const char *out_path = + (argc > 1) ? argv[1] : "ze_device_property.json"; + + ZE_CHECK(zeInit(ZE_INIT_FLAG_GPU_ONLY)); + + uint32_t driver_count = 0; + ZE_CHECK(zeDriverGet(&driver_count, nullptr)); + std::vector drivers(driver_count); + if (driver_count > 0) + ZE_CHECK(zeDriverGet(&driver_count, drivers.data())); + + std::ofstream out(out_path); + if (!out) { + std::fprintf(stderr, "unable to open '%s' for writing\n", out_path); + return 1; + } + + out << "{\n \"devices\": ["; + + bool first_device = true; + for (uint32_t d = 0; d < driver_count; ++d) { + uint32_t device_count = 0; + ZE_CHECK(zeDeviceGet(drivers[d], &device_count, nullptr)); + std::vector devices(device_count); + if (device_count > 0) + ZE_CHECK(zeDeviceGet(drivers[d], &device_count, devices.data())); + + for (uint32_t i = 0; i < device_count; ++i) { + ze_device_properties_t dev_props{}; + dev_props.stype = ZE_STRUCTURE_TYPE_DEVICE_PROPERTIES; + ZE_CHECK(zeDeviceGetProperties(devices[i], &dev_props)); + + uint32_t group_count = 0; + ZE_CHECK(zeDeviceGetCommandQueueGroupProperties(devices[i], &group_count, + nullptr)); + std::vector groups(group_count); + for (auto &g : groups) + g.stype = ZE_STRUCTURE_TYPE_COMMAND_QUEUE_GROUP_PROPERTIES; + if (group_count > 0) + ZE_CHECK(zeDeviceGetCommandQueueGroupProperties( + devices[i], &group_count, groups.data())); + + out << (first_device ? "\n" : ",\n"); + first_device = false; + + out << " {\n"; + out << " \"driver_index\": " << d << ",\n"; + out << " \"device_index\": " << i << ",\n"; + out << " \"name\": \"" << dev_props.name << "\",\n"; + out << " \"command_queue_groups\": ["; + + for (uint32_t g = 0; g < group_count; ++g) { + out << (g == 0 ? "\n" : ",\n"); + out << " {\n"; + out << " \"ordinal\": " << g << ",\n"; + out << " \"type\": \"" << engine_type(groups[g].flags) + << "\",\n"; + out << " \"numQueues\": " << groups[g].numQueues << "\n"; + out << " }"; + } + + out << (group_count == 0 ? "" : "\n ") << "]\n"; + out << " }"; + } + } + + out << (first_device ? "" : "\n ") << "]\n}\n"; + out.close(); + + std::fprintf(stderr, "wrote %s\n", out_path); + return 0; +} diff --git a/backends/ze/ze_thread_safety.yaml b/backends/ze/ze_thread_safety.yaml new file mode 100644 index 00000000..b87a46f8 --- /dev/null +++ b/backends/ze/ze_thread_safety.yaml @@ -0,0 +1,29 @@ +--- +zeCommandListDestroy: + - [hCommandList, command_list] +zeCommandListClose: + - [hCommandList, command_list] +zeCommandListReset: + - [hCommandList, command_list] +zeCommandListAppendWriteGlobalTimestamp: + - [hCommandList, command_list] +zeCommandListAppendLaunchKernel: + - [hCommandList, command_list] +zeCommandListAppendBarrier: + - [hCommandList, command_list] +zeCommandListAppendLaunchCooperativeKernel: + - [hCommandList, command_list] +zeCommandListAppendMemoryCopy: + - [hCommandList, command_list] +zeCommandListAppendMemoryFill: + - [hCommandList, command_list] +zeCommandListAppendMemoryCopyRegion: + - [hCommandList, command_list] +zeCommandListAppendSignalEvent: + - [hCommandList, command_list] +zeCommandListAppendWaitOnEvents: + - [hCommandList, command_list] +zeCommandListAppendEventReset: + - [hCommandList, command_list] +zeCommandQueueExecuteCommandLists: + - [phCommandLists_vals, command_list] diff --git a/backends/ze/ze_validator.in b/backends/ze/ze_validator.in new file mode 100644 index 00000000..5e91ca94 --- /dev/null +++ b/backends/ze/ze_validator.in @@ -0,0 +1,131 @@ +#!/usr/bin/env ruby +# coding: utf-8 +DATADIR = File.join("@prefix@", "share") +BINDIR = File.join("@prefix@", "bin") +$:.unshift(DATADIR) if File.directory?(DATADIR) +require 'optparse' +require 'babeltrace2' +require 'find' +require 'ze_library' +require 'pp' +require 'set' +require 'ze_validator_zemodel' +require 'ze_validator_function_entry_exit_callbacks' +require 'ze_validator_state_object' +require 'yaml' + +# Don't complain about broken pipe +Signal.trap('SIGPIPE', 'SYSTEM_DEFAULT') + +# Runs the ze_device_property helper to (re)generate ze_device_property.json. +# Best-effort: the validator continues without the device topology data. +def generate_device_properties + json_path = File.join(DATADIR, 'ze_device_property.json') + binary = File.join(BINDIR, 'ze_device_property') + binary = 'ze_device_property' unless File.executable?(binary) + + ok = system(binary, json_path, out: File::NULL, err: File::NULL) + unless ok + $stderr.puts "Warning: could not generate #{json_path} via '#{binary}'; " \ + "continuing without device properties." + end +rescue SystemCallError => e + $stderr.puts "Warning: could not run ze_device_property: #{e.message}; " \ + "continuing without device properties." +end + +$options = { live: false, device_agnostic: true, performance: true, + gen_device_properties: true } + +OptionParser.new do |opts| + opts.banner = 'Usage: ze_validator [OPTIONS] trace_directory...' + + opts.on('-h', '--help', 'Prints this help') do + puts opts + exit + end + + opts.on('--no-device-properties', + 'Skip running the ze_device_property helper to (re)generate ze_device_property.json') do + $options[:gen_device_properties] = false + end + + opts.on('--live', 'Enable live processing of the trace') do + $options[:live] = true + end + + opts.on('--disable-dagn', 'Disables device agnostic semantic misuse checking') do + $options[:device_agnostic] = false + end + + opts.on('--disable-performance', 'Disables reporting of API usages that results in downgrade of performance') do + $options[:performance] = false + end +end.parse! + + +def build_and_run_graph( source_location, sink_object ) + # build graph and set up source + graph = BT2::BTGraph.new + + ctf_fs = BT2::BTPlugin.find('ctf').get_source_component_class_by_name('fs') + ctf_lttng_live = BT2::BTPlugin.find("ctf").get_source_component_class_by_name("lttng-live") + utils_muxer = BT2::BTPlugin.find('utils').get_filter_component_class_by_name('muxer') + + if !$options[:live] + trace_locations = + Find.find(*source_location).reject do |path| + FileTest.directory?(path) + end.select do |path| + File.basename(path) == 'metadata' + end.collect do |path| + File.dirname(path) + end.select do |path| + qe = BT2::BTQueryExecutor.new(component_class: ctf_fs, object_name: 'babeltrace.support-info', + params: { 'input' => path, 'type' => 'directory' }) + qe.query.value['weight'] > 0.5 + end + else + trace_locations = source_location + end + raise 'Could not find lttng trace' if trace_locations.size == 0 + + if !$options[:live] + comp_sources = trace_locations.each_with_index.collect { |trace_location, i| graph.add_component(ctf_fs, "trace_#{i}", params: {"inputs" => [ trace_location ] }) } + else + comp_sources = trace_locations.each_with_index.collect { |trace_location, i| graph.add_component(ctf_lttng_live, "trace_#{i}", params: {"inputs" => [ trace_location ], "session-not-found-action" => "end" }) } + end + + # Muxer + comp_muxer = graph.add_component(utils_muxer, 'mux') + + sink = graph.add_simple_sink('babeltrace_thapi', sink_object.consume) + + # Sources to muxer + comp_sources.flat_map(&:output_ports).each_with_index do |op, i| + ip = comp_muxer.input_port(i) + graph.connect_ports(op, ip) + end + + # Chain the rest + [comp_muxer, sink].flatten.each_cons(2) do |_out, _in| + op = _out.output_port(0) + ip = _in.input_port(0) + graph.connect_ports(op, ip) + end + + graph.run + sink_object.check_issues() + +end + +# only executive this code if we launch this as the main +# script. if it's just included with "require" we just want access to the functions. +if __FILE__ == $0 + generate_device_properties if $options[:gen_device_properties] + sink_obj = StateObject.new(device_agnostic: $options[:device_agnostic], + performance: $options[:performance]) + ARGV.uniq! + source_location = ARGV + build_and_run_graph(source_location, sink_obj) +end \ No newline at end of file diff --git a/backends/ze/ze_validator_entry_exit_helpers.rb b/backends/ze/ze_validator_entry_exit_helpers.rb new file mode 100644 index 00000000..e63a066c --- /dev/null +++ b/backends/ze/ze_validator_entry_exit_helpers.rb @@ -0,0 +1,656 @@ +require 'ze_validator_zemodel' +require 'ze_library' + + + +# Checks for oob index. A command queue is created with an (ordinal, index) +# pair -- which engine group, and which queue within that group. +def check_valid_index_for_ordinal(state,ctx,queue_handle,ordinal,index) + if state.device_properties + command_queue_prop = state.device_properties["devices"][0]["command_queue_groups"] + command_queue_prop.each do |prop| + #find matching ordinal, and check whether the index is oob + if prop["ordinal"] == ordinal && (index >= prop["numQueues"] || index < 0) + state.print_usage_error(ctx, "command queue (#{state.get_handle_str(queue_handle)}) with ordinal = #{ordinal} was created " + + "with index = #{index}. Index value should be: 0<= index < #{prop["numQueues"]}") + end + end + end +end + +# Checking whether the application ever called zeDeviceGetCommandQueueGroupProperties +# before calling command queue/list create. Not calling it implies hardcoded ordinals +def check_group_property_queued(state, ctx, payload, device) + if !(device.cmd_queue_group_properties_queried) && state.print_tracker["check_group_property"] == 0 + state.print_tracker["check_group_property"] = 1 + state.print_usage_error(ctx,"command queue group wasn't queried. Hardcoded group properties may break the code on different devices") + end +end + + +# returns the copy ordinals if retrieved from the ze_device_property.json +def copy_only_ordinals(state) + return [1, 2] unless state.device_properties + state.device_properties["devices"][0]["command_queue_groups"] + .select { |prop| prop["type"] == "copy" } + .map { |prop| prop["ordinal"] } +end + +# checks whether a command list attached to a copy-only engine receives a kernel +def check_valid_ordinal(state, ctx, payload, cqg_ordinal) + copy_only_ords = copy_only_ordinals(state) + if copy_only_ords.include?(cqg_ordinal) && state.print_tracker["check_valid_ordinal"] == 0 + state.print_tracker["check_valid_ordinal"] = 1 + kernels = state.find_objects(ctx, 'kernel') + kernel_handle = state.find_param(ctx, 'hKernel') + kernel_name = "UNKNOWN" #kernel name wasn't passed, so mark it as unknown + command_list_handle = state.find_param(ctx, 'hCommandList') + if kernels[kernel_handle] + kernel_name = kernels[kernel_handle].name + end + state.print_usage_error(ctx, "Launching kernel (#{kernel_name}) to a command list with Copy Ordinal: #{state.get_handle_str(command_list_handle)}") + end +end + +#list of compute launches +COMPUTE_LAUNCH_APIS = ['zeCommandListAppendLaunchKernel', + 'zeCommandListAppendLaunchCooperativeKernel'].freeze + +def command_list_has_kernel_launch?(cmd_list) + cmd_list && cmd_list.ops.any? { |op| op.kind == :launch && COMPUTE_LAUNCH_APIS.include?(op.api) } +end + +#Checks whether a command list that has a compute kernel gets submitted to a command queue that is attached to a copy only engine. +def check_copy_only_queue_submission(state, ctx, queue, cmd_list) + return unless queue && queue.desc + return unless command_list_has_kernel_launch?(cmd_list) + queue_ordinal = queue.desc[:ordinal] + return unless copy_only_ordinals(state).include?(queue_ordinal) + key = "copyq-submit-#{state.get_handle_str(queue.handle)}-#{state.get_handle_str(cmd_list.handle)}" + return unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, "command list #{state.get_handle_str(cmd_list.handle)} contains a compute kernel " \ + "launch but was submitted to command queue #{state.get_handle_str(queue.handle)} " \ + "with copy-only ordinal #{queue_ordinal}") +end + +# Checks whether the kernel module's context matches that of the command list's. +def check_kernel_list_context_match(state, ctx, payload) + command_lists = state.find_objects(ctx, 'command_list') + kernels = state.find_objects(ctx, 'kernel') + cmd_list = command_lists[payload['hCommandList']] + kernel = kernels[payload['hKernel']] + return unless cmd_list && cmd_list.context && kernel + mod = kernel.module + return unless mod && mod.context + return if mod.context == cmd_list.context + key = "kernel-list-ctx-#{state.get_handle_str(cmd_list.handle)}-#{state.get_handle_str(kernel.handle)}" + return unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, + "kernel #{state.get_handle_str(kernel.handle)} (from module " \ + "#{state.get_handle_str(mod.handle)} on context #{state.get_handle_str(mod.context.handle)}) " \ + "does not share the context of command list #{state.get_handle_str(cmd_list.handle)} " \ + "(context #{state.get_handle_str(cmd_list.context.handle)})") +end + +# Checks if the kernel was created +def check_kernel_created(state, ctx, payload) + kernels = state.find_objects(ctx, 'kernel') + kernel_handle = payload['hKernel'] + unless kernels[kernel_handle] + state.print_usage_error(ctx, "kernel: #{state.get_handle_str(kernel_handle)} wasn't created. Consider calling zeKernelCreate") + end +end + +#Checks for using fence without reset +def check_fence_misuse(state, ctx, payload) + fence_handle = payload['hFence'] + fence = get_fence(state,ctx,fence_handle) + if fence && (fence.status == fence.signaled || fence.status == fence.in_use) + state.print_usage_error(ctx, "Used fence: #{state.get_handle_str(fence_handle)} twice without resetting it") + end +end + +# Check whether the queue handed to ExecuteCommandLists was never created (or was already destroyed). +def check_valid_command_queue(state,ctx,payload, cmd_queues, cmd_queue_ptr) + cmd_queue = cmd_queues[cmd_queue_ptr] + unless cmd_queue + state.print_usage_error(ctx, "Invalid commandQueue (#{state.get_handle_str(cmd_queue_ptr)}) was handed to zeCommandQueueExecuteCommandLists") + end + +end + +# Checks for submitting nothing, submitting a handle that was never created, or +# submitting an immediate list, which carries its own queue. +def check_valid_command_lists(state, ctx, payload) + command_lists = payload['phCommandLists_vals'] + known_command_lists = state.find_objects(ctx, 'command_list') + if command_lists.nil? || command_lists.empty? + state.print_usage_error(ctx, "No valid commandlist was chosen at zeCommandQueueExecuteCommandLists") + end + + command_lists.each do |command_list_handle| + if !(known_command_lists[command_list_handle]) + state.print_usage_error(ctx, "Invalid commandlist (#{command_list_handle}) was handed to zeCommandQueueExecuteCommandLists") + elsif known_command_lists[command_list_handle] && known_command_lists[command_list_handle].immediate + state.print_usage_error(ctx, "Immediate Command List was chosen for the Command Queue: #{state.get_handle_str(command_queue_handle)}") + end + end +end + + + +# Resolve a fence handle to its model object (nil if unknown). +def get_fence(state,context,fence_handle) + fences = state.find_objects(context, 'fence') + fence = fences[fence_handle] #returns fence +end + +# Resolve the Level Zero context handle that owns a command list. +def cmd_list_ctx_handle(state, ctx, cmd_list_handle) + cmd_list = state.find_objects(ctx, 'command_list')[cmd_list_handle] + cmd_list && cmd_list.context ? cmd_list.context.handle : nil +end + +# retrieves the wait event handles at the current state +def wait_event_handles(state, ctx) + handles = state.find_param(ctx, 'phWaitEvents_vals') || + state.find_param(ctx, 'phEvents_vals') || [] + handles.reject { |h| h.nil? || h == 0 } +end + +# Record one op onto a command list +def record_op(state, ctx, cmd_list_handle, op) + cmd_list = state.find_objects(ctx, 'command_list')[cmd_list_handle] + return unless cmd_list + if cmd_list.immediate + check_event_pool_immediate_list_context_match(state, ctx, cmd_list, op) + state.enqueue_immediate_op(ctx, op, cmd_list_handle) + else + cmd_list.ops << op + end +end + +# Record a memory-copy op (zeCommandListAppendMemoryCopy / MemoryFill). +def record_copy_op(state, ctx, api, dst_key, src_key) + cmd_list_handle = state.find_param(ctx, 'hCommandList') + op = ZEModel::RecordedOp.new(:copy, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + params: { api: api, + ctx_handle: cmd_list_ctx_handle(state, ctx, cmd_list_handle), + dst: (dst_key ? state.find_param(ctx, dst_key) : nil), + src: (src_key ? state.find_param(ctx, src_key) : nil), + size: state.find_param(ctx, 'size') }) + record_op(state, ctx, cmd_list_handle, op) +end + +# Records a zeCommandListAppendMemoryRangesBarrier op. +def record_ranges_barrier_op(state, ctx) + cmd_list_handle = state.find_param(ctx, 'hCommandList') + bases = state.find_param(ctx, 'pRanges_vals') || [] + sizes = state.find_param(ctx, 'pRangeSizes_vals') || [] + ranges = bases.each_with_index.map { |base, i| { base: base, size: sizes[i] } } + op = ZEModel::RecordedOp.new(:ranges_barrier, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + params: { api: 'zeCommandListAppendMemoryRangesBarrier', + ctx_handle: cmd_list_ctx_handle(state, ctx, cmd_list_handle), + ranges: ranges }) + record_op(state, ctx, cmd_list_handle, op) +end + +# Check if a command list was closed before launching anything on it (called at the execute command lists, for non-immediate command queues) +def check_command_list_closed(state, ctx, payload) + command_queue_handle = payload['hCommandQueue'] + command_lists = payload['phCommandLists_vals'] || [] + known_command_lists = state.find_objects(ctx, 'command_list') + command_lists.each do |command_list_handle| + cmd_list = known_command_lists[command_list_handle] + next unless cmd_list + if cmd_list.status == ZEModel::CommandList.class_variable_get(:@@INITIALIZED) + state.print_usage_error(ctx, "commandlist: #{state.get_handle_str(command_list_handle)} wasn't closed before executing on #{state.get_handle_str(command_queue_handle)}") + elsif cmd_list.status == ZEModel::CommandList.class_variable_get(:@@DESTROYED) + state.print_usage_error(ctx, "commandlist: #{state.get_handle_str(command_list_handle)} was already destroyed #{state.get_handle_str(command_queue_handle)}") + end + end +end + + +# check if the command list reset is valid or not. +# Invalid calls: reset on destroyed lists, reset on immeidate lists, and reset on command lists that are already exeucting. +def check_command_list_reset(state, ctx, payload) + handle = payload['hCommandList'] + cmd_list = state.find_objects(ctx, 'command_list')[handle] + + if cmd_list.status == ZEModel::CommandList.class_variable_get(:@@DESTROYED) + key = "clreset-destroyed-#{state.get_handle_str(handle)}" + if state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, "command list #{state.get_handle_str(handle)} was already destroyed before zeCommandListReset") + end + return + end + + if cmd_list.immediate + key = "clreset-immediate-#{state.get_handle_str(handle)}" + if state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, "zeCommandListReset called on immediate command list #{state.get_handle_str(handle)}; " \ + "immediate command lists cannot be reset") + end + end + + if state.command_list_in_flight?(ctx, handle) + key = "clreset-inflight-#{state.get_handle_str(handle)}" + if state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, "command list #{state.get_handle_str(handle)} is being reset while a prior " \ + "zeCommandQueueExecuteCommandLists submission is still in-flight; the device may " \ + "still be executing it (undefined behavior)") + end + end +end + +# checks whether zeKernelCreate was given a null module handle. +def check_valid_module(state,ctx,payload) + module_handle = state.find_param(ctx, 'hModule') + if !module_handle || module_handle == 0 + state.print_usage_error(ctx, "Improper hModule was handed") + end +end + + +# Checks if the fence's queue and the command list is on the same context. +def check_list_and_fence_have_matching_context(state,ctx,payload,cmd_list,fence) + if fence + unless cmd_list && fence.command_queue && + cmd_list.context == fence.command_queue.context + list_handle = cmd_list ? state.get_handle_str(cmd_list.handle) : "nullptr" + fence_handle = fence + state.print_usage_error(ctx, "Mismatching context between command list #{list_handle} and fence #{fence_handle}") + end + end +end + +# Checks for context between queue and the fence. +# Stronger than a context match, as it checks for the matching of the queue. +def check_fence_and_queue_compatibility(state,ctx,payload,cmd_queue,fence) + if fence + unless cmd_queue && cmd_queue == fence.command_queue + queue_handle = cmd_queue ? state.get_handle_str(cmd_queue.handle) : "nullptr" + fence_handle = fence + state.print_usage_error(ctx, "Associated command queue (#{state.get_handle_str(fence.command_queue.handle)}) of fence #{fence_handle} " + + "is different from the one that was provided #{queue_handle}") + end + end +end + +# Check the context between the queue and the list +def check_list_and_queue_have_matching_context(state,ctx,payload,cmd_list, cmd_queue) + unless cmd_queue && cmd_list && cmd_list.context == cmd_queue.context + queue_handle = cmd_queue ? state.get_handle_str(cmd_queue.handle) : "nullptr" + list_handle = cmd_list ? state.get_handle_str(cmd_list.handle) : "nullptr" + state.print_usage_error(ctx, "Mismatching context between command queue #{queue_handle} and command list #{list_handle}") + end +end + +# List of operations to collect the events from +EVENT_OP_KINDS = [:copy, :launch, :signal, :wait, :reset].freeze + +# retrieves the events in a given op +def event_handles_in_op(op) + return [] unless EVENT_OP_KINDS.include?(op.kind) + handles = [] + handles << op.signal if op.signal + handles.concat(op.waits) if op.waits + handles +end + +# returns the distinct event handles a command list references across all of its +# recorded ops that are subject to the same-context requirement. +def event_handles_in_list(cmd_list) + cmd_list.ops.flat_map { |op| event_handles_in_op(op) }.uniq +end + +#Check if all events share the same context +def check_events_share_context(state, ctx, event_handles, ref_context, ref_kind, ref_handle) + return unless ref_context + events = state.find_objects(ctx, 'event') + event_handles.uniq.each do |h| + ev = events[h] + next unless ev && ev.event_pool && ev.event_pool.context + next if ev.event_pool.context == ref_context + key = "evpool-#{ref_kind}-ctx-#{state.get_handle_str(ref_handle)}-#{state.get_handle_str(h)}" + next unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, + "event #{state.get_handle_str(h)} (from event pool " \ + "#{state.get_handle_str(ev.event_pool.handle)} on context " \ + "#{state.get_handle_str(ev.event_pool.context.handle)}) does not share the context of " \ + "#{ref_kind} #{state.get_handle_str(ref_handle)} " \ + "(context #{state.get_handle_str(ref_context.handle)})") + end +end + +# Check if event pool's context matches the command queue's context +def check_event_pool_list_context_match(state, ctx, cmd_list) + return unless cmd_list + check_events_share_context(state, ctx, event_handles_in_list(cmd_list), + cmd_list.context, 'command list', cmd_list.handle) +end + +# Check if event pool's context matches the immediate command list's context +def check_event_pool_immediate_list_context_match(state, ctx, cmd_list, op) + return unless cmd_list && cmd_list.context + check_events_share_context(state, ctx, event_handles_in_op(op), + cmd_list.context, 'immediate command list', cmd_list.handle) +end + + +#Find the memory allocation containing the ptr +#O(N) per lookup +def find_allocation_containing(allocations, ptr) + allocations.each_value.find { |m| m.base && m.base <= ptr && ptr < m.base + m.size } +end + +# Check whether the copy's endpoints have enough space to support the requested size +# Deduped so an append checked at entry is not reported again when it executes. +def check_copy_endpoint_oob(state, ctx, allocations, ptr, size, api, role) + return if ptr.nil? || ptr == 0 || size.nil? + mem = allocations[ptr] || find_allocation_containing(allocations, ptr) + return unless mem + offset = ptr - mem.base + available = mem.size - offset + return unless available < size + key = "oob-#{api}-#{role}-#{state.get_handle_str(ptr)}-#{size}" + return unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + state.print_usage_error(ctx, "#{api}: #{role} memory #{state.get_handle_str(ptr)} only has #{available} " \ + "bytes available from this offset but the copy needs #{size} bytes") +end + +# Performs the oob check for copy for both endpoints (src and dst) +def check_oob_copy(state, ctx, params) + api = params[:api] || 'zeCommandListAppendMemoryCopy' + size = params[:size] + allocations = state.memory_allocations(ctx, params[:ctx_handle]) + check_copy_endpoint_oob(state, ctx, allocations, params[:dst], size, api, 'destination') + check_copy_endpoint_oob(state, ctx, allocations, params[:src], size, api, 'source') +end + +# Check if the copy is from/to a nullptr +def check_null_copy_ptr(state, ctx, api, endpoints) + endpoints.each do |role, ptr| + if ptr.nil? || ptr == 0 + state.print_usage_error(ctx, "#{api}: #{role} pointer is nullptr") + end + end +end + +# Deletes the address with a new allocation +# An address might be reused after a free. In this case, we need to update the validator's state as well. +def mark_reallocated(state, ctx, ctx_handle, handle, size) + freed = state.freed_memory_allocations(ctx, ctx_handle) + return if freed.empty? + freed.delete_if { |_addr, m| ranges_overlap?(m.base, m.size, handle, size) } +end + +# Finds the freed allocation that contains the ptr +def find_freed_allocation_containing(freed, ptr) + freed.each_value.find { |m| m.base && m.base <= ptr && ptr < m.base + m.size } +end + +# Checks for use-after-free on an address +def check_uaf_endpoint(state, ctx, live, freed, ptr, api, role) + return if ptr.nil? || ptr == 0 + return if live[ptr] || find_allocation_containing(live, ptr) + mem = freed[ptr] || find_freed_allocation_containing(freed, ptr) + return unless mem + key = "uaf-#{api}-#{state.get_handle_str(ptr)}" + return unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + offset = ptr - mem.base + where = offset == 0 ? "" : " (offset #{offset} into the freed allocation)" + state.print_memory_error(ctx, "#{api}: #{role} memory #{state.get_handle_str(ptr)}#{where} was already " \ + "freed#{mem.freed_by ? " by #{mem.freed_by}" : ""}; use-after-free") +end + +# Checks for when an API uses a memory that has been freed +def check_use_after_free(state, ctx, params) + api = params[:api] || 'zeCommandListAppendMemoryCopy' + live = state.memory_allocations(ctx, params[:ctx_handle]) + freed = state.freed_memory_allocations(ctx, params[:ctx_handle]) + return if freed.empty? + check_uaf_endpoint(state, ctx, live, freed, params[:dst], api, 'destination') + check_uaf_endpoint(state, ctx, live, freed, params[:src], api, 'source') +end + +# calls the check_use_after_free only if the wait events have been satisfied +def check_use_after_free_on_append(state, ctx, params, waits) + if state.waits_satisfied?(ctx, waits) + check_use_after_free(state, ctx, params) + end +end + +# Calls check_oob_copy at append time, so an append that crashes the driver (and +# so emits no _exit) is still checked. Gated on the waits like the uaf check. +def check_oob_copy_on_append(state, ctx, params, waits) + if state.waits_satisfied?(ctx, waits) + check_oob_copy(state, ctx, params) + end +end + +# Checks for uaf on memory ranges barrier +def check_uaf_ranges_barrier(state, ctx, params) + api = params[:api] || 'zeCommandListAppendMemoryRangesBarrier' + live = state.memory_allocations(ctx, params[:ctx_handle]) + freed = state.freed_memory_allocations(ctx, params[:ctx_handle]) + return if freed.empty? + (params[:ranges] || []).each do |r| + check_uaf_endpoint(state, ctx, live, freed, r[:base], api, 'range') + end +end + +# returns true if [a, a+asize) and [b, b+bsize) overlap. +def ranges_overlap?(a, asize, b, bsize) + return false unless a && b && asize && bsize + a < b + bsize && b < a + asize +end + +# Checks for whether memory was deleted during execution of a command list +def check_free_in_flight(state, ctx, mem) + return unless mem + mem_ctx_handle = mem.context ? mem.context.handle : nil + state.each_inflight_copy_op(ctx) do |unit, op| + p = op.params + next unless p[:ctx_handle] == mem_ctx_handle + hit = [[p[:dst], 'destination'], [p[:src], 'source']].find do |ptr, _role| + ptr && ptr != 0 && ranges_overlap?(mem.base, mem.size, ptr, p[:size]) + end + next unless hit + _ptr, role = hit + state.print_memory_error(ctx, "memory #{state.get_handle_str(mem.base)} is being freed while still in use as " \ + "the #{role} of an in-flight #{p[:api] || 'copy'} on #{unit.label}; the device " \ + "may access freed memory") + end +end + + +# Finds the memory object in the validator that matches the ptr, or the object that contains the ptr +def find_memory_in_submap(submap, ptr) + submap[ptr] || find_allocation_containing(submap, ptr) +end + +# Returns [memory, ctx_handle] for ptr, preferring the passed context (usually command list's context). +def find_known_memory(state, ctx, ptr, prefer_ctx_handle) + return [nil, nil] if ptr.nil? || ptr == 0 + all_maps = state.get_process(ctx).memory_allocations + if prefer_ctx_handle && all_maps.key?(prefer_ctx_handle) + mem = find_memory_in_submap(all_maps[prefer_ctx_handle], ptr) + return [mem, prefer_ctx_handle] if mem + end + all_maps.each do |cth, submap| + next if cth == prefer_ctx_handle + mem = find_memory_in_submap(submap, ptr) + return [mem, cth] if mem + end + [nil, nil] +end + +# Checks that one copy/fill endpoint was allocated on the command list's +# context. Untracked pointers are skipped; deduped per (list, endpoint, ptr). +def check_ptr_endpoint_list_context(state, ctx, list_ctx_handle, list_handle, ptr, api, role) + return if ptr.nil? || ptr == 0 + return if list_ctx_handle.nil? # unknown command list context -> skip + mem, found_ctx = find_known_memory(state, ctx, ptr, list_ctx_handle) + return unless mem # unknown pointer -> skip (no false alarm) + return if found_ctx == list_ctx_handle # correctly in the list's context -> fine + key = "ptr-list-ctx-#{state.get_handle_str(list_handle)}-#{role}-#{state.get_handle_str(ptr)}" + return unless state.print_tracker[key] == 0 + state.print_tracker[key] = 1 + mem_ctx_str = mem.context ? state.get_handle_str(mem.context.handle) : state.get_handle_str(found_ctx) + state.print_usage_error(ctx, + "#{api}: #{role} memory #{state.get_handle_str(ptr)} was allocated on context #{mem_ctx_str} " \ + "but command list #{state.get_handle_str(list_handle)} is on context #{state.get_handle_str(list_ctx_handle)}; " \ + "the command list and copied memory must share a context") +end + +# Checks a copy/fill's endpoints against the command list's context. Runs at +# entry: a cross-context copy can be rejected inside the append. +def check_copy_ptr_list_context(state, ctx, api, list_handle, endpoints) + list_ctx_handle = cmd_list_ctx_handle(state, ctx, list_handle) + endpoints.each do |role, ptr| + check_ptr_endpoint_list_context(state, ctx, list_ctx_handle, list_handle, ptr, api, role) + end +end + +# Checks for an event signaled while already signaled with no reset between: +# reuse-no-reset if the host observed the prior signal, double-signal if not. +def check_event_signal_reuse(state, ctx, handle, who) + ev = state.event_by_handle(ctx, handle) + return unless ev && ev.signaled + if ev.observed + state.print_usage_error(ctx, "event #{state.get_handle_str(handle)} was reused as a signal target by #{who} " \ + "without calling zeEventHostReset/zeCommandListAppendEventReset after it was " \ + "signaled#{ev.signaled_by ? " by #{ev.signaled_by}" : ""}") + else + state.print_usage_error(ctx, "event #{state.get_handle_str(handle)} was signaled by #{who} before being reset " \ + "or consumed#{ev.signaled_by ? " (already signaled by #{ev.signaled_by})" : ""}; " \ + "concurrent signals of the same event are undefined") + end +end + +# Reports wait-events never signaled by end of trace, i.e. a deferred op that +# could never complete. +def report_unsignaled_waits(state, ctx, waits) + (waits || []).each do |h| + ev = state.event_by_handle(ctx, h) + next unless ev && !ev.signaled + state.print_usage_error(ctx, "event #{state.get_handle_str(h)} was never signaled; a deferred command list " \ + "operation could not complete (possible deadlock or missing signal)") + end +end + + +# Checks for a circular event dependency across the units still stuck at end of +# trace, reporting the first cycle found since cycles overlap and share units. +def check_circular_deadlock(state, units) + stuck = units.select { |u| u.blocked_on && !u.blocked_on.empty? } + return if stuck.empty? + + # event handle -> units that may still signal it + signalers = Hash.new { |h, k| h[k] = [] } + stuck.each { |u| u.pending_signals.each { |ev| signalers[ev] << u } } + + # adjacency: U -> V if U waits on an event V still owes + succ = Hash.new { |h, k| h[k] = [] } + stuck.each do |u| + u.blocked_on.each do |ev| + signalers[ev].each { |v| succ[u] << v unless v.equal?(u) } + end + end + + # DFS; stop at the first cycle and report only that one. on_path holds the + # current branch (reaching one again closes a cycle), visited the rest. + path = [] + on_path = {} + visited = {} + found = nil + dfs = lambda do |u| + return true if found + on_path[u] = true + path.push(u) + succ[u].uniq.each do |v| + if on_path[v] + found = path[path.index(v)..] # the cycle, from v back to the current node + break + elsif !visited[v] + break if dfs.call(v) + end + end + path.pop + on_path[u] = false + visited[u] = true + !found.nil? + end + #the graph may be disconnected, so start a search from each stuck unit until + #one of them turns up a cycle + stuck.each { |u| break if dfs.call(u); } + report_deadlock_cycle(state, found) if found +end + +# Labels one node of a deadlock cycle as "::". +def deadlock_node_label(state, unit) + op = unit.current_op + #fall back to the op kind so the label is never blank + api = op ? (op.api || op.kind.to_s) : 'unknown' + waits = unit.blocked_on.map { |h| state.get_handle_str(h) }.join(', ') + "#{unit.label}::#{api} (waiting on event #{waits})" +end + +def report_deadlock_cycle(state, cycle) + ctx = cycle.first.context + desc = cycle.map { |u| deadlock_node_label(state, u) }.join(" -> ") + # close the loop for readability + desc << " -> #{deadlock_node_label(state, cycle.first)}" + state.print_deadlock_error(ctx, "circular event dependency among command list operations; none can start: #{desc}") +end + +# Checks for an in-order list parked on an event only a later op in the same +# list signals. The cross-list detector misses this since it drops self-edges. +def check_in_order_self_deadlock(state, units) + units.each do |unit| + next unless unit.in_order + next if unit.blocked_on.nil? || unit.blocked_on.empty? + self_waits = unit.blocked_on & unit.pending_signals + self_waits.each do |ev| + #the later op in this same list that would signal ev (but never runs) + later = unit.ops[(unit.cursor + 1)..]&.find { |o| o.signal == ev } + report_in_order_self_deadlock(state, unit, ev, later) + end + end +end + +# Reports one intra-list self-deadlock as -> . +def report_in_order_self_deadlock(state, unit, ev, signaling_op) + waiting = unit.current_op + waiting_api = waiting ? (waiting.api || waiting.kind.to_s) : 'unknown' + signaling_api = signaling_op ? (signaling_op.api || signaling_op.kind.to_s) : 'unknown' + ev_str = state.get_handle_str(ev) + desc = "#{unit.label}::#{waiting_api} (waits on event #{ev_str}) -> " \ + "#{unit.label}::#{signaling_api} (signals event #{ev_str} later in the same in-order list)" + state.print_deadlock_error(unit.context, + "in-order command list cannot complete; an earlier command waits on an event a later " \ + "command in the same list signals: #{desc}") +end + +# Checks a descriptor's stype. Current drivers ignore a wrong one, but it is a +# latent bug a future driver may reject. Reported once per expected stype. +def check_struct_stype_misuse(state,ctx,payload,expected_stype, observed_stype) + if expected_stype != observed_stype && state.print_tracker[expected_stype] == 0 + state.print_tracker[expected_stype] = 1 + state.print_usage_error(ctx,"\nExpected stype of #{expected_stype}\nbut #{observed_stype} was observed.") + end +end diff --git a/backends/ze/ze_validator_function_entry_exit_callbacks.rb b/backends/ze/ze_validator_function_entry_exit_callbacks.rb new file mode 100644 index 00000000..6034698a --- /dev/null +++ b/backends/ze/ze_validator_function_entry_exit_callbacks.rb @@ -0,0 +1,733 @@ +require 'ze_validator_entry_exit_helpers' +require 'ze_validator_zemodel' +require 'ze_library' + + + +$upon_entry = {} #called to modify program state on entry +$on_successful_exit = {} #called upon seeing exit functions with a successful return code +$on_erroneous_exit = {} #called upon seeing exit functions with a non-successful return code + +#these two record only that the app asked, for the portability checks +$on_successful_exit["zeDeviceGetProperties"] = lambda{|state, ctx, payload| + device_ptr = state.find_param(ctx,'hDevice') + devices = state.find_objects(ctx, 'device') + devices[device_ptr].property_fetched = true +} +# Mark that the queue group property was queried. +$on_successful_exit["zeDeviceGetCommandQueueGroupProperties"] = lambda{|state, ctx, payload| + device_ptr = state.find_param(ctx,'hDevice') + devices = state.find_objects(ctx, 'device') + devices[device_ptr].cmd_queue_group_properties_queried = true +} + +# For every append API below: validation at entry (the call may crash), +# recording at exit (only a successful append will ever execute). +$upon_entry["zeCommandListAppendLaunchKernel"] = lambda { |state, ctx, payload| + #Retrieve the compute ordinal from the command list + command_lists = state.find_objects(ctx, 'command_list') + cmd_list = command_lists[payload['hCommandList']] + cqg_ordinal = 0 + #a normal list carries the ordinal in desc; an immediate list in altdesc + if cmd_list && cmd_list.desc + cqg_ordinal = cmd_list.desc[:commandQueueGroupOrdinal] + elsif cmd_list && cmd_list.altdesc + cqg_ordinal = cmd_list.altdesc[:ordinal] + end + #both checks must run even if the launch later aborts + check_valid_ordinal(state, ctx, payload, cqg_ordinal) + check_kernel_created(state, ctx, payload) + #the kernel's module must be on the same context as the command list + check_kernel_list_context_match(state, ctx, payload) +} + +$on_successful_exit["zeCommandListAppendLaunchKernel"] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:launch, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + api: 'zeCommandListAppendLaunchKernel')) +} + +$upon_entry["zeCommandListReset"] = lambda { |state, ctx, payload| + check_command_list_reset(state, ctx, payload) +} + +# on success the list is empty and open again, so clear the recorded ops. +# In-flight executions are unaffected: they snapshotted the ops at submit time. +$on_successful_exit["zeCommandListReset"] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + cmd_list = command_lists[state.find_param(ctx, 'hCommandList')] + return unless cmd_list + cmd_list.ops.clear + cmd_list.status = ZEModel::CommandList.class_variable_get(:@@INITIALIZED) +} + +#check_command_list_closed later verifies this happened before any submission +$on_successful_exit["zeCommandListClose"] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + command_list_handle = state.find_param(ctx,"hCommandList") + cmd_list = command_lists[command_list_handle] + cmd_list.status = ZEModel::CommandList.class_variable_get(:@@CLOSED) +} + +$upon_entry["zeCommandListAppendLaunchCooperativeKernel"] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + cmd_list = command_lists[payload['hCommandList']] + check_group_property_queued(state,ctx,payload,cmd_list.device) if cmd_list + #the kernel's module must be on the same context as the command list + check_kernel_list_context_match(state, ctx, payload) +} + +$on_successful_exit["zeCommandListAppendLaunchCooperativeKernel"] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:launch, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + api: 'zeCommandListAppendLaunchCooperativeKernel')) +} + +# Copy/event ops are recorded in list order for deferred replay. The +# out-of-bounds check waits until the op's wait-events are satisfied. +$upon_entry['zeCommandListAppendMemoryCopy'] = lambda { |state, ctx, payload| + params = { api: 'zeCommandListAppendMemoryCopy', + ctx_handle: cmd_list_ctx_handle(state, ctx, payload['hCommandList']), + dst: payload['dstptr'], src: payload['srcptr'], size: payload['size'] } + waits = wait_event_handles(state, ctx) + check_null_copy_ptr(state, ctx, 'zeCommandListAppendMemoryCopy', + { 'destination' => payload['dstptr'], 'source' => payload['srcptr'] }) + check_use_after_free_on_append(state, ctx, params, waits) + #an out-of-bounds copy can crash the driver, which emits no _exit + check_oob_copy_on_append(state, ctx, params, waits) + # known memory endpoints must be allocated on the command list's context + check_copy_ptr_list_context(state, ctx, 'zeCommandListAppendMemoryCopy', payload['hCommandList'], + { 'destination' => payload['dstptr'], 'source' => payload['srcptr'] }) +} + +$upon_entry['zeCommandListAppendMemoryFill'] = lambda { |state, ctx, payload| + params = { api: 'zeCommandListAppendMemoryFill', + ctx_handle: cmd_list_ctx_handle(state, ctx, payload['hCommandList']), + dst: payload['ptr'], src: nil, size: payload['size'] } + waits = wait_event_handles(state, ctx) + check_null_copy_ptr(state, ctx, 'zeCommandListAppendMemoryFill', + { 'destination' => payload['ptr'] }) + check_use_after_free_on_append(state, ctx, params, waits) + #an out-of-bounds fill can crash the driver, which emits no _exit + check_oob_copy_on_append(state, ctx, params, waits) + # known memory endpoint must be allocated on the command list's context + check_copy_ptr_list_context(state, ctx, 'zeCommandListAppendMemoryFill', payload['hCommandList'], + { 'destination' => payload['ptr'] }) +} + +$on_successful_exit['zeCommandListAppendMemoryCopy'] = lambda { |state, ctx, payload| + record_copy_op(state, ctx, 'zeCommandListAppendMemoryCopy', 'dstptr', 'srcptr') +} + +$on_successful_exit['zeCommandListAppendMemoryFill'] = lambda { |state, ctx, payload| + #a fill only touches the destination; model it as a copy with no source + record_copy_op(state, ctx, 'zeCommandListAppendMemoryFill', 'ptr', nil) +} + +# A failed append is never recorded, so the deferred check would never see it, +# but the copy is out-of-bounds regardless of the error code. Check it here. +$on_erroneous_exit['zeCommandListAppendMemoryCopy'] = lambda { |state, ctx, payload| + params = { api: 'zeCommandListAppendMemoryCopy', + ctx_handle: cmd_list_ctx_handle(state, ctx, state.find_param(ctx, 'hCommandList')), + dst: state.find_param(ctx, 'dstptr'), + src: state.find_param(ctx, 'srcptr'), + size: state.find_param(ctx, 'size') } + check_oob_copy(state, ctx, params) + check_use_after_free(state, ctx, params) +} + +$on_erroneous_exit['zeCommandListAppendMemoryFill'] = lambda { |state, ctx, payload| + params = { api: 'zeCommandListAppendMemoryFill', + ctx_handle: cmd_list_ctx_handle(state, ctx, state.find_param(ctx, 'hCommandList')), + dst: state.find_param(ctx, 'ptr'), + src: nil, + size: state.find_param(ctx, 'size') } + check_oob_copy(state, ctx, params) + check_use_after_free(state, ctx, params) +} + +# region copies carry 2D/3D extents, so `size` is not a flat byte count; we only +# record ordering + event effects and skip the flat OOB comparison +$on_successful_exit['zeCommandListAppendMemoryCopyRegion'] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:launch, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + api: 'zeCommandListAppendMemoryCopyRegion')) +} + +#A device-side signal: the event is signaled when this op executes (after waits). +$on_successful_exit['zeCommandListAppendSignalEvent'] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:signal, signal: state.find_param(ctx, 'hEvent'), + api: 'zeCommandListAppendSignalEvent')) +} + +#A device-side wait: this op blocks the list until phEvents are signaled. +$on_successful_exit['zeCommandListAppendWaitOnEvents'] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:wait, waits: wait_event_handles(state, ctx), + api: 'zeCommandListAppendWaitOnEvents')) +} + +#A device-side reset: returns the event to unsignaled when this op executes. +$on_successful_exit['zeCommandListAppendEventReset'] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:reset, params: { reset_handle: state.find_param(ctx, 'hEvent') })) +} + +#A barrier waits on its events and signals its completion event. +$on_successful_exit['zeCommandListAppendBarrier'] = lambda { |state, ctx, payload| + record_op(state, ctx, state.find_param(ctx, 'hCommandList'), + ZEModel::RecordedOp.new(:barrier, + signal: state.find_param(ctx, 'hSignalEvent'), + waits: wait_event_handles(state, ctx), + api: 'zeCommandListAppendBarrier')) +} + +# Same event semantics as a plain barrier, plus the memory ranges it names, +# which are validated when the barrier executes (check_uaf_ranges_barrier). +$on_successful_exit['zeCommandListAppendMemoryRangesBarrier'] = lambda { |state, ctx, payload| + record_ranges_barrier_op(state, ctx) +} + +#host-side event operations, effective immediately in trace order +$on_successful_exit['zeEventHostSignal'] = lambda { |state, ctx, payload| + handle = state.find_param(ctx, 'hEvent') + check_event_signal_reuse(state, ctx, handle, 'zeEventHostSignal') + state.signal_event(ctx, handle, 'zeEventHostSignal') +} + +$on_successful_exit['zeEventHostReset'] = lambda { |state, ctx, payload| + state.reset_event(ctx, state.find_param(ctx, 'hEvent')) +} + +#does not signal the event, only records that the signaled state was consumed +$on_successful_exit['zeEventHostSynchronize'] = lambda { |state, ctx, payload| + state.observe_event(ctx, state.find_param(ctx, 'hEvent')) +} + +#A successful status query also observes the signaled state. +$on_successful_exit['zeEventQueryStatus'] = lambda { |state, ctx, payload| + state.observe_event(ctx, state.find_param(ctx, 'hEvent')) +} + +#the host waited for all submitted work, so every signaled event was consumed +$on_successful_exit['zeCommandQueueSynchronize'] = lambda { |state, ctx, payload| + state.observe_all_signaled_events(ctx) +} + +$on_successful_exit['zeCommandListHostSynchronize'] = lambda { |state, ctx, payload| + state.observe_all_signaled_events(ctx) +} + +# Submission is where the queue, the lists, their events and the fence are +# So the same context checkings between those objects are called here. +$upon_entry["zeCommandQueueExecuteCommandLists"] = lambda { |state, ctx, payload| + command_queues = state.find_objects(ctx, 'command_queue') + command_queue_handle = payload['hCommandQueue'] + command_queue = command_queues[command_queue_handle] + + #check if any command list is null + check_valid_command_lists(state,ctx,payload) + check_valid_command_queue(state,ctx,payload,command_queues,command_queue_handle) + #Check if command list was closed before executing it on the queue + #ignore if it is the first execute call + check_command_list_closed(state, ctx, payload) + check_fence_misuse(state,ctx,payload) + + known_command_lists = state.find_objects(ctx, 'command_list') + command_list_handles = payload['phCommandLists_vals'] || [] + + fences = state.find_objects(ctx, 'fence') + fence_handle = payload['hFence'] + fence = fences[fence_handle] + + if fence + fence.status = fence.in_use #set this at the entry so that other command lists can view it + end + + if command_queue + check_group_property_queued(state,ctx,payload,command_queue.device) + check_fence_and_queue_compatibility(state,ctx,payload,command_queue,fence) + command_list_handles.each do |command_list_handle| + check_list_and_queue_have_matching_context(state,ctx,payload,known_command_lists[command_list_handle],command_queue) + check_list_and_fence_have_matching_context(state,ctx,payload,known_command_lists[command_list_handle],fence) + #a list with a compute kernel launch must not go to a copy-only queue + check_copy_only_queue_submission(state,ctx,command_queue,known_command_lists[command_list_handle]) + #events used by the list must come from a pool on the queue's context + check_event_pool_list_context_match(state,ctx,known_command_lists[command_list_handle]) + end + else + #report and continue so the deferred execution below still runs + state.print_usage_error(ctx, "command queue #{state.get_handle_str(command_queue_handle)} was not found ") + end +} + +# Execute is asynchronous, so each submitted list becomes a deferred unit and +# its copies are checked when their wait-events are signaled, not here. +$on_successful_exit["zeCommandQueueExecuteCommandLists"] = lambda { |state, ctx, payload| + known_command_lists = state.find_objects(ctx, 'command_list') + command_list_handles = state.find_param(ctx, 'phCommandLists_vals') || [] + command_lists = command_list_handles.map { |h| known_command_lists[h] } + state.enqueue_deferred_execution(ctx, command_lists) +} + +#When a fence signals the host, set the fence's status to signaled +$on_successful_exit["zeFenceHostSynchronize"] = lambda { |state, ctx, payload| + fence_handle = state.find_param(ctx,"hFence") + fence = get_fence(state, ctx, fence_handle) + if fence + fence.status = fence.signaled + else + state.print_usage_error(ctx, "nullptr fence was used for zeFenceHostSynchronize") + end +} + +#should a double reset be considered as a usage error? +#Also, a fence can be shared throughout the threads and is modeled correctly (if you are wondering about whether the model treats fence associated with different thread-id differently). +$upon_entry["zeFenceReset"] = lambda { |state, ctx, payload| + curr_fence = get_fence(state, ctx, payload['hFence']) + return unless curr_fence + curr_fence.status = curr_fence.not_signaled +} + +#Set the driver for the current context +$on_successful_exit['zeDriverGet'] = lambda { |state, ctx, payload| + drivers = state.get_process(ctx).drivers + payload['phDrivers_vals'].each { |h| + drivers[h] = ZEModel::Driver.new(h) unless drivers[h] + } +} + + +$on_successful_exit['zeDeviceGet'] = lambda { |state, ctx, payload| + devices = state.find_objects(ctx, 'device') + driver = state.find_object(ctx, 'driver', 'hDriver') + if driver + payload['phDevices_vals'].each { |h| + unless devices[h] + devices[h] = ZEModel::Device.new(h) + driver.devices.push devices[h] + end + } + end +} + + + +$on_successful_exit['zeDeviceGetSubDevices'] = lambda { |state, ctx, payload| + devices = state.find_objects(ctx, 'device') + device = state.find_object(ctx, 'device', 'hDevice') + payload['phSubdevices_vals'].each { |h| + unless devices[h] + devices[h] = ZEModel::SubDevice.new(h, device) + device.sub_devices.push devices[h] + end + } +} + + +$on_successful_exit['zeContextCreate'] = lambda { |state, ctx, payload| + contexts = state.find_objects(ctx, 'context') + driver = state.find_object(ctx, 'driver', 'hDriver') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEContextDesc) + handle = payload['phContext_val'] + contexts[handle] = ZEModel::Context.new(handle, driver, desc) + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_CONTEXT_DESC,desc[:stype]) +} + +#Experimental API, it practically serves the same purpose as zeContextCreate +$on_successful_exit['zeContextCreateEx'] = lambda { |state, ctx, payload| + contexts = state.find_objects(ctx, 'context') + devices = state.find_objects(ctx, 'device') + driver = state.find_object(ctx, 'driver', 'hDriver') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEContextDesc) + devs = state.find_param(ctx, 'phDevices_vals').collect { |h| devices[h] } + devs = nil unless state.find_param(ctx, 'phDevices') != 0 + handle = payload['phContext_val'] + contexts[handle] = ZEModel::Context.new(handle, driver, desc, devs) +} + + +$on_successful_exit['zeContextDestroy'] = lambda { |state, ctx, payload| + contexts = state.find_objects(ctx, 'context') + contexts.delete(state.find_param(ctx, 'hContext')) { |h| + raise_internal_error(ctx, "context #{state.get_handle_str(h)} does not exist") + } +} + + +$on_successful_exit['zeEventPoolCreate'] = lambda { |state, ctx, payload| + context = state.find_object(ctx, 'context', 'hContext') + devices = state.find_objects(ctx, 'device') + event_pools = state.find_objects(ctx, 'event_pool') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEEventPoolDesc) + devs = state.find_param(ctx, 'phDevices_vals').collect { |h| devices[h] } + devs = nil unless state.find_param(ctx, 'phDevices') != 0 + handle = payload['phEventPool_val'] + event_pools[handle] = ZEModel::EventPool.new(handle, context, desc, devs) + context.event_pools[handle] = event_pools[handle] + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_EVENT_POOL_DESC,desc[:stype]) +} + + +# Destroying a pool while events are being used should not occur +$on_successful_exit['zeEventPoolDestroy'] = lambda { |state, ctx, payload| + event_pools = state.find_objects(ctx, 'event_pool') + handle = state.find_param(ctx, 'hEventPool') + event_pool = event_pools.delete(handle) { + state.object_not_found(ctx, 'event_pool', handle) + } + event_pool.context.event_pools.delete(handle) { + state.object_not_found(ctx, 'event_pool', handle, 'context') + } + event_pool.events.each { |h, _| + state.print_usage_error(ctx, "event #{state.get_handle_str(h)} was not destroyed prior to event_pool #{state.get_handle_str(handle)} destruction") + } +} + + + +$on_successful_exit['zeEventCreate'] = lambda { |state, ctx, payload| + events = state.find_objects(ctx, 'event') + event_pool = state.find_object(ctx, 'event_pool', 'hEventPool') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEEventDesc) + handle = payload['phEvent_val'] + events[handle] = ZEModel::Event.new(handle, event_pool, desc) + # "delete?" returns nil when desc[:index] was not free. + if !event_pool.indices.delete?(desc[:index]) + state.print_usage_error(ctx, "event_pool #{state.get_handle_str(event_pool.handle)} index #{desc[:index]} is already used") + end + event_pool.events[handle] = events[handle] + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_EVENT_DESC,desc[:stype]) +} + + +$on_successful_exit['zeEventDestroy'] = lambda { |state, ctx, payload| + events = state.find_objects(ctx, 'event') + handle = state.find_param(ctx, 'hEvent') + event = events.delete(handle) { + state.object_not_found(ctx, 'event', handle) + } + event_pool = event.event_pool + event_pool.events.delete(handle) { + state.object_not_found(ctx, 'event', handle, 'event_pool') + } + if !event_pool.indices.add?(event.desc[:index]) + state.print_usage_error(ctx, "event_pool #{state.get_handle_str(event_pool.handle)} index #{event.desc[:index]} is already freed") + end +} + + +$on_successful_exit['zeCommandQueueCreate'] = lambda { |state, ctx, payload| + command_queues = state.find_objects(ctx, 'command_queue') + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device', 'hDevice') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZECommandQueueDesc) + handle = payload['phCommandQueue_val'] + command_queues[handle] = ZEModel::CommandQueue.new(handle, context, device, desc) + context.command_queues[handle] = command_queues[handle] + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC,desc[:stype]) +} + +# A failed creation is likely an (ordinal, index) the device does not have, so +# check the index against the real topology to explain the failure. +$on_erroneous_exit['zeCommandQueueCreate'] = lambda { |state, ctx, payload| + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZECommandQueueDesc) + handle = state.find_param(ctx, 'phCommandQueue') + check_valid_index_for_ordinal(state,ctx,handle,desc[:ordinal],desc[:index]) +} + + +$on_successful_exit['zeCommandQueueDestroy'] = lambda { |state, ctx, payload| + command_queues = state.find_objects(ctx, 'command_queue') + handle = state.find_param(ctx, 'hCommandQueue') + command_queue = command_queues.delete(handle) { + state.object_not_found(ctx, 'command_queue', handle) + } + command_queue.context.command_queues.delete(handle) { + state.object_not_found(ctx, 'command_queue', handle, 'context') + } + command_queue.fences.each { |h, _| + state.print_usage_error(ctx, "fence #{state.get_handle_str(h)} was not destroyed prior to command_queue #{state.get_handle_str(handle)} destruction") + } +} + + +$on_successful_exit['zeFenceCreate'] = lambda { |state, ctx, payload| + fences = state.find_objects(ctx, 'fence') + command_queue = state.find_object(ctx, 'command_queue', 'hCommandQueue') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEFenceDesc) + handle = payload['phFence_val'] + fence = ZEModel::Fence.new(handle, command_queue, desc) + fences[handle] = fence + command_queue.fences[handle] = fence + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_FENCE_DESC,desc[:stype]) +} + + +$on_successful_exit['zeFenceDestroy'] = lambda { |state, ctx, payload| + fences = state.find_objects(ctx, 'fence') + handle = state.find_param(ctx, 'hFence') + fence = fences.delete(handle) { + state.object_not_found(ctx, 'fence', handle) + } + command_queue = fence.command_queue + command_queue.fences.delete(handle) { + state.object_not_found(ctx, 'fence', handle, 'command_queue') + } +} + + +$on_successful_exit['zeCommandListCreate'] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device', 'hDevice') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZECommandListDesc) + handle = payload['phCommandList_val'] + command_lists[handle] = ZEModel::CommandList.new(handle, context, device, desc, nil) + #in-order enables the intra-list self-deadlock check + command_lists[handle].in_order = !!(desc && desc[:flags].respond_to?(:include?) && + desc[:flags].include?(:ZE_COMMAND_LIST_FLAG_IN_ORDER)) + context.command_lists[handle] = command_lists[handle] + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_COMMAND_LIST_DESC,desc[:stype]) +} + + +$on_successful_exit['zeCommandListCreateImmediate'] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device', 'hDevice') + altdesc_val = state.find_param(ctx, 'altdesc_val') + altdesc = state.to_struct(altdesc_val, ZE::ZECommandQueueDesc) + handle = payload['phCommandList_val'] + check_group_property_queued(state,ctx,payload,device) + command_lists[handle] = ZEModel::CommandList.new(handle, context, device, nil, altdesc) + command_lists[handle].immediate = true #immdediate command lists cannot be passed to the execute command lists + command_lists[handle].associated_ordinal = altdesc[:ordinal] + #each immediate append is its own single-op unit, so a cycle between them is + #caught by check_circular_deadlock; recorded here for consistency + command_lists[handle].in_order = !!(altdesc && altdesc[:flags].respond_to?(:include?) && + altdesc[:flags].include?(:ZE_COMMAND_QUEUE_FLAG_IN_ORDER)) + context.command_lists[handle] = command_lists[handle] + + #immediate command list does not take in the list descriptor as an input + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_COMMAND_QUEUE_DESC,altdesc[:stype]) +} + + +$on_successful_exit['zeCommandListDestroy'] = lambda { |state, ctx, payload| + command_lists = state.find_objects(ctx, 'command_list') + handle = state.find_param(ctx, 'hCommandList') + command_list = command_lists.delete(handle) { + state.object_not_found(ctx, 'command_list', handle) + } + command_list.context.command_lists.delete(handle) { + state.object_not_found(ctx, 'command_list', handle, 'context') + } +} + + +$on_successful_exit['zeModuleCreate'] = lambda { |state, ctx, payload| + modules = state.find_objects(ctx, 'module') + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device', 'hDevice') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEModuleDesc) + handle = payload['phModule_val'] + mod = ZEModel::Module.new(handle, context, device, desc) + modules[handle] = mod + context.modules[handle] = mod + build_log_handle = payload['phBuildLog_val'] + if build_log_handle != 0 + module_build_logs = state.find_objects(ctx, 'module_build_log') + build_log = ZEModel::Module::BuildLog.new(build_log_handle, mod) + module_build_logs[build_log_handle] = build_log + context.module_build_logs[build_log_handle] = build_log + modules[handle].build_log = build_log + end + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_MODULE_DESC,desc[:stype]) + + +} + +# Runs diagnoistics on why the module crete failed +$on_erroneous_exit['zeModuleCreate'] = lambda { |state, ctx, payload| + build_log_handle = payload['phBuildLog_val'] + if build_log_handle != 0 + module_build_logs = state.find_objects(ctx, 'module_build_log') + build_log = ZEModel::Module::BuildLog.new(build_log_handle) + module_build_logs[build_log_handle] = build_log + context.module_build_logs[build_log_handle] = build_log + end +} + +# Kernel must be destroyed first before module +$on_successful_exit['zeModuleDestroy'] = lambda { |state, ctx, payload| + modules = state.find_objects(ctx, 'module') + handle = state.find_param(ctx, 'hModule') + mod = modules.delete(handle) { + state.object_not_found(ctx, 'module', handle) + } + mod.context.modules.delete(handle) { + state.object_not_found(ctx, 'module', handle, 'context') + } + mod.kernels.each { |h, _| + state.print_usage_error(ctx, "kernel #{state.get_handle_str(h)} was not destroyed prior to module #{state.get_handle_str(handle)} destruction") + } +} + + +$on_erroneous_exit['zeModuleDynamicLink'] = $on_successful_exit['zeModuleDynamicLink'] = lambda { |state, ctx, payload| + build_log_handle = payload['phLinkLog_val'] + if build_log_handle != 0 + module_build_logs = state.find_objects(ctx, 'module_build_log') + build_log = ZEModel::Module::BuildLog.new(build_log_handle) + module_build_logs[build_log_handle] = build_log + context.module_build_logs[build_log_handle] = build_log + end +} + + +$on_successful_exit['zeModuleBuildLogDestroy'] = lambda { |state, ctx, payload| + module_build_logs = state.find_objects(ctx, 'module_build_log') + handle = state.find_param(ctx, 'hModuleBuildLog') + module_build_log = module_build_logs.delete(handle) { + state.object_not_found(ctx, 'module_build_log', handle) + } + if module_build_log.module + module_build_log.module.context.module_build_logs.delete(handle) { + state.object_not_found(ctx, 'module_build_log', handle, 'context') + } + module_build_log.module.build_log = nil + end +} + +# upon entering, check if a valid module was passed +$upon_entry['zeKernelCreate'] = lambda {|state, ctx, payload| + check_valid_module(state,ctx, payload) +} + + +$on_successful_exit['zeKernelCreate'] = lambda { |state, ctx, payload| + kernels = state.find_objects(ctx, 'kernel') + mod = state.find_object(ctx, 'module', 'hModule') + desc_val = state.find_param(ctx, 'desc_val') + desc = state.to_struct(desc_val, ZE::ZEKernelDesc) + handle = payload['phKernel_val'] + kernelName = state.find_param(ctx, 'desc__pKernelName_val') + kernel = ZEModel::Kernel.new(handle, mod, desc, kernelName) + kernels[handle] = kernel + mod.kernels[handle] = kernel + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_KERNEL_DESC, desc[:stype]) +} + + +$on_successful_exit['zeKernelDestroy'] = lambda { |state, ctx, payload| + kernels = state.find_objects(ctx, 'kernel') + handle = state.find_param(ctx, 'hKernel') + kernel = kernels.delete(handle) { + state.object_not_found(ctx, 'kernel', handle) + } + mod = kernel.module + mod.kernels.delete(handle) { + state.object_not_found(ctx, 'kernel', handle, 'module') + } +} + +# Each allocator keys the allocation by its Level Zero context, then calls +# mark_reallocated since the driver may hand back an address that was freed. + + +$on_successful_exit['zeMemAllocDevice'] = lambda { |state, ctx, payload| + # memory is associated with devices + ctx_handle = state.find_param(ctx, 'hContext') + memory_allocations = state.memory_allocations(ctx, ctx_handle) + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device','hDevice') + size = state.find_param(ctx,"size") + device_desc_val = state.find_param(ctx,"device_desc_val") + handle = payload['pptr_val'] + mark_reallocated(state, ctx, ctx_handle, handle, size) + memory_allocation = ZEModel::Memory.new(handle, context, size, device, "device") + memory_allocations[handle] = memory_allocation + device.memory_allocations[ctx_handle][handle] = memory_allocation + device_desc = state.to_struct(device_desc_val, ZE::ZEDeviceMemAllocDesc) + check_struct_stype_misuse(state,ctx,payload,:ZE_STRUCTURE_TYPE_DEVICE_MEM_ALLOC_DESC, device_desc[:stype]) +} + + +$on_successful_exit['zeMemAllocShared'] = lambda { |state, ctx, payload| + ctx_handle = state.find_param(ctx, 'hContext') + memory_allocations = state.memory_allocations(ctx, ctx_handle) + # finds the device and context objects associated with the params + context = state.find_object(ctx, 'context', 'hContext') + device = state.find_object(ctx, 'device','hDevice') + # A nullptr device handle shares ownership between the host and all devices + # supporting cross-device shared access. + size = state.find_param(ctx,"size") + handle = payload['pptr_val'] + mark_reallocated(state, ctx, ctx_handle, handle, size) + memory_allocation = ZEModel::Memory.new(handle, context, size, device) + memory_allocations[handle] = memory_allocation + device.memory_allocations[ctx_handle][handle] = memory_allocation if device +} + + +$on_successful_exit['zeMemAllocHost'] = lambda { |state, ctx, payload| + # Host allocations are accessible by the host and all devices within the driver’s context. + ctx_handle = state.find_param(ctx, 'hContext') + memory_allocations = state.memory_allocations(ctx, ctx_handle) + context = state.find_object(ctx, 'context', 'hContext') + size = state.find_param(ctx,"size") + handle = payload['pptr_val'] + mark_reallocated(state, ctx, ctx_handle, handle, size) + memory_allocation = ZEModel::Memory.new(handle, context, size, nil, "host") + memory_allocations[handle] = memory_allocation +} + +# The free is applied at entry: zeMemFree may block until the buffer is idle, so +# by _exit a gated copy could have drained and the in-flight check would miss it. +$upon_entry['zeMemFree'] = lambda { |state, ctx, payload| + ctx_handle = payload['hContext'] + memory_allocations = state.memory_allocations(ctx, ctx_handle) + handle = payload['ptr'] + memory_allocation = memory_allocations[handle] + next unless memory_allocation + # flag if this buffer is still referenced by a copy/fill that has been + # submitted but not yet completed (in-flight device work would touch freed mem) + check_free_in_flight(state, ctx, memory_allocation) + memory_allocations.delete(handle) + owned = memory_allocation.owned_by + owned.memory_allocations[ctx_handle].delete(handle) if owned + # keep the freed allocation in this context's freed registry so a later + # copy/fill/kernel referencing this address is caught as use-after-free + memory_allocation.freed_by = state.get_api_context(ctx) + state.freed_memory_allocations(ctx, ctx_handle)[handle] = memory_allocation +} + +# the free was applied at entry, so restore the allocation if it actually failed +$on_erroneous_exit['zeMemFree'] = lambda { |state, ctx, payload| + ctx_handle = state.find_param(ctx, 'hContext') + handle = state.find_param(ctx, "ptr") + mem = state.freed_memory_allocations(ctx, ctx_handle).delete(handle) + if mem + mem.freed_by = nil + state.memory_allocations(ctx, ctx_handle)[handle] = mem + owned = mem.owned_by + owned.memory_allocations[ctx_handle][handle] = mem if owned + end +} \ No newline at end of file diff --git a/backends/ze/ze_validator_state_object.rb b/backends/ze/ze_validator_state_object.rb new file mode 100644 index 00000000..7eae5a88 --- /dev/null +++ b/backends/ze/ze_validator_state_object.rb @@ -0,0 +1,538 @@ +require 'babeltrace2' +require 'ze_library' +require 'set' +require 'ze_validator_zemodel' +require 'ze_validator_function_entry_exit_callbacks' +require 'ze_validator_state_object' +require 'yaml' +require 'json' + +class StateObject + attr_reader :state + attr_reader :ze_thread_safety + attr_reader :lock_shared_object_on_entry + attr_reader :unlock_shared_object_on_exit + attr_accessor :print_tracker + attr_accessor :device_agnostic + attr_accessor :performance + attr_accessor :memory_in_transit + attr_reader :device_properties + + def initialize(**opts) + @deprecated = JSON.parse(File.read(File.join(DATADIR, 'ze_deprecated.json'))) + @device_properties = load_device_properties + #for supressing redundant error outputs + @print_tracker = Hash.new { |h, k| h[k] = 0 } + # Append a third slot to every entry: "has this warning been printed?". + @deprecated.each do |api, (version, replacement)| + @deprecated[api] = [version, replacement, false] + end + @performance = opts[:performance] + @device_agnostic = opts[:device_agnostic] + @state = Hash.new { |h, k| h[k] = ZEModel::Node.new(k) } + @ze_thread_safety = YAML::load_file(File.join(DATADIR, 'ze_thread_safety.yaml')) + @lock_shared_object_on_entry = Hash.new { |h, k| h[k] = [] } + @unlock_shared_object_on_exit = Hash.new { |h, k| h[k] = [] } + @init_called = Hash.new { |h, k| h[k] = false } #pid : init called status + @memory_in_transit = Hash.new {|h,k| h[k] = []} #pid : [[mem, (src|dst)]] list of memories being transferred + @printed_init_error = false + @deferred_units = [] + @signal_epoch = 0 + @ze_thread_safety.each { |api, objects| + objects.each { |o| + @lock_shared_object_on_entry[api].push( lambda { |state, ctx, payload| + #at entry the input args are in payload directly + handle = payload[o.first] + if handle.kind_of? Array + handle.each { |h| + obj = state.find_object(ctx, o.last, h) + obj.lock(state, ctx) if obj + } + else + obj = state.find_object(ctx, o.last, handle) + obj.lock(state, ctx) if obj + end + }) + @unlock_shared_object_on_exit[api].push( lambda { |state, ctx, payload| + #at exit payload holds only outputs, so the input + #handle comes from the saved entry payload + handle = state.find_param(ctx, o.first) + if handle.kind_of? Array + handle.each { |h| + obj = state.find_object(ctx, o.last, h) + obj.unlock(ctx) if obj + } + else + obj = state.find_object(ctx, o.last, handle) + obj.unlock(ctx) if obj + end + }) + } + } + + end + + # Returns the parsed ze_device_property.json, or nil if it is missing or + # unparseable so the validator degrades gracefully. + def load_device_properties + path = File.join(DATADIR, 'ze_device_property.json') + return nil unless File.file?(path) + JSON.parse(File.read(path)) + rescue JSON::ParserError => e + $stderr.puts "Warning: could not parse #{path}: #{e.message}" + nil + end + + # Returns a map e.g.,{"ordinal"=>1, "type"=>"copy", "numQueues"=>8} or nil. Without a + # device index it uses the first device. + def command_queue_group(ordinal, device_index: nil) + return nil unless @device_properties + devices = @device_properties['devices'] || [] + devices = devices.select { |d| d['device_index'] == device_index } if device_index + devices.each do |dev| + group = (dev['command_queue_groups'] || []).find { |g| g['ordinal'] == ordinal } + return group if group + end + nil + end + + + # The innermost API call currently executing on this thread, or nil. + def get_last_entry(context) + @state[context['hostname']].processes[context['vpid']].threads[context['vtid']].last_entry + end + + def get_thread(context) + @state[context['hostname']].processes[context['vpid']].threads[context['vtid']] + end + + def get_process(context) + @state[context['hostname']].processes[context['vpid']] + end + + # Checks that the call we return from is on top of this thread's stack. A + # mismatch means the model lost sync with the trace, so it aborts. + def check_last_entry(context) + last_entry = get_last_entry(context) + unless last_entry && last_entry.name == context['api'] + raise "Invalid State in #{context['api']}" + end + end + + + # Pushes a call frame, so a traced API calling another traced API on the same + # thread nests correctly. + def set_last_entry(state, context, payload) + get_thread(context).call_stack.push(ZEModel::ApiCall.new(context['api'], payload)) + end + + # Pops the innermost frame on return, exposing the caller's frame. + def reset_last_entry(context) + get_thread(context).call_stack.pop + end + + # Decides whether on_exit runs the success or the error callback. + def validate_result(payload) + ZE::ZEResult.from_native(payload["zeResult"], nil) == :ZE_RESULT_SUCCESS + end + + # Zero-padded so the same object reads identically everywhere, which also + # makes these strings safe as print_tracker dedup keys. + def get_handle_str(handle) + '0x%016x' % handle + end + + # "hostname - pid", for process-wide findings such as leaks and deadlocks. + def get_proc_context_str(context) + "#{context['hostname']} - #{context['vpid']}" + end + + # "tid in zeSomeApi": identifies the specific call. + def get_api_context(context) + "#{context['vtid']} in #{context['api']}" + end + + # "hostname - pid - tid in zeSomeApi", for findings attributable to one call. + def get_context_str(context) + "#{get_proc_context_str(context)} - #{get_api_context(context)}" + end + + # Warns once per deprecated API actually used. + def print_deprecation_warning(old_api) + if @deprecated.include?(old_api) and @deprecated[old_api][2] + deprecated_since = @deprecated[old_api][0] + new_api = @deprecated[old_api][1] + if deprecated_since == "" + puts "#{old_api} is deprecated. Please use #{new_api} instead." + else + puts "#{old_api} is deprecated since #{deprecated_since}. Please use #{new_api} instead." + end + end + end + + def print_portability_error(context,str) + $stderr.puts "Level Zero Portability Error: on #{get_context_str(context)}: #{str}\n\n" + end + def print_performance_issue(context,str) + $stderr.puts "Level Zero Performance Issue: on #{get_context_str(context)}: #{str}\n\n" + end + def print_usage_error(context, str) + $stderr.puts "Level Zero Usage Error: on #{get_context_str(context)}: #{str}\n\n" + end + + def print_crash_error(context, str) + $stderr.puts "Level Zero Crash Error: on #{get_context_str(context)}: #{str}\n\n" + end + + def print_memory_error(context, str) + $stderr.puts "Level Zero Memory Error: on #{get_context_str(context)}: #{str}\n\n" + end + + def print_deadlock_error(context, str) + $stderr.puts "Level Zero Deadlock: on #{get_proc_context_str(context)}: #{str}\n\n" + end + + + def print_leak_error(context, type, handle, memtypestr="") + if memtypestr.empty? + $stderr.puts "Level Zero Leak: on #{get_proc_context_str(context)}: #{type} #{get_handle_str(handle)}\n\n" + else + $stderr.puts "Level Zero Leak #{memtypestr}-memory: on #{get_proc_context_str(context)}: #{type} #{get_handle_str(handle)}\n\n" + end + end + + # Not a finding about the traced program: the validator's own bookkeeping is + # wrong, so further output would be untrustworthy. + def raise_internal_error(context, str) + raise "Invalid state #{get_context_str(context)}: #{str}" + end + + # Deduped per (object, other holder) so a racing loop reports once. + def print_race_condition(context, other_context, type, handle) + if @print_tracker["#{type}-#{get_handle_str(handle)}-#{get_api_context(other_context)}"] == 0 + @print_tracker["#{type}-#{get_handle_str(handle)}-#{get_api_context(other_context)}"] = 1 + print_usage_error(context, "concurrent acces to #{type} #{get_handle_str(handle)}, already held by #{get_api_context(other_context)}") + end + end + + # Passed as the block to Hash#delete, so it fires when a destroy names a + # handle the model never recorded. + def object_not_found(context, type, handle, sub_context = nil) + raise_internal_error(context, "event_pool #{get_handle_str(handle)} not found#{sub_context ? " in #{sub_context}" : ""}") + end + + # Reads one input argument of the call executing on this thread. Works at + # _exit too, since the entry payload is still on the call stack. + def find_param(context, name) + get_last_entry(context).params[name] + end + + # The whole handle -> object table for a type. + def find_objects(context, type) + get_process(context).instance_variable_get("@#{type}s") + end + + # `handle` may be the handle itself or the name of the param carrying it. + def find_object(context, type, handle) + handle = find_param(context, handle) if handle.kind_of? String + find_objects(context, type)[handle] + end + + # The live allocations of one Level Zero context (address -> Memory). + def memory_allocations(context, ctx_handle) + get_process(context).memory_allocations[ctx_handle] + end + + # The freed allocations of one Level Zero context, for use-after-free checks. + def freed_memory_allocations(context, ctx_handle) + get_process(context).freed_memory_allocations[ctx_handle] + end + + # Yields [unit, op] for every copy op still pending in this process, so a free + # can tell whether the buffer is still referenced by submitted work. + def each_inflight_copy_op(context) + @deferred_units.each do |unit| + next unless unit.context['hostname'] == context['hostname'] && + unit.context['vpid'] == context['vpid'] + unit.ops[unit.cursor..].each do |op| + next unless op && op.kind == :copy + yield unit, op + end + end + end + + # True if a prior submission of this command list has not drained yet. + def command_list_in_flight?(context, handle) + @deferred_units.any? do |unit| + unit.cmd_list_handle == handle && + unit.context['hostname'] == context['hostname'] && + unit.context['vpid'] == context['vpid'] && + !unit.done? + end + end + + # Decodes a raw descriptor blob from the trace into a typed FFI struct, or nil + # for a null descriptor. + def to_struct(memory, klass) + memory.size > 0 ? klass.new(FFI::MemoryPointer.from_string(memory)) : nil + end + + # Returns the Event for a handle, nil for a null or unknown one. + def event_by_handle(context, handle) + return nil if handle.nil? || handle == 0 + find_objects(context, 'event')[handle] + end + + # Signals an event and notes progress so pump_deferred sweeps again. + def signal_event(context, handle, by = nil) + ev = event_by_handle(context, handle) + if ev + ev.signal(by) + @signal_epoch += 1 + end + ev + end + + # Reset's the given handle's event + def reset_event(context, handle) + event_by_handle(context, handle)&.reset + end + + # Records that the host observed an event's signaled state. + def observe_event(context, handle) + event_by_handle(context, handle)&.observe + end + + # A device-wide host synchronization means every signaled event was consumed. + def observe_all_signaled_events(context) + find_objects(context, 'event').each_value { |ev| ev.observe if ev.signaled } + end + + # True once every wait handle is signaled. Untracked handles count as + # satisfied, so we never invent a deadlock for one. + def waits_satisfied?(context, waits) + return true if waits.nil? || waits.empty? + waits.all? { |h| ev = event_by_handle(context, h); ev.nil? || ev.signaled } + end + + # Runs the op the cursor points at, applying its deferred checks and signal. + def run_deferred_op(unit) + context = unit.context + op = unit.current_op + if op.kind == :copy + check_oob_copy(self, context, op.params) + #a pointer freed before this copy's turn to execute is a use-after-free + check_use_after_free(self, context, op.params) + end + #a memory-ranges barrier references memory freed before its turn is a UAF + check_uaf_ranges_barrier(self, context, op.params) if op.kind == :ranges_barrier + #a reset takes effect before this op signals its own completion event + reset_event(context, op.params[:reset_handle]) if op.kind == :reset + signaled = false + if op.signal + #the completion event must be unsignaled here: reuse without an intervening + #reset (or a concurrent double-signal) is a misuse + check_event_signal_reuse(self, context, op.signal, op.api || 'a command list append') + signal_event(context, op.signal, op.api) + unit.pending_signals.delete(op.signal) + signaled = true + end + unit.cursor += 1 + unit.blocked_on = [] + signaled + end + + # Advances every unit as far as its wait-events allow, sweeping until a whole + # pass makes no progress since one unit's signal can unblock another. + def pump_deferred + progress = true + while progress + progress = false + @deferred_units.each do |unit| + until unit.done? + op = unit.current_op + if waits_satisfied?(unit.context, op.waits) + run_deferred_op(unit) + progress = true + else + #park the unit on this op and record what it is blocked on so the + #deadlock detector can see the wait-for edges + unit.blocked_on = op.waits.reject { |h| + ev = event_by_handle(unit.context, h); ev.nil? || ev.signaled + } + break + end + end + end + @deferred_units.reject!(&:done?) + end + end + + # Registers a command list's ops as a deferred unit and pumps. + def run_deferred_list(context, ops, label, in_order: false, cmd_list_handle: nil) + @deferred_units << ZEModel::DeferredUnit.new(ops, context, label, in_order: in_order, + cmd_list_handle: cmd_list_handle) + pump_deferred + end + + # Each submitted list becomes its own unit: lists in one submit are ordered by + # events, not by list order, so a cycle between two of them is a real deadlock. + def enqueue_deferred_execution(context, command_lists) + command_lists.each do |cl| + next unless cl + run_deferred_list(context, cl.ops.dup, "command_list (#{get_handle_str(cl.handle)})", + in_order: cl.in_order, cmd_list_handle: cl.handle) + end + end + + # Immediate lists execute each op as it is appended, but still go through the + # same machinery so they get the same checks. + def enqueue_immediate_op(context, op, handle = nil) + label = handle ? "immediate command list (#{get_handle_str(handle)})" \ + : 'immediate command list' + run_deferred_list(context, [op], label) + end + + # End-of-trace drain: reports deadlocks among whatever is still stuck, then + # forces each remaining op so its deferred checks run against the final state. + def flush_deferred + pump_deferred + return if @deferred_units.empty? + check_circular_deadlock(self, @deferred_units) + check_in_order_self_deadlock(self, @deferred_units) + until @deferred_units.empty? + unit = @deferred_units.first + #force the op the unit is stuck on: report its unsignaled waits, then run it + report_unsignaled_waits(self, unit.context, unit.current_op.waits) if unit.current_op + run_deferred_op(unit) unless unit.done? + @deferred_units.reject!(&:done?) + #a forced completion may unblock others cleanly + pump_deferred + end + end + + + # Checks for the issues only visible at end of trace: deadlocks, calls that + # never returned, and objects that were never destroyed. + def check_issues() + #drain deferred command-list executions before reporting leaks/crashes + flush_deferred + crash = false + @state.each { |hostname, node| + node.processes.each { |pid, process| + process.threads.each { |tid, thread| + #any frame still on the stack is a call that never returned + thread.call_stack.each { |frame| + ctx = {'hostname' => hostname, 'vpid'=> pid, 'vtid' => tid, 'api' => frame.name} + print_crash_error(ctx, 'command did not finish execution') + crash = true + } + } + } + } + + unless crash && false + @state.each { |hostname, node| + node.processes.each { |pid, process| + ctx = {'hostname' => hostname, 'vpid'=> pid} + [ 'context', + 'event_pool', + 'command_queue', + 'fence', + 'command_list', + 'module', + 'module_build_log', + 'kernel', + ].each { |t| + process.objects(t).each { |h, c| + print_leak_error(ctx, t, h) + } + } + process.objects('memory_allocation').each { |_ctx_handle, allocs| + allocs.each { |h, c| + print_leak_error(ctx, 'memory_allocation', h, c.instance_variable_get(:@memtypestr)) + } + } + } + } + end + end + + + # Checks that zeInit or zeInitDrivers came before any other API call. Keyed by + # pid, and reported once per run. + def check_initialization(context) + if ZEModel::INIT_API_NAMES.include?(context['api']) + @init_called[context['vpid']] = true + end + + if !@init_called[context['vpid']] && !@printed_init_error + self.print_usage_error(context, "zeInit or zeDriversInit wasn't called before #{context['api']}") + @printed_init_error = true + end + end + + + # Pushes the call frame, takes the thread-safety locks and runs the API's + # entry callback. Checks run here when the call itself might crash. + def on_entry(m,hostname, context,payload) + set_last_entry(self, context, payload) #sets the per-thread callstack of the APIs + @lock_shared_object_on_entry[m[1]].each { |l| + l.call(self, context, payload) + } + #modifies the satate based on entry fields. Needed because some fields are easier to access it from the entry + l = $upon_entry[m[1]] + l.call(self,context,payload) if l + end + + def on_exit(m,hostname,context,payload) + #unlock the shared object if the api name matches the predefined in ze_thread_safety.yaml + @unlock_shared_object_on_exit[m[1]].reverse_each { |l| + l.call(self, context, payload) + } + + #check if the return code indicates successful return from the API call + if validate_result(payload) + l = $on_successful_exit[m[1]] #This might be a problem for tracking erroneous exits. + l.call(self, context, payload) if l + else + l = $on_erroneous_exit[m[1]] + l.call(self, context, payload) if l + end + + check_last_entry(context) #When we return from _exit, we need to see what we saw in _entry for the current thread_id + reset_last_entry(context) #Reset the callstack for current thread_id + end + + # The main loop: returns the lambda called with each batch of decoded + # messages. + def consume = lambda { |iterator, _| + iterator.next_messages.each do |m| + next unless m.type == :BT_MESSAGE_TYPE_EVENT + e = m.event + #splits "lttng_ust_ze:zeMemAllocDevice_entry" into the API name (m[1]) + #and the phase (m[2]); anything else is ignored + m = e.name.match(/:(z.*)_(entry|exit)/) + if m + hostname = e.stream.trace.get_environment_entry_value_by_name('hostname').value + context = e.get_common_context_field.value + #the event's own fields: input args at _entry, results at _exit + payload = e.payload_field.value + context['hostname'] = hostname + context['api'] = m[1] + #zeDriversInit or zeInit must be the first one to be called before any api calls + check_initialization(context) + print_deprecation_warning(m[1]) if @deprecated[m[1]] + + if m[2] == 'entry' + on_entry(m, hostname, context, payload) + elsif m[2] == 'exit' + on_exit(m, hostname, context, payload) + end + # Runs the blocked commands, if the wait-event(s) are satisfied + pump_deferred + end + end + } + +end diff --git a/backends/ze/ze_validator_zemodel.rb b/backends/ze/ze_validator_zemodel.rb new file mode 100644 index 00000000..100d05f1 --- /dev/null +++ b/backends/ze/ze_validator_zemodel.rb @@ -0,0 +1,457 @@ +require 'set' + +module ZEModel + #One of these APIs must be called before any other calls + INIT_API_NAMES = ['zeInit', 'zeInitDrivers'] + #This defines the object in which most ze objects (command list, command queue) extend form + class Object + attr_reader :handle + attr_accessor :status + + # returns what object the caller is + # e.g., 'Device' will return device + def self.typename + @typename + end + + # lock is needed to check for concurrent properties. + # e.g., calling the same APIs that can be called from simultaneous threads (zeCommandListAppendBarrier) + def initialize(handle) + @handle = handle + @lock = nil + end + + # Reports a race: the trace is timestamp-ordered, so finding the object + # already locked means two calls really overlapped. + def lock(state,ctx) + if @lock + state.print_race_condition(ctx, @lock, self.class.typename, @handle) + else + @lock = ctx + end + end + + # Releases only if this same call is the holder, so a call that lost the + # race above does not steal the real holder's lock on its way out. + def unlock(ctx) + if @lock == ctx + @lock = nil + end + end + end + + class Driver < Object + @typename = 'driver' + attr_reader :devices + + def initialize(handle) + super + @devices = [] + end + end + + + class Device < Object + @typename = 'device' + attr_reader :properties #delete + attr_reader :sub_devices + # ctx_handle -> {addr -> Memory}, keyed by context like Process#memory_allocations + attr_accessor :memory_allocations + # the "did the app query this before assuming it?" flags the portability + # checks look at (see check_group_property_queued) + attr_accessor :property_fetched + attr_accessor :cmd_queue_group_properties_queried + + def initialize(handle) + super + @sub_devices = [] + @memory_allocations = Hash.new { |h, k| h[k] = {} } + @property_fetched = false + @cmd_queue_group_properties_queried = false + end + end + + + + class SubDevice < Device + attr_reader :parent + def initialize(handle, parent) + @parent = parent + super(handle) + end + end + + #create memory object so that device, shared, host mem allocs can be differentiated + class Memory < Object + @typename = 'memory_allocation' + attr_reader :context + attr_reader :size + attr_reader :owned_by # the Device for a device allocation; nil for host + attr_accessor :memtypestr # "device" | "host" | "shared" + attr_accessor :base + # the zeMemFree that released this allocation, nil while live. Freed + # allocations are kept so a later reference is caught as use-after-free. + attr_accessor :freed_by + + def initialize(handle, context, size, owned_by, memtypestr="shared") + super(handle) + @context = context + @size = size + @owned_by = owned_by + @memtypestr = memtypestr + @base = handle + @freed_by = nil + end + end + + + class Context < Object + @typename = 'context' + attr_reader :driver + attr_reader :desc + attr_reader :devices + attr_reader :event_pools + attr_reader :command_queues + attr_reader :command_lists + attr_reader :modules + attr_reader :module_build_logs + + def initialize(handle, driver, desc, devices = nil) + super(handle) + @driver = driver + @desc = desc + @devices = devices + + @event_pools = {} + @command_queues = {} + @command_lists = {} + @modules = {} #binaries for gpu + @module_build_logs = {} + end + end + + class EventPool < Object + @typename = 'event_pool' + attr_reader :context + attr_reader :desc + attr_reader :devices + attr_reader :events + # slot indices not yet in use: zeEventCreate removes one (double use = + # error), zeEventDestroy puts it back (double free = error) + attr_reader :indices + + def initialize(handle, context, desc, devices = nil) + super(handle) + @context = context + @desc = desc + @devices = devices + @events = {} + @indices = Set.new(desc[:count].times.to_a) + end + end + + class Event < Object + @typename = 'event' + attr_reader :event_pool + attr_reader :desc + attr_accessor :signaled + attr_reader :signaled_by # who last signaled it, for diagnostics + # whether the host observed the signaled state since the last signal. Tells + # a concurrent double-signal (never consumed) from a reuse-without-reset. + attr_reader :observed + + def initialize(handle, event_pool, desc) + super(handle) + @event_pool = event_pool + @desc = desc + #event can have 2 states, not signaled or signaled + @signaled = false + @signaled_by = nil + @observed = false + end + + # `by` records who signaled it, for messages + def signal(by = nil) + @signaled = true + @signaled_by = by + @observed = false + end + + def reset + @signaled = false + @signaled_by = nil + @observed = false + end + + def observe + @observed = true + end + end + + class CommandQueue < Object + @typename = 'command_queue' + attr_reader :context + attr_reader :device + # :ordinal and :index are checked against the topology in + # ze_device_property.json + attr_reader :desc + attr_reader :fences + + def initialize(handle, context, device, desc) + super(handle) + @context = context + @device = device + @desc = desc + @fences = {} + @valid_fences = Hash.new { |h, k| h[k] = true } #fences that have been reset or haven't been signaled + end + end + + class Fence < Object + @typename = 'fence' + attr_reader :command_queue + attr_reader :desc + # not_signaled -> in_use -> signaled -> not_signaled (zeFenceReset). Compared + # as `fence.status == fence.signaled`; see check_fence_misuse. + attr_accessor :status + attr_reader :not_signaled + attr_reader :in_use + attr_reader :signaled + + + def initialize(handle, command_queue, desc) + super(handle) + @command_queue = command_queue + @desc = desc + @not_signaled = 0 + @in_use = 1 + @signaled = 2 + @status = @not_signaled + end + + end + + class CommandList < Object + @typename = 'command_list' + attr_reader :context + attr_reader :device + attr_reader :desc # nil for immediate lists + attr_reader :altdesc # queue descriptor, immediate lists only + attr_accessor :associated_command_queue + attr_accessor :immediate + attr_accessor :associated_ordinal + # enables check_in_order_self_deadlock: in an in-order list an op waiting on + # an event only a later op in the same list signals can never complete + attr_accessor :in_order + # RecordedOps in append order, replayed when the list is executed so the + # deferred checks run at the point the op would actually execute + attr_accessor :ops + @@INITIALIZED = 0 #created or being properly recycled + @@CLOSED = 1 + @@DESTROYED = 2 + + def initialize(handle, context, device, desc, altdesc) + super(handle) + @context = context + @device = device + @desc = desc + @altdesc = altdesc + @associated_command_queue = nil + @status = @@INITIALIZED + @immediate = false + @associated_ordinal = 0 + @in_order = false + @api_calls = [] + @ops = [] + end + + # An immediate list is given a queue descriptor instead of a list one, so a + # nil desc identifies it. + def immediate? + return !desc + end + end + + class RecordedOp + # :copy, :wait, :signal, :reset, :barrier, :ranges_barrier or :launch + attr_reader :kind + attr_reader :signal # event this op signals on completion (nil if none) + attr_reader :waits # events that must be signaled before this op may run + attr_reader :params + attr_reader :api + + def initialize(kind, signal: 0, waits: [], params: {}, api: nil) + @kind = kind + #normalize a null (0) signal handle to nil so "does this op signal?" is a + #simple truthiness test + @signal = (signal && signal != 0) ? signal : nil + @waits = waits || [] + @params = params + @api = api || params[:api] + end + end + + class DeferredUnit + attr_reader :ops # snapshot of the list's ops for this execution + attr_reader :context # trace context captured at submit time + attr_reader :label # e.g. "command_list (0x00007f...)", for messages + attr_accessor :cursor # index of the next op to run; == ops.size means done + attr_accessor :blocked_on # events the current op is still waiting for + # Events this unit has not signaled yet. If unit U is blocked on an event + # only in V's pending_signals, U waits on V: an edge in the wait-for graph. + attr_accessor :pending_signals + attr_reader :in_order + # the command list this unit came from, so list-scoped checks can find their + # units without matching on the label string + attr_reader :cmd_list_handle + + def initialize(ops, context, label, in_order: false, cmd_list_handle: nil) + @ops = ops + @context = context + @label = label + @cursor = 0 + @blocked_on = [] + @in_order = in_order + @cmd_list_handle = cmd_list_handle + #every event this unit will eventually signal, for the wait-for graph + @pending_signals = ops.map { |op| op.signal }.compact + end + + # true once every op has executed + def done? + @cursor >= @ops.size + end + + # the op the cursor currently points at (nil when done) + def current_op + @ops[@cursor] + end + end + + class Module < Object + @typename = 'module' + + class BuildLog < Object + @typename = 'module_build_log' + attr_reader :module # nil when the build failed and produced no module + + def initialize(handle, mod = nil) + super(handle) + @module = mod + end + end + + attr_reader :context + attr_reader :device + attr_reader :desc + attr_accessor :build_log + attr_reader :kernels + + def initialize(handle, context, device, desc) + super(handle) + @context = context + @device = device + @desc = desc + @kernels = {} + end + end + + class Kernel < Object + @typename = 'kernel' + attr_reader :module # also how the kernel's context is found + attr_reader :desc + attr_reader :name # kernel name from the descriptor, for diagnostics + + def initialize(handle, mod, desc, name) + super(handle) + @module = mod + @desc = desc + @name = name + end + end + + class ApiCall + attr_reader :name + attr_reader :params # the _entry payload, i.e. the call's input arguments + + def initialize(name, params) + @name = name + @params = params + end + end + + class Thread + attr_reader :vtid + # a stack, not a single slot: a traced API may call another traced API on + # the same thread (e.g. zelLoaderDriverCheck calls zeInit) + attr_reader :call_stack + + def initialize(vtid) + @vtid = vtid + @call_stack = [] + end + + # the innermost in-flight ApiCall, or nil if the thread has none + def last_entry + @call_stack.last + end + end + + class Process + attr_reader :vpid # LTTng virtual pid + attr_reader :threads # tid -> Thread (auto-created on first sight) + # handle -> object, one table per Level Zero object type + attr_reader :drivers + attr_reader :devices + attr_reader :contexts + attr_reader :event_pools + attr_reader :events + attr_reader :command_queues + attr_reader :fences + attr_reader :command_lists + attr_reader :modules + attr_reader :module_build_logs + # allocations kept after zeMemFree, for use-after-free detection + attr_reader :freed_memory_allocations + # { ctx_handle => { address => Memory } }: addresses are only guaranteed + # non-aliasing within a context, so a flat map would lose one of two. + attr_reader :memory_allocations + + def initialize(vpid) + @vpid = vpid + @threads = Hash.new { |h, k| h[k] = Thread.new(k) } + #can it model memory imports/exports?? + @drivers = {} + @event_dependencies = {} #for detecting deadlocks + @devices = {} + @contexts = {} + @event_pools = {} + @events = {} + @command_queues = {} + @fences = {} + @command_lists = {} + @modules = {} + @module_build_logs = {} + @kernels = {} + @memory_allocations = Hash.new { |h, k| h[k] = {} } + @freed_memory_allocations = Hash.new { |h, k| h[k] = {} } + end + + # objects('command_list') returns @command_lists, so callers can iterate + # object types by name (see StateObject#check_issues). + def objects(type) + instance_variable_get(:"@#{type}s") + end + end + + class Node + attr_reader :name # hostname + attr_reader :processes # pid -> Process (auto-created on first sight) + + def initialize(name) + @name = name + @processes = Hash.new { |h, k| h[k] = Process.new(k) } + end + end + +end diff --git a/configure.ac b/configure.ac index 27579632..f39fe943 100644 --- a/configure.ac +++ b/configure.ac @@ -181,6 +181,7 @@ AC_CONFIG_FILES([utils/test_wrapper_thapi_text_pretty.sh], [chmod +x utils/test_ AC_CONFIG_FILES([backends/opencl/tracer_opencl.sh], [chmod +x backends/opencl/tracer_opencl.sh]) AC_CONFIG_FILES([backends/opencl/extract_enqueues], [chmod +x backends/opencl/extract_enqueues]) AC_CONFIG_FILES([backends/ze/tracer_ze.sh], [chmod +x backends/ze/tracer_ze.sh]) +AC_CONFIG_FILES([backends/ze/ze_validator], [chmod +x backends/ze/ze_validator]) AC_CONFIG_FILES([backends/cuda/tracer_cuda.sh], [chmod +x backends/cuda/tracer_cuda.sh]) AC_CONFIG_FILES([backends/omp/tracer_omp.sh], [chmod +x backends/omp/tracer_omp.sh]) AC_CONFIG_FILES([backends/hip/tracer_hip.sh], [chmod +x backends/hip/tracer_hip.sh])