pub const SHADER_SRC: &str = r#"
@group(0) @binding(0) var<storage, read> node_labels: array<u32>;
@group(0) @binding(1) var<storage, read> edge_offsets: array<u32>;
@group(0) @binding(2) var<storage, read> edge_targets: array<u32>;
@group(0) @binding(3) var<storage, read> source_nodes: array<u32>;
@group(0) @binding(4) var<storage, read_write> findings: array<vec4<u32>>;
@group(0) @binding(5) var<storage, read_write> finding_count: array<atomic<u32>>;
@group(0) @binding(6) var<uniform> params: Params;
@group(0) @binding(7) var<storage, read_write> visited_set: array<u32>;
struct Params {
num_sources: u32,
num_nodes: u32,
max_findings: u32,
max_depth: u32,
words_per_source: u32,
_pad0: u32,
_pad1: u32,
_pad2: u32,
}
const LABEL_SOURCE: u32 = 1u;
const LABEL_SINK: u32 = 2u;
const MAX_BFS_QUEUE: u32 = 4096u;
@compute @workgroup_size(64)
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
let source_idx = gid.x;
if source_idx >= params.num_sources { return; }
let start_node = source_nodes[source_idx];
// BFS from start_node
var queue: array<u32, 4096>;
var queue_head: u32 = 0u;
var queue_tail: u32 = 1u;
queue[0] = start_node;
let base = source_idx * params.words_per_source;
// Mark start as visited
let start_word = start_node / 32u;
let start_bit = 1u << (start_node % 32u);
visited_set[base + start_word] = visited_set[base + start_word] | start_bit;
var depth: u32 = 0u;
while queue_head < queue_tail && depth < params.max_depth {
let level_end = queue_tail;
while queue_head < level_end {
let node = queue[queue_head];
queue_head += 1u;
// node_labels packed format: (kind << 24) | (label_type << 16) | (label_id & 0xFFFF)
let label_type = (node_labels[node] >> 16u) & 0xFFu;
// Stop traversal at sanitizers (label_type 4)
if label_type == 4u {
continue;
}
// Check if this node is a sink (skip self-taint on source==sink nodes)
if (label_type == LABEL_SINK || label_type == 3u) && node != start_node { // 3 is BOTH
let idx = atomicAdd(&finding_count[0], 1u);
if idx < params.max_findings {
findings[idx] = vec4<u32>(start_node, node, depth, source_idx);
}
}
// Explore neighbors
let edge_start = edge_offsets[node];
let edge_end = edge_offsets[node + 1u];
for (var e = edge_start; e < edge_end; e++) {
let tgt = edge_targets[e];
let word = tgt / 32u;
let bit = 1u << (tgt % 32u);
let visited_word = visited_set[base + word];
if (visited_word & bit) == 0u {
visited_set[base + word] = visited_word | bit;
if queue_tail < MAX_BFS_QUEUE {
queue[queue_tail] = tgt;
queue_tail += 1u;
}
}
}
}
depth += 1u;
}
}
"#;