compose and authority fixes. finish sprints.
This commit is contained in:
@@ -6,3 +6,4 @@ Source of truth: `docs/implplan/SPRINT_20260112_003_BE_csproj_audit_pending_appl
|
||||
| Task ID | Status | Notes |
|
||||
| --- | --- | --- |
|
||||
| AUDIT-HOTLIST-SIGNALS-0001 | DONE | Hotlist apply for `src/Signals/StellaOps.Signals/StellaOps.Signals.csproj`; audit tracker updated. |
|
||||
| MWD-001 | DONE | Implemented deterministic BTF fallback selection and metadata emission for runtime eBPF collection (`source_kind`, `source_path`, `source_digest`, `selection_reason`); verified with Signals and Scanner tests. |
|
||||
|
||||
@@ -119,7 +119,8 @@ public static class ServiceCollectionExtensions
|
||||
{
|
||||
var logger = sp.GetRequiredService<ILogger<RuntimeSignalCollector>>();
|
||||
var probeLoader = sp.GetRequiredService<IEbpfProbeLoader>();
|
||||
return new RuntimeSignalCollector(logger, probeLoader);
|
||||
var btfSelector = RuntimeBtfSourceSelector.CreateDefault(options.BtfSelectionOptions);
|
||||
return new RuntimeSignalCollector(logger, probeLoader, btfSelector);
|
||||
});
|
||||
|
||||
return services;
|
||||
@@ -187,4 +188,9 @@ public sealed class EbpfEvidenceOptions
|
||||
/// Collector options.
|
||||
/// </summary>
|
||||
public RuntimeEvidenceCollectorOptions CollectorOptions { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Runtime BTF selection options.
|
||||
/// </summary>
|
||||
public RuntimeBtfSelectionOptions BtfSelectionOptions { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -50,6 +50,11 @@ public interface IRuntimeSignalCollector
|
||||
/// <returns>True if eBPF probes can be loaded.</returns>
|
||||
bool IsSupported();
|
||||
|
||||
/// <summary>
|
||||
/// Gets deterministic BTF source metadata used for runtime collection.
|
||||
/// </summary>
|
||||
RuntimeBtfSelection GetBtfSelection();
|
||||
|
||||
/// <summary>
|
||||
/// Gets available probe types on this system.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
// <copyright file="RuntimeBtfSourceSelector.cs" company="StellaOps">
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
// </copyright>
|
||||
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace StellaOps.Signals.Ebpf.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for deterministic BTF source selection.
|
||||
/// </summary>
|
||||
public sealed record RuntimeBtfSelectionOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Candidate full-kernel BTF (vmlinux) paths in deterministic order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ExternalVmlinuxPaths { get; init; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Candidate split-BTF root directories in deterministic order.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> SplitBtfDirectories { get; init; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a deterministic BTF source for runtime eBPF collection.
|
||||
/// </summary>
|
||||
public sealed class RuntimeBtfSourceSelector
|
||||
{
|
||||
private const string KernelBtfPath = "/sys/kernel/btf/vmlinux";
|
||||
|
||||
private static readonly string[] DefaultSplitBtfDirectories =
|
||||
[
|
||||
"/var/lib/stellaops/btf/split",
|
||||
"/usr/share/stellaops/btf/split",
|
||||
"/usr/lib/stellaops/btf/split",
|
||||
];
|
||||
|
||||
private readonly RuntimeBtfSelectionOptions _options;
|
||||
private readonly Func<bool> _isLinuxPlatform;
|
||||
private readonly Func<string, bool> _fileExists;
|
||||
private readonly Func<string, byte[]> _readAllBytes;
|
||||
private readonly Func<string> _kernelReleaseProvider;
|
||||
private readonly Func<string> _kernelArchProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a selector with explicit environment probes.
|
||||
/// </summary>
|
||||
public RuntimeBtfSourceSelector(
|
||||
RuntimeBtfSelectionOptions? options = null,
|
||||
Func<bool>? isLinuxPlatform = null,
|
||||
Func<string, bool>? fileExists = null,
|
||||
Func<string, byte[]>? readAllBytes = null,
|
||||
Func<string>? kernelReleaseProvider = null,
|
||||
Func<string>? kernelArchProvider = null)
|
||||
{
|
||||
_options = options ?? new RuntimeBtfSelectionOptions();
|
||||
_isLinuxPlatform = isLinuxPlatform ?? (() => RuntimeInformation.IsOSPlatform(OSPlatform.Linux));
|
||||
_fileExists = fileExists ?? File.Exists;
|
||||
_readAllBytes = readAllBytes ?? File.ReadAllBytes;
|
||||
_kernelReleaseProvider = kernelReleaseProvider ?? GetKernelRelease;
|
||||
_kernelArchProvider = kernelArchProvider ?? GetKernelArch;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a selector using host runtime probes.
|
||||
/// </summary>
|
||||
public static RuntimeBtfSourceSelector CreateDefault(RuntimeBtfSelectionOptions? options = null)
|
||||
=> new(options: options);
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the BTF source with deterministic precedence.
|
||||
/// </summary>
|
||||
public RuntimeBtfSelection Resolve()
|
||||
{
|
||||
var kernelRelease = _kernelReleaseProvider();
|
||||
var kernelArch = _kernelArchProvider();
|
||||
|
||||
if (!_isLinuxPlatform())
|
||||
{
|
||||
return new RuntimeBtfSelection
|
||||
{
|
||||
SourceKind = "unsupported",
|
||||
SourcePath = null,
|
||||
SourceDigest = null,
|
||||
SelectionReason = "platform_not_linux",
|
||||
KernelRelease = kernelRelease,
|
||||
KernelArch = kernelArch,
|
||||
};
|
||||
}
|
||||
|
||||
if (TryCreateSelection(
|
||||
sourceKind: "kernel",
|
||||
sourcePath: KernelBtfPath,
|
||||
selectionReason: "kernel_btf_present",
|
||||
kernelRelease,
|
||||
kernelArch,
|
||||
out var kernelSelection))
|
||||
{
|
||||
return kernelSelection;
|
||||
}
|
||||
|
||||
foreach (var candidate in NormalizePaths(_options.ExternalVmlinuxPaths))
|
||||
{
|
||||
if (TryCreateSelection(
|
||||
sourceKind: "external-vmlinux",
|
||||
sourcePath: candidate,
|
||||
selectionReason: "external_vmlinux_configured",
|
||||
kernelRelease,
|
||||
kernelArch,
|
||||
out var externalSelection))
|
||||
{
|
||||
return externalSelection;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var candidate in BuildSplitBtfCandidates(kernelRelease, kernelArch))
|
||||
{
|
||||
if (TryCreateSelection(
|
||||
sourceKind: "split-btf",
|
||||
sourcePath: candidate,
|
||||
selectionReason: "split_btf_fallback",
|
||||
kernelRelease,
|
||||
kernelArch,
|
||||
out var splitSelection))
|
||||
{
|
||||
return splitSelection;
|
||||
}
|
||||
}
|
||||
|
||||
return new RuntimeBtfSelection
|
||||
{
|
||||
SourceKind = "unavailable",
|
||||
SourcePath = null,
|
||||
SourceDigest = null,
|
||||
SelectionReason = "no_btf_source_found",
|
||||
KernelRelease = kernelRelease,
|
||||
KernelArch = kernelArch,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> NormalizePaths(IEnumerable<string>? paths)
|
||||
{
|
||||
var seen = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (paths is null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
foreach (var path in paths)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var trimmed = path.Trim();
|
||||
if (seen.Add(trimmed))
|
||||
{
|
||||
yield return trimmed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<string> BuildSplitBtfCandidates(string kernelRelease, string kernelArch)
|
||||
{
|
||||
var splitRoots = NormalizePaths(
|
||||
_options.SplitBtfDirectories.Concat(DefaultSplitBtfDirectories));
|
||||
|
||||
foreach (var root in splitRoots)
|
||||
{
|
||||
var releaseArchDir = Path.Combine(root, kernelRelease, kernelArch);
|
||||
yield return Path.Combine(releaseArchDir, "vmlinux.btf");
|
||||
yield return Path.Combine(root, kernelRelease, $"{kernelArch}.btf");
|
||||
yield return Path.Combine(root, kernelRelease, "vmlinux.btf");
|
||||
yield return Path.Combine(root, $"{kernelRelease}.btf");
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryCreateSelection(
|
||||
string sourceKind,
|
||||
string sourcePath,
|
||||
string selectionReason,
|
||||
string kernelRelease,
|
||||
string kernelArch,
|
||||
out RuntimeBtfSelection selection)
|
||||
{
|
||||
selection = null!;
|
||||
|
||||
if (!_fileExists(sourcePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var digest = "sha256:" + Convert.ToHexStringLower(SHA256.HashData(_readAllBytes(sourcePath)));
|
||||
selection = new RuntimeBtfSelection
|
||||
{
|
||||
SourceKind = sourceKind,
|
||||
SourcePath = sourcePath,
|
||||
SourceDigest = digest,
|
||||
SelectionReason = selectionReason,
|
||||
KernelRelease = kernelRelease,
|
||||
KernelArch = kernelArch,
|
||||
};
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetKernelRelease()
|
||||
{
|
||||
const string releasePath = "/proc/sys/kernel/osrelease";
|
||||
if (_fileExists(releasePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var release = Encoding.UTF8.GetString(_readAllBytes(releasePath)).Trim();
|
||||
if (!string.IsNullOrWhiteSpace(release))
|
||||
{
|
||||
return release;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fall through to runtime description.
|
||||
}
|
||||
}
|
||||
|
||||
return RuntimeInformation.OSDescription.Trim();
|
||||
}
|
||||
|
||||
private static string GetKernelArch()
|
||||
{
|
||||
return RuntimeInformation.OSArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "x86_64",
|
||||
Architecture.X86 => "x86",
|
||||
Architecture.Arm64 => "arm64",
|
||||
Architecture.Arm => "arm",
|
||||
_ => RuntimeInformation.OSArchitecture.ToString().ToLowerInvariant(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ using StellaOps.Reachability.Core;
|
||||
using StellaOps.Signals.Ebpf.Probes;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
@@ -27,16 +26,20 @@ public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposabl
|
||||
private readonly IEbpfProbeLoader _probeLoader;
|
||||
private readonly ConcurrentDictionary<Guid, CollectionSession> _activeSessions;
|
||||
private readonly bool _isSupported;
|
||||
private readonly RuntimeBtfSelection _btfSelection;
|
||||
private bool _disposed;
|
||||
|
||||
public RuntimeSignalCollector(
|
||||
ILogger<RuntimeSignalCollector> logger,
|
||||
IEbpfProbeLoader probeLoader)
|
||||
IEbpfProbeLoader probeLoader,
|
||||
RuntimeBtfSourceSelector? btfSourceSelector = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_probeLoader = probeLoader;
|
||||
_activeSessions = new ConcurrentDictionary<Guid, CollectionSession>();
|
||||
_isSupported = CheckEbpfSupport();
|
||||
var selector = btfSourceSelector ?? RuntimeBtfSourceSelector.CreateDefault();
|
||||
_btfSelection = selector.Resolve();
|
||||
_isSupported = !string.IsNullOrWhiteSpace(_btfSelection.SourcePath);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -51,7 +54,7 @@ public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposabl
|
||||
if (!_isSupported)
|
||||
{
|
||||
throw new PlatformNotSupportedException(
|
||||
"eBPF is not supported on this platform. Linux 5.8+ with BTF enabled is required.");
|
||||
$"eBPF is not supported on this platform. Selection reason: {_btfSelection.SelectionReason}");
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
@@ -180,6 +183,7 @@ public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposabl
|
||||
ObservedNodeHashes = observedNodeHashes,
|
||||
ObservedPathHashes = observedPathHashes,
|
||||
CombinedPathHash = combinedPathHash,
|
||||
BtfSelection = _btfSelection,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -219,6 +223,9 @@ public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposabl
|
||||
/// <inheritdoc />
|
||||
public bool IsSupported() => _isSupported;
|
||||
|
||||
/// <inheritdoc />
|
||||
public RuntimeBtfSelection GetBtfSelection() => _btfSelection;
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes()
|
||||
{
|
||||
@@ -256,18 +263,6 @@ public sealed class RuntimeSignalCollector : IRuntimeSignalCollector, IDisposabl
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private static bool CheckEbpfSupport()
|
||||
{
|
||||
// eBPF is only supported on Linux 5.8+ with BTF
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for BTF support by looking for /sys/kernel/btf/vmlinux
|
||||
return File.Exists("/sys/kernel/btf/vmlinux");
|
||||
}
|
||||
|
||||
private async Task ProcessEventsAsync(CollectionSession session, CancellationToken ct)
|
||||
{
|
||||
var rateLimiter = new RateLimiter(session.Options.MaxEventsPerSecond);
|
||||
|
||||
@@ -8,6 +8,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using StellaOps.Signals.Ebpf.Probes;
|
||||
using StellaOps.Signals.Ebpf.Schema;
|
||||
using StellaOps.Signals.Ebpf.Services;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
||||
/// <summary>
|
||||
@@ -36,6 +37,86 @@ public sealed class RuntimeSignalCollectorTests
|
||||
Assert.True(isSupported == false || Environment.OSVersion.Platform == PlatformID.Unix);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBtfSelection_PrefersKernelBtfOverConfiguredFallback()
|
||||
{
|
||||
var external = "/opt/stellaops/btf/vmlinux-6.8.0-test";
|
||||
var collector = CreateCollectorWithFiles(
|
||||
CreateFileMap("/sys/kernel/btf/vmlinux", external),
|
||||
new RuntimeBtfSelectionOptions
|
||||
{
|
||||
ExternalVmlinuxPaths = [external],
|
||||
});
|
||||
|
||||
var selection = collector.GetBtfSelection();
|
||||
|
||||
Assert.True(collector.IsSupported());
|
||||
Assert.Equal("kernel", selection.SourceKind);
|
||||
Assert.Equal("/sys/kernel/btf/vmlinux", selection.SourcePath);
|
||||
Assert.Equal("kernel_btf_present", selection.SelectionReason);
|
||||
Assert.StartsWith("sha256:", selection.SourceDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBtfSelection_UsesExternalVmlinuxWhenKernelBtfMissing()
|
||||
{
|
||||
var external = "/opt/stellaops/btf/vmlinux-6.8.0-test";
|
||||
var collector = CreateCollectorWithFiles(
|
||||
CreateFileMap(external),
|
||||
new RuntimeBtfSelectionOptions
|
||||
{
|
||||
ExternalVmlinuxPaths = [external],
|
||||
});
|
||||
|
||||
var selection = collector.GetBtfSelection();
|
||||
|
||||
Assert.True(collector.IsSupported());
|
||||
Assert.Equal("external-vmlinux", selection.SourceKind);
|
||||
Assert.Equal(external, selection.SourcePath);
|
||||
Assert.Equal("external_vmlinux_configured", selection.SelectionReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetBtfSelection_UsesSplitBtfFallbackWhenConfigured()
|
||||
{
|
||||
var splitRoot = "/var/lib/stellaops/btf/split";
|
||||
var splitCandidate = Path.Combine(splitRoot, "6.8.0-test", "x86_64", "vmlinux.btf");
|
||||
var collector = CreateCollectorWithFiles(
|
||||
CreateFileMap(splitCandidate),
|
||||
new RuntimeBtfSelectionOptions
|
||||
{
|
||||
SplitBtfDirectories = [splitRoot],
|
||||
});
|
||||
|
||||
var selection = collector.GetBtfSelection();
|
||||
|
||||
Assert.True(collector.IsSupported());
|
||||
Assert.Equal("split-btf", selection.SourceKind);
|
||||
Assert.Equal(splitCandidate, selection.SourcePath);
|
||||
Assert.Equal("split_btf_fallback", selection.SelectionReason);
|
||||
Assert.Equal("6.8.0-test", selection.KernelRelease);
|
||||
Assert.Equal("x86_64", selection.KernelArch);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StopCollectionAsync_EmitsBtfSelectionMetadata()
|
||||
{
|
||||
var external = "/opt/stellaops/btf/vmlinux-6.8.0-test";
|
||||
var collector = CreateCollectorWithFiles(
|
||||
CreateFileMap(external),
|
||||
new RuntimeBtfSelectionOptions
|
||||
{
|
||||
ExternalVmlinuxPaths = [external],
|
||||
});
|
||||
|
||||
var handle = await collector.StartCollectionAsync("container-1", new RuntimeSignalOptions());
|
||||
var summary = await collector.StopCollectionAsync(handle);
|
||||
|
||||
Assert.NotNull(summary.BtfSelection);
|
||||
Assert.Equal("external-vmlinux", summary.BtfSelection!.SourceKind);
|
||||
Assert.Equal(external, summary.BtfSelection.SourcePath);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetSupportedProbeTypes_ReturnsEmptyOnUnsupportedPlatform()
|
||||
{
|
||||
@@ -206,4 +287,33 @@ public sealed class RuntimeSignalCollectorTests
|
||||
|
||||
public IReadOnlyList<ProbeType> GetSupportedProbeTypes() => [];
|
||||
}
|
||||
|
||||
private static RuntimeSignalCollector CreateCollectorWithFiles(
|
||||
IReadOnlyDictionary<string, byte[]> files,
|
||||
RuntimeBtfSelectionOptions options)
|
||||
{
|
||||
var selector = new RuntimeBtfSourceSelector(
|
||||
options,
|
||||
isLinuxPlatform: () => true,
|
||||
fileExists: path => files.ContainsKey(path),
|
||||
readAllBytes: path => files[path],
|
||||
kernelReleaseProvider: () => "6.8.0-test",
|
||||
kernelArchProvider: () => "x86_64");
|
||||
|
||||
return new RuntimeSignalCollector(
|
||||
NullLogger<RuntimeSignalCollector>.Instance,
|
||||
new MockProbeLoader(),
|
||||
selector);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, byte[]> CreateFileMap(params string[] paths)
|
||||
{
|
||||
var map = new Dictionary<string, byte[]>(StringComparer.Ordinal);
|
||||
foreach (var path in paths)
|
||||
{
|
||||
map[path] = Encoding.UTF8.GetBytes($"btf:{path}");
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user