[GH-ISSUE #6671] [Bug] Missing sigrok channel targets return successful decode Evidence #4050

Closed
opened 2026-10-31 00:46:21 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @coygeek on GitHub (Oct 23, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/6671

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

decode-signal-recording returns successful decode Evidence for --decoders uart:rx:baudrate=9600 when the producer exits zero with cli: Channel name for "rx" missing. The rejected channel target is retained only as a diagnostic alongside annotation text. A control using uart:rx=NONEXISTENT:baudrate=9600 and the corresponding nonexistent-name diagnostic returns invalid_request for decoders, preserving the same partial stdout.

This was executed through the production CLI registration, application service, SigrokRecordingProvider, owned child-process collection, identity checks and cleanup, with a command-boundary simulator. No real sigrok binary or physical signal capture was used. The real producer diagnostic and its selector reachability are supported separately by the released upstream source below.

Reproduction

In a source checkout of the revision below with its locked dependencies installed, save the following as a disposable .mts file outside the checkout, then run it from the checkout root with the pinned Node runtime:

node node_modules/tsx/dist/cli.mjs /tmp/signals-channel-probe.mts

The fixture creates and removes its own synthetic input and private workspaces. Its launcher replaces sigrok/prlimit with an owned Node child, following the repository's existing boundary-test pattern; it does not exercise real decoding or kernel resource enforcement. It restores the test driver's exit status after the deliberately rejected control so that all assertions can finish.

import assert from "node:assert/strict";
import { mkdtemp, writeFile, chmod, rm, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { createRequire } from "node:module";
const checkout = process.cwd();
const load = (path: string) => import(pathToFileURL(join(checkout, path)).href);
const require = createRequire(join(checkout, "package.json"));
const { Cli } = await import(pathToFileURL(require.resolve("incur")).href);
const { SigrokRecordingProvider } = await load("src/signals/SigrokRecordingProvider.ts");
const { SignalRecordingService } = await load("src/application/signals/SignalRecordingService.ts");
const { PrivateRuntimeRoot } = await load("src/process/PrivateRuntimeRoot.ts");
const { spawnOwnedProviderProcess } = await load("src/process/ProviderProcess.ts");
const { readStableArtifact } = await load("src/artifacts/readStableArtifact.ts");
const { registerSignalCommands } = await load("src/cli/signalCommands.ts");
const { silentLogger } = await load("src/logger.ts");
const root = await mkdtemp(join(tmpdir(), "rea-channel-probe-"));
const script = join(root, "producer.mjs");
const recording = join(root, "recording.vcd");
const annotation = "1000-1105 uart-1: Start bit\n";
await writeFile(recording, "synthetic command-boundary input\n");
await writeFile(script, `
if (process.argv.includes("--version")) console.log("sigrok-cli 0.7.2\\nlibsigrokdecode 0.5.3");
else {
  process.stdout.write("1000-1105 uart-1: Start bit\\n");
  if (process.argv.some(a => a.includes(":rx:"))) console.error('cli: Channel name for "rx" missing.');
  if (process.argv.some(a => a.includes(":rx=NONEXISTENT:"))) console.error('cli: No channel with name "NONEXISTENT" found.');
}
`);
await chmod(script, 0o700);
try {
  for (const [label, decoders] of [
    ["missing-target", "uart:rx:baudrate=9600"],
    ["unknown-name-control", "uart:rx=NONEXISTENT:baudrate=9600"],
    ["valid-mapping-control", "uart:rx=RX:baudrate=9600"],
  ]) {
    const calls: string[][] = [];
    const provider = new SigrokRecordingProvider(
      { ...process.env, REA_SIGROK_COMMAND: script, REA_SIGROK_PRLIMIT_COMMAND: script },
      async (input: any) => {
        calls.push([...input.arguments]);
        return spawnOwnedProviderProcess({ ...input, command: process.execPath,
          arguments: [script, ...input.arguments], expectedCommand: process.execPath });
      },
      () => PrivateRuntimeRoot.create({ parent: root, prefix: "decoder-" }),
      "linux", readStableArtifact, async (limits: any) => limits,
    );
    try {
      const previousExitCode = process.exitCode;
      const cli = Cli.create("rea-channel-probe", { sync: false });
      registerSignalCommands(cli, silentLogger, {}, new SignalRecordingService(provider));
      let stdout = "";
      await cli.serve(["decode-signal-recording", recording, "--decoders", decoders,
        "--format", "json"], { stdout: (text: string) => { stdout += text; }, exit: () => {} });
      process.exitCode = previousExitCode;
      const response = JSON.parse(stdout);
      assert.equal(calls.length, 2);
      const report = response.normalized_result;
      const captured = response.details?.captured_output;
      if (label === "unknown-name-control") {
        assert.equal(response.code, "invalid_request");
        assert.equal(captured.stdout, annotation);
      } else {
        assert.equal(report.annotations_text, annotation);
        assert.equal(report.selection.decoders, decoders);
        assert.equal(report.output_window.total_lines, 1);
      }
      console.log(JSON.stringify({ label, code: response.code ?? "success", decoders,
        annotations_text: report?.annotations_text ?? captured?.stdout,
        diagnostics: report?.diagnostics ?? captured?.stderr,
        ...(captured ? { exit_code: captured.exit_code } : {}) }));
    } finally { await provider.close(); }
  }
  assert.equal((await readdir(root)).filter(name => name.startsWith("decoder-")).length, 0);
  console.log("private decoder workspaces remaining: 0");
} finally { await rm(root, { recursive: true, force: true }); }

The final fixture run exited 0 and produced the exact output below. The missing-target case also succeeded incorrectly in the preceding run; that run's overall exit status was 1 because the invalid-input control set the test driver's exit code, before the fixture restored that control side effect.

Expected behavior

A channel-target refusal tied to the caller's decoders selector should return invalid_request identifying decoders, with the bounded producer stdout/stderr retained as captured output. A valid mapping should continue to succeed, and unrelated warnings should remain diagnostic-only.

The documented selection contract says channel names must match, REA does not guess wiring, and a missing selected channel returns invalid_request even when sigrok emits fallback annotations and exits successfully. Omitting the target after explicitly selecting rx reaches another producer diagnostic for this same failed wiring selection.

Impact and scope

Observed in 2/2 command-boundary runs of the missing-target case. A malformed selected wiring can be reported as a completed analysis rather than a request failure, allowing the caller to mistake fallback annotations for the requested decode. The diagnostic remains visible; there was no observed loss of source data. Supplying an explicit existing target, such as rx=RX, avoids this case. Real-provider annotation contents, decoder-specific behavior beyond the source path, and MCP projection were not executed.

REA version

6.7.0; source revision 99a5ffae3bf3d73f11bca3d9b8893347964b10b9.

Runtime and platform

Node.js v24.22.1; Linux x64; locked tsx 4.23.15. The command-boundary simulator emits the admitted sigrok-cli 0.7.2/libsigrokdecode banner. Neither sigrok nor prlimit was executed.

Target and artifact context

Disposable synthetic recording bytes, used only to exercise the adapter's selection, result projection and cleanup. No hardware, network traffic, private recordings or other target programs were involved.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

  • Expected source: The documented selection contract linked above and the existing provider's conversion of zero-exit selector refusals to input errors.
  • Failure source: The executed CLI command-boundary fixture and exact output below.
  • Evidence provenance: observed for REA's handling of simulated producer output; static for the real producer's diagnostic reachability.
  • Local verification: reproduced through the production CLI with an injected producer launcher; real sigrok decoding was not run.
  • Reproduction completeness: complete for the adapter's handling of this zero-exit diagnostic; no real-provider reproduction is claimed.
{"label":"missing-target","code":"success","decoders":"uart:rx:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":"cli: Channel name for \"rx\" missing.\n"}
{"label":"unknown-name-control","code":"invalid_request","decoders":"uart:rx=NONEXISTENT:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":"cli: No channel with name \"NONEXISTENT\" found.\n","exit_code":0}
{"label":"valid-mapping-control","code":"success","decoders":"uart:rx=RX:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":""}
private decoder workspaces remaining: 0

Typed outcome or recovery expected

An invalid_request / invalid_input result with an issue at decoders, the producer diagnostic and partial stdout retained in captured_output, and guidance to supply a recording channel target. Restoration means the missing-target fixture receives that outcome while the valid-mapping control still succeeds and the existing nonexistent-name control still fails as invalid input.

Analysis or suspected cause

The channel-refusal recognizer gathers only values from assignments containing = and recognizes only No channel with name ... found. A bare rx contributes no selected value. The provider then permits a zero-exit result when this recognizer returns no rejection.

The released sigrok-cli 0.7.2 selector parser represents a bare rx with a null target. Its generic argument loop retains that key/value, and decoder channel extraction moves it into the channel map. The channel mapper prints the exact missing-target diagnostic and skips that mapping. The missing-target and nonexistent-name branches both continue without adding a channel index; REA already recognizes the latter branch. This source comparison establishes why the simulated diagnostic is reachable, rather than inventing a new producer message; it does not establish real decoded sample contents.

Additional context

The full supplied open-issue corpus and live open/closed searches covered sigrok, missing channels and fallback annotations. Closed #6631 reported a nonexistent target supplied with rx=NONEXISTENT; that case now returns invalid input, as the executed control shows. This remaining case omits the target entirely (rx), reaches a different producer diagnostic, and still succeeds. #5984 concerns version-probe failures before decoding. First affected release is NOT_ENOUGH_INFO. Prepared with AI assistance; no tracked source was changed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A zero-exit rejection of an explicitly selected decoder-channel target is projected as successful decode Evidence in the production CLI boundary, with diagnostics still visible; the scope is bounded to this malformed selector and real decoded sample contents were not observed.
  • Workaround status: available
  • Workaround basis: Supply an explicit target present in the recording, as documented; the rx=RX command-boundary control succeeds without the refusal diagnostic.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6671 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `decode-signal-recording` returns successful decode Evidence for `--decoders uart:rx:baudrate=9600` when the producer exits zero with `cli: Channel name for "rx" missing.` The rejected channel target is retained only as a diagnostic alongside annotation text. A control using `uart:rx=NONEXISTENT:baudrate=9600` and the corresponding nonexistent-name diagnostic returns `invalid_request` for `decoders`, preserving the same partial stdout. This was executed through the production CLI registration, application service, SigrokRecordingProvider, owned child-process collection, identity checks and cleanup, with a command-boundary simulator. No real sigrok binary or physical signal capture was used. The real producer diagnostic and its selector reachability are supported separately by the released upstream source below. ### Reproduction In a source checkout of the revision below with its locked dependencies installed, save the following as a disposable `.mts` file outside the checkout, then run it from the checkout root with the pinned Node runtime: ```sh node node_modules/tsx/dist/cli.mjs /tmp/signals-channel-probe.mts ``` The fixture creates and removes its own synthetic input and private workspaces. Its launcher replaces sigrok/prlimit with an owned Node child, following the repository's existing boundary-test pattern; it does not exercise real decoding or kernel resource enforcement. It restores the test driver's exit status after the deliberately rejected control so that all assertions can finish. ```ts import assert from "node:assert/strict"; import { mkdtemp, writeFile, chmod, rm, readdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { createRequire } from "node:module"; const checkout = process.cwd(); const load = (path: string) => import(pathToFileURL(join(checkout, path)).href); const require = createRequire(join(checkout, "package.json")); const { Cli } = await import(pathToFileURL(require.resolve("incur")).href); const { SigrokRecordingProvider } = await load("src/signals/SigrokRecordingProvider.ts"); const { SignalRecordingService } = await load("src/application/signals/SignalRecordingService.ts"); const { PrivateRuntimeRoot } = await load("src/process/PrivateRuntimeRoot.ts"); const { spawnOwnedProviderProcess } = await load("src/process/ProviderProcess.ts"); const { readStableArtifact } = await load("src/artifacts/readStableArtifact.ts"); const { registerSignalCommands } = await load("src/cli/signalCommands.ts"); const { silentLogger } = await load("src/logger.ts"); const root = await mkdtemp(join(tmpdir(), "rea-channel-probe-")); const script = join(root, "producer.mjs"); const recording = join(root, "recording.vcd"); const annotation = "1000-1105 uart-1: Start bit\n"; await writeFile(recording, "synthetic command-boundary input\n"); await writeFile(script, ` if (process.argv.includes("--version")) console.log("sigrok-cli 0.7.2\\nlibsigrokdecode 0.5.3"); else { process.stdout.write("1000-1105 uart-1: Start bit\\n"); if (process.argv.some(a => a.includes(":rx:"))) console.error('cli: Channel name for "rx" missing.'); if (process.argv.some(a => a.includes(":rx=NONEXISTENT:"))) console.error('cli: No channel with name "NONEXISTENT" found.'); } `); await chmod(script, 0o700); try { for (const [label, decoders] of [ ["missing-target", "uart:rx:baudrate=9600"], ["unknown-name-control", "uart:rx=NONEXISTENT:baudrate=9600"], ["valid-mapping-control", "uart:rx=RX:baudrate=9600"], ]) { const calls: string[][] = []; const provider = new SigrokRecordingProvider( { ...process.env, REA_SIGROK_COMMAND: script, REA_SIGROK_PRLIMIT_COMMAND: script }, async (input: any) => { calls.push([...input.arguments]); return spawnOwnedProviderProcess({ ...input, command: process.execPath, arguments: [script, ...input.arguments], expectedCommand: process.execPath }); }, () => PrivateRuntimeRoot.create({ parent: root, prefix: "decoder-" }), "linux", readStableArtifact, async (limits: any) => limits, ); try { const previousExitCode = process.exitCode; const cli = Cli.create("rea-channel-probe", { sync: false }); registerSignalCommands(cli, silentLogger, {}, new SignalRecordingService(provider)); let stdout = ""; await cli.serve(["decode-signal-recording", recording, "--decoders", decoders, "--format", "json"], { stdout: (text: string) => { stdout += text; }, exit: () => {} }); process.exitCode = previousExitCode; const response = JSON.parse(stdout); assert.equal(calls.length, 2); const report = response.normalized_result; const captured = response.details?.captured_output; if (label === "unknown-name-control") { assert.equal(response.code, "invalid_request"); assert.equal(captured.stdout, annotation); } else { assert.equal(report.annotations_text, annotation); assert.equal(report.selection.decoders, decoders); assert.equal(report.output_window.total_lines, 1); } console.log(JSON.stringify({ label, code: response.code ?? "success", decoders, annotations_text: report?.annotations_text ?? captured?.stdout, diagnostics: report?.diagnostics ?? captured?.stderr, ...(captured ? { exit_code: captured.exit_code } : {}) })); } finally { await provider.close(); } } assert.equal((await readdir(root)).filter(name => name.startsWith("decoder-")).length, 0); console.log("private decoder workspaces remaining: 0"); } finally { await rm(root, { recursive: true, force: true }); } ``` The final fixture run exited 0 and produced the exact output below. The missing-target case also succeeded incorrectly in the preceding run; that run's overall exit status was 1 because the invalid-input control set the test driver's exit code, before the fixture restored that control side effect. ### Expected behavior A channel-target refusal tied to the caller's `decoders` selector should return `invalid_request` identifying `decoders`, with the bounded producer stdout/stderr retained as captured output. A valid mapping should continue to succeed, and unrelated warnings should remain diagnostic-only. The [documented selection contract](https://github.com/morluto/rea/blob/99a5ffae3bf3d73f11bca3d9b8893347964b10b9/docs/signal-recordings.md#L32-L37) says channel names must match, REA does not guess wiring, and a missing selected channel returns `invalid_request` even when sigrok emits fallback annotations and exits successfully. Omitting the target after explicitly selecting `rx` reaches another producer diagnostic for this same failed wiring selection. ### Impact and scope Observed in 2/2 command-boundary runs of the missing-target case. A malformed selected wiring can be reported as a completed analysis rather than a request failure, allowing the caller to mistake fallback annotations for the requested decode. The diagnostic remains visible; there was no observed loss of source data. Supplying an explicit existing target, such as `rx=RX`, avoids this case. Real-provider annotation contents, decoder-specific behavior beyond the source path, and MCP projection were not executed. ### REA version 6.7.0; source revision `99a5ffae3bf3d73f11bca3d9b8893347964b10b9`. ### Runtime and platform Node.js v24.22.1; Linux x64; locked tsx 4.23.15. The command-boundary simulator emits the admitted sigrok-cli 0.7.2/libsigrokdecode banner. Neither sigrok nor prlimit was executed. ### Target and artifact context Disposable synthetic recording bytes, used only to exercise the adapter's selection, result projection and cleanup. No hardware, network traffic, private recordings or other target programs were involved. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence - Expected source: The documented selection contract linked above and the existing provider's conversion of zero-exit selector refusals to input errors. - Failure source: The executed CLI command-boundary fixture and exact output below. - Evidence provenance: observed for REA's handling of simulated producer output; static for the real producer's diagnostic reachability. - Local verification: reproduced through the production CLI with an injected producer launcher; real sigrok decoding was not run. - Reproduction completeness: complete for the adapter's handling of this zero-exit diagnostic; no real-provider reproduction is claimed. ```text {"label":"missing-target","code":"success","decoders":"uart:rx:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":"cli: Channel name for \"rx\" missing.\n"} {"label":"unknown-name-control","code":"invalid_request","decoders":"uart:rx=NONEXISTENT:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":"cli: No channel with name \"NONEXISTENT\" found.\n","exit_code":0} {"label":"valid-mapping-control","code":"success","decoders":"uart:rx=RX:baudrate=9600","annotations_text":"1000-1105 uart-1: Start bit\n","diagnostics":""} private decoder workspaces remaining: 0 ``` ### Typed outcome or recovery expected An `invalid_request` / `invalid_input` result with an issue at `decoders`, the producer diagnostic and partial stdout retained in `captured_output`, and guidance to supply a recording channel target. Restoration means the missing-target fixture receives that outcome while the valid-mapping control still succeeds and the existing nonexistent-name control still fails as invalid input. ### Analysis or suspected cause The [channel-refusal recognizer](https://github.com/morluto/rea/blob/99a5ffae3bf3d73f11bca3d9b8893347964b10b9/src/signals/sigrokSelectorRejection.ts#L58-L72) gathers only values from assignments containing `=` and recognizes only `No channel with name ... found.` A bare `rx` contributes no selected value. The [provider then permits a zero-exit result](https://github.com/morluto/rea/blob/99a5ffae3bf3d73f11bca3d9b8893347964b10b9/src/signals/SigrokRecordingProvider.ts#L245-L257) when this recognizer returns no rejection. The released [sigrok-cli 0.7.2 selector parser](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/parsers.c#L268-L289) represents a bare `rx` with a null target. Its [generic argument loop](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/parsers.c#L349-L356) retains that key/value, and [decoder channel extraction](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/decode.c#L99-L116) moves it into the channel map. The [channel mapper](https://github.com/sigrokproject/sigrok-cli/blob/sigrok-cli-0.7.2/decode.c#L254-L277) prints the exact missing-target diagnostic and skips that mapping. The missing-target and nonexistent-name branches both continue without adding a channel index; REA already recognizes the latter branch. This source comparison establishes why the simulated diagnostic is reachable, rather than inventing a new producer message; it does not establish real decoded sample contents. ### Additional context The full supplied open-issue corpus and live open/closed searches covered sigrok, missing channels and fallback annotations. Closed #6631 reported a nonexistent target supplied with `rx=NONEXISTENT`; that case now returns invalid input, as the executed control shows. This remaining case omits the target entirely (`rx`), reaches a different producer diagnostic, and still succeeds. #5984 concerns version-probe failures before decoding. First affected release is NOT_ENOUGH_INFO. Prepared with AI assistance; no tracked source was changed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A zero-exit rejection of an explicitly selected decoder-channel target is projected as successful decode Evidence in the production CLI boundary, with diagnostics still visible; the scope is bounded to this malformed selector and real decoded sample contents were not observed. - Workaround status: available - Workaround basis: Supply an explicit target present in the recording, as documented; the `rx=RX` command-boundary control succeeds without the refusal diagnostic.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#4050
No description provided.