[GH-ISSUE #5081] [Bug] Denied Linux process stat reads drop descendants without marking sampling partial #3118

Closed
opened 2026-10-31 00:36:51 +00:00 by bsakaguchi · 1 comment
Owner

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

Before submitting

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

Area

Process capture or runtime observation

Observed behavior

A mocked filesystem-boundary probe through production startProcessSampler shows that a denied stat read for an already discovered descendant removes that process and its subtree while returning sampling_partial: false, sampling_failures: 0 and first_sampling_failure: null. The aggregate truncation helper returns false when other coverage is complete. A denied command read instead retains the row with an empty command and the same failure-free coverage. Stat and command permission errors are the primary failure here; an additional task-enumeration control exercises the shared classifier.

This was executed against source with synthetic procfs data, matching root identity/run-token fixtures and Linux platform selection. No real Linux permission policy, PTY capture, CLI or MCP session was exercised.

Reproduction

In a checkout of the revision below with its locked dependencies installed, save the following as src/process/capture/procReadCoverage.test.ts. It mocks filesystem reads below the production error classifier; the sampler, identity checks, traversal and coverage logic are real. The synthetic tree is 100 → 200 → 300 and its sample limit is 20.

import { afterEach, expect, it, vi } from "vitest";
import { startProcessSampler } from "./ProcessSampling.js";
import { hasCaptureTruncation } from "../../domain/process/processCaptureCoverage.js";
import { emptyProcessCapture } from "../../domain/process/processCapture.fixture.js";

const fixture = vi.hoisted(() => ({ failure: "", reads: [] as string[] }));
vi.mock("../ProcessOwnership.js", () => ({ readProcessRunId: async () => "fixture-run" }));
vi.mock("node:fs/promises", () => ({
  readdir: async (path: string) => {
    fixture.reads.push(path);
    if (fixture.failure === "task-EACCES" && path === "/proc/200/task")
      throw Object.assign(new Error("synthetic denied"), { code: "EACCES" });
    return [path.split("/")[2]];
  },
  readFile: async (path: string) => {
    fixture.reads.push(path);
    const [kind, code] = fixture.failure.split("-");
    if (path === `/proc/200/${kind}`)
      throw Object.assign(new Error("synthetic read failure"), { code });
    const pid = Number(path.split("/")[2]);
    if (path.endsWith("/stat")) {
      const fields = Array(20).fill("0");
      fields[0] = "S";
      fields[1] = String(pid === 100 ? 1 : pid === 200 ? 100 : 200);
      fields[2] = fields[3] = "100";
      fields[19] = String(pid + 10);
      return `${pid} (fixture) ${fields.join(" ")}`;
    }
    if (path.endsWith("/cmdline")) return Buffer.from(`fixture-${pid}\0`);
    if (path.endsWith("/children")) return pid === 100 ? "200" : pid === 200 ? "300" : "";
    throw new Error(`unexpected path ${path}`);
  },
}));

afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); });
it.each(["none", "stat-EACCES", "stat-EPERM", "task-EACCES", "cmdline-EACCES", "stat-EIO"])("records coverage for %s", async (failure) => {
  fixture.failure = failure;
  fixture.reads = [];
  vi.spyOn(process, "platform", "get").mockReturnValue("linux");
  vi.useFakeTimers();
  const samples: any[] = [];
  const stop = startProcessSampler({ rootPid: 100, runId: "fixture-run", started: 0, limit: 20, samples });
  await vi.advanceTimersByTimeAsync(1);
  const result = await stop();
  const pids = samples.map(s => s.pid);
  const details = { ...emptyProcessCapture().truncation_details, process: result.coverage };
  console.log(JSON.stringify({ failure, pids, commands: samples.map(s => s.command), ...result.coverage, aggregateTruncated: hasCaptureTruncation(details) }));
  expect(fixture.reads).toContain("/proc/200/stat");
  expect(pids).toEqual(failure.startsWith("stat-EA") || failure === "stat-EPERM" ? [100] : failure === "task-EACCES" ? [100, 200] : failure === "stat-EIO" ? [] : [100, 200, 300]);
  expect(result.coverage.sampling_partial).toBe(failure === "stat-EIO");
  expect(result.coverage.sampling_failures).toBe(failure === "stat-EIO" ? 1 : 0);
  expect(hasCaptureTruncation(details)).toBe(failure === "stat-EIO");
  if (failure === "cmdline-EACCES") expect(samples[1].command).toBe("");
  if (failure === "stat-EACCES" || failure === "stat-EPERM" || failure === "task-EACCES") expect(fixture.reads).not.toContain("/proc/300/stat");
});

Use a scratch config to run only this probe, with one worker and a scratch cache:

export WORK=$(mktemp -d)
cat > "$WORK/vitest.config.mjs" <<'JS'
export default {
  root: process.cwd(),
  cacheDir: process.env.WORK + "/cache",
  test: {
    include: ["src/process/capture/procReadCoverage.test.ts"],
    pool: "forks", maxWorkers: 1, fileParallelism: false
  }
};
JS
node node_modules/vitest/vitest.mjs run --config "$WORK/vitest.config.mjs" --maxWorkers=1 --no-file-parallelism

All six cases passed in one focused run. The assertions describe the current behavior; passing them confirms the defect rather than a correction. Remove the added test and scratch directory afterward.

Expected behavior

Retain readable observations and expose denied-read uncertainty with a meaningful diagnostic and partial process coverage. An unreadable identity need not produce a trusted process row. The request does not require bypassing identity checks or event-complete sampling.

AGENTS.md:19 requires preserving omission, uncertainty, partial facts and meaningful failure reasons. docs/process-capture.md:420 describes process partial coverage and its aggregate truncation signal.

Impact and scope

A procfs permission failure loses process data or a command without retaining the reason or marking the failed observation partial. This hides a known read failure beyond the documented possibility of missing short-lived descendants. The mocked boundary reproduced this in each permission case; a generic EIO control correctly recorded a sampling failure and partial coverage.

NOT_ENOUGH_INFO: real Linux permission configurations that produce these errors for an in-scope descendant, frequency in live captures, downstream comparison effects, and a practical caller workaround were not established.

REA version

Source 3537e58c1ab29309f06eabf50bc52adba9817a0c, package manifest 6.7.0. Relevant sampler, coverage and capture-consumer source is identical to the published rea-agents-6.7.0 tag; that release runtime was not executed.

Runtime and platform

Node v26.12.0; macOS 27.0.1 arm64; locked Vitest v5.0.3. Linux platform, filesystem and run-token lookup were mocked. No native provider or real process target was used.

Target and artifact context

Synthetic procfs records for a stable three-process tree. Matching root group/session identity and a fixture run token satisfy the sampler initialization guards. No actual process is launched or signaled.

Interface

Other

Earliest failure stage

Capture or observation

Relevant output or evidence

Observed reduced output from the six-case mocked-boundary run:

condition          retained PIDs   sampling_partial   sampling_failures   aggregate truncation
no error           100,200,300     false              0                   false
child stat EACCES  100             false              0                   false
child stat EPERM   100             false              0                   false
child task EACCES  100,200         false              0                   false
child cmd EACCES   100,200,300     false              0                   false
child stat EIO     none            true               1                   true

All permission cases returned a null first failure; the command case returned an empty command for PID 200. Recorded mock read paths confirm PID 300 was never inspected when its ancestor or child enumeration was omitted.

The error collapse is at src/process/capture/ProcessSampling.ts:62; stat/command readers at lines 113-137 and task/children readers at lines 152-175 use it. src/process/capture/ProcessSampling.ts:220 drops unavailable stat rows, blanks unavailable commands and substitutes an empty child list. The BFS at lines 259-305 then omits the subtree. src/process/capture/ProcessSampling.ts:501 never records swallowed errors as partial coverage or sampling failures.

SOURCE-ONLY caller propagation: src/process/capture/ProcessHarness.ts:318 starts this sampler; lines 509-510 await its result; src/process/capture/ProcessHarness.ts:585 passes its coverage to capture construction. src/process/capture/ProcessCaptureLifecycle.ts:338 uses that coverage for truncation and the incomplete-sampling limitation. The probe directly executes the same src/domain/process/processCaptureCoverage.ts:99 aggregate helper with otherwise empty coverage. Final capture assembly itself was not executed.

Typed outcome or recovery expected

Expose a failed/unavailable process read and partial sampling coverage while preserving readable observations. sampling_failures, first_sampling_failure and sampling_partial currently report no such gap. Exact diagnostic representation is a maintainer design choice; no weakened identity or ownership checks are requested.

Analysis or suspected cause

EACCES/EPERM are grouped with ENOENT/ESRCH and converted to undefined before the sampler can retain their meaning. That value means a skipped row, empty command or empty child list depending on the read. The existing root-child enumeration fallback to ps at sampler lines 267-268 does not cover denied descendant reads. The generic sampled-process limitation remains present; this report concerns a known read failure being erased.

Additional context

Open and closed issues were searched. #180 concerns enumerating non-leader-thread children; #2543 concerns evidence retained after ownership cleanup failures. Neither covers permission-read accounting in this sampler. No earlier known-good version or real Linux permission reproduction was established.

Originally created by @coygeek on GitHub (Oct 18, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5081 ## 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 Process capture or runtime observation ## Observed behavior A mocked filesystem-boundary probe through production `startProcessSampler` shows that a denied stat read for an already discovered descendant removes that process and its subtree while returning `sampling_partial: false`, `sampling_failures: 0` and `first_sampling_failure: null`. The aggregate truncation helper returns false when other coverage is complete. A denied command read instead retains the row with an empty command and the same failure-free coverage. Stat and command permission errors are the primary failure here; an additional task-enumeration control exercises the shared classifier. This was executed against source with synthetic procfs data, matching root identity/run-token fixtures and Linux platform selection. No real Linux permission policy, PTY capture, CLI or MCP session was exercised. ## Reproduction In a checkout of the revision below with its locked dependencies installed, save the following as `src/process/capture/procReadCoverage.test.ts`. It mocks filesystem reads below the production error classifier; the sampler, identity checks, traversal and coverage logic are real. The synthetic tree is 100 → 200 → 300 and its sample limit is 20. ```ts import { afterEach, expect, it, vi } from "vitest"; import { startProcessSampler } from "./ProcessSampling.js"; import { hasCaptureTruncation } from "../../domain/process/processCaptureCoverage.js"; import { emptyProcessCapture } from "../../domain/process/processCapture.fixture.js"; const fixture = vi.hoisted(() => ({ failure: "", reads: [] as string[] })); vi.mock("../ProcessOwnership.js", () => ({ readProcessRunId: async () => "fixture-run" })); vi.mock("node:fs/promises", () => ({ readdir: async (path: string) => { fixture.reads.push(path); if (fixture.failure === "task-EACCES" && path === "/proc/200/task") throw Object.assign(new Error("synthetic denied"), { code: "EACCES" }); return [path.split("/")[2]]; }, readFile: async (path: string) => { fixture.reads.push(path); const [kind, code] = fixture.failure.split("-"); if (path === `/proc/200/${kind}`) throw Object.assign(new Error("synthetic read failure"), { code }); const pid = Number(path.split("/")[2]); if (path.endsWith("/stat")) { const fields = Array(20).fill("0"); fields[0] = "S"; fields[1] = String(pid === 100 ? 1 : pid === 200 ? 100 : 200); fields[2] = fields[3] = "100"; fields[19] = String(pid + 10); return `${pid} (fixture) ${fields.join(" ")}`; } if (path.endsWith("/cmdline")) return Buffer.from(`fixture-${pid}\0`); if (path.endsWith("/children")) return pid === 100 ? "200" : pid === 200 ? "300" : ""; throw new Error(`unexpected path ${path}`); }, })); afterEach(() => { vi.restoreAllMocks(); vi.useRealTimers(); }); it.each(["none", "stat-EACCES", "stat-EPERM", "task-EACCES", "cmdline-EACCES", "stat-EIO"])("records coverage for %s", async (failure) => { fixture.failure = failure; fixture.reads = []; vi.spyOn(process, "platform", "get").mockReturnValue("linux"); vi.useFakeTimers(); const samples: any[] = []; const stop = startProcessSampler({ rootPid: 100, runId: "fixture-run", started: 0, limit: 20, samples }); await vi.advanceTimersByTimeAsync(1); const result = await stop(); const pids = samples.map(s => s.pid); const details = { ...emptyProcessCapture().truncation_details, process: result.coverage }; console.log(JSON.stringify({ failure, pids, commands: samples.map(s => s.command), ...result.coverage, aggregateTruncated: hasCaptureTruncation(details) })); expect(fixture.reads).toContain("/proc/200/stat"); expect(pids).toEqual(failure.startsWith("stat-EA") || failure === "stat-EPERM" ? [100] : failure === "task-EACCES" ? [100, 200] : failure === "stat-EIO" ? [] : [100, 200, 300]); expect(result.coverage.sampling_partial).toBe(failure === "stat-EIO"); expect(result.coverage.sampling_failures).toBe(failure === "stat-EIO" ? 1 : 0); expect(hasCaptureTruncation(details)).toBe(failure === "stat-EIO"); if (failure === "cmdline-EACCES") expect(samples[1].command).toBe(""); if (failure === "stat-EACCES" || failure === "stat-EPERM" || failure === "task-EACCES") expect(fixture.reads).not.toContain("/proc/300/stat"); }); ``` Use a scratch config to run only this probe, with one worker and a scratch cache: ```sh export WORK=$(mktemp -d) cat > "$WORK/vitest.config.mjs" <<'JS' export default { root: process.cwd(), cacheDir: process.env.WORK + "/cache", test: { include: ["src/process/capture/procReadCoverage.test.ts"], pool: "forks", maxWorkers: 1, fileParallelism: false } }; JS node node_modules/vitest/vitest.mjs run --config "$WORK/vitest.config.mjs" --maxWorkers=1 --no-file-parallelism ``` All six cases passed in one focused run. The assertions describe the current behavior; passing them confirms the defect rather than a correction. Remove the added test and scratch directory afterward. ## Expected behavior Retain readable observations and expose denied-read uncertainty with a meaningful diagnostic and partial process coverage. An unreadable identity need not produce a trusted process row. The request does not require bypassing identity checks or event-complete sampling. [AGENTS.md:19](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/AGENTS.md#L19-L31) requires preserving omission, uncertainty, partial facts and meaningful failure reasons. [docs/process-capture.md:420](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/docs/process-capture.md#L420-L425) describes process partial coverage and its aggregate truncation signal. ## Impact and scope A procfs permission failure loses process data or a command without retaining the reason or marking the failed observation partial. This hides a known read failure beyond the documented possibility of missing short-lived descendants. The mocked boundary reproduced this in each permission case; a generic EIO control correctly recorded a sampling failure and partial coverage. NOT_ENOUGH_INFO: real Linux permission configurations that produce these errors for an in-scope descendant, frequency in live captures, downstream comparison effects, and a practical caller workaround were not established. ## REA version Source `3537e58c1ab29309f06eabf50bc52adba9817a0c`, package manifest 6.7.0. Relevant sampler, coverage and capture-consumer source is identical to the published `rea-agents-6.7.0` tag; that release runtime was not executed. ## Runtime and platform Node v26.12.0; macOS 27.0.1 arm64; locked Vitest v5.0.3. Linux platform, filesystem and run-token lookup were mocked. No native provider or real process target was used. ## Target and artifact context Synthetic procfs records for a stable three-process tree. Matching root group/session identity and a fixture run token satisfy the sampler initialization guards. No actual process is launched or signaled. ## Interface Other ## Earliest failure stage Capture or observation ## Relevant output or evidence Observed reduced output from the six-case mocked-boundary run: ```text condition retained PIDs sampling_partial sampling_failures aggregate truncation no error 100,200,300 false 0 false child stat EACCES 100 false 0 false child stat EPERM 100 false 0 false child task EACCES 100,200 false 0 false child cmd EACCES 100,200,300 false 0 false child stat EIO none true 1 true ``` All permission cases returned a null first failure; the command case returned an empty command for PID 200. Recorded mock read paths confirm PID 300 was never inspected when its ancestor or child enumeration was omitted. The error collapse is at [src/process/capture/ProcessSampling.ts:62](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessSampling.ts#L62-L73); stat/command readers at lines 113-137 and task/children readers at lines 152-175 use it. [src/process/capture/ProcessSampling.ts:220](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessSampling.ts#L220-L255) drops unavailable stat rows, blanks unavailable commands and substitutes an empty child list. The BFS at lines 259-305 then omits the subtree. [src/process/capture/ProcessSampling.ts:501](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessSampling.ts#L501-L556) never records swallowed errors as partial coverage or sampling failures. SOURCE-ONLY caller propagation: [src/process/capture/ProcessHarness.ts:318](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessHarness.ts#L318-L325) starts this sampler; lines 509-510 await its result; [src/process/capture/ProcessHarness.ts:585](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessHarness.ts#L585-L600) passes its coverage to capture construction. [src/process/capture/ProcessCaptureLifecycle.ts:338](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/process/capture/ProcessCaptureLifecycle.ts#L338-L350) uses that coverage for truncation and the incomplete-sampling limitation. The probe directly executes the same [src/domain/process/processCaptureCoverage.ts:99](https://github.com/morluto/rea/blob/3537e58c1ab29309f06eabf50bc52adba9817a0c/src/domain/process/processCaptureCoverage.ts#L99-L109) aggregate helper with otherwise empty coverage. Final capture assembly itself was not executed. ## Typed outcome or recovery expected Expose a failed/unavailable process read and partial sampling coverage while preserving readable observations. `sampling_failures`, `first_sampling_failure` and `sampling_partial` currently report no such gap. Exact diagnostic representation is a maintainer design choice; no weakened identity or ownership checks are requested. ## Analysis or suspected cause EACCES/EPERM are grouped with ENOENT/ESRCH and converted to `undefined` before the sampler can retain their meaning. That value means a skipped row, empty command or empty child list depending on the read. The existing root-child enumeration fallback to `ps` at sampler lines 267-268 does not cover denied descendant reads. The generic sampled-process limitation remains present; this report concerns a known read failure being erased. ## Additional context Open and closed issues were searched. [#180](https://github.com/morluto/rea/issues/180) concerns enumerating non-leader-thread children; [#2543](https://github.com/morluto/rea/issues/2543) concerns evidence retained after ownership cleanup failures. Neither covers permission-read accounting in this sampler. No earlier known-good version or real Linux permission reproduction was established.
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in ba866a26ecbe179a717e746cbc0d1605ae616ad5, including the related finalization gap #5194.

Procfs reads now distinguish expected disappearance from unavailable observations. Typed read outcomes compose successful rows/children with a count, first diagnostic and internal interruption fact, so denied stat/command/task reads and ordinary I/O failures cannot silently erase their meaning or readable siblings. Root token-read failures are retained. Linux fallback uses ps only to discover candidate edges, then checks identity/parentage and commands through procfs; a failed fallback preserves the stable root. Contradictory PID payloads fail attribution.

Shutdown awaits issued operations and retains completed identity brackets. The existing public sampling_failures counts failed read observations plus at most one interrupted pass; its first diagnostic preserves an actual read failure and appends interruption context when both occur. Partial coverage is derived once from the producer facts. No capture-schema field was added: the previous strict reader accepts a fresh CLI capture and an interrupted-coverage schema fixture. Coverage consumers and documentation were updated together; the superseded untyped readers and duplicate root preflight were removed.

Validation: nine selected regressions fail on baseline e8da7e1ed; 22 sampler regressions pass on the fix. The final selection passes 123 tests across 12 files, including real macOS CLI/MCP capture, cancellation, cleanup, process-only comparison uncertainty, and converted MCP schema checks. Full npm run check, npm run docs:check (5 consistency tests), and discovery (1,635 files exactly once) pass. Independent source review found no remaining concrete counterexample in this slice.

Six alternating completed CLI captures checked identical fixture/executable inputs, terminal markers, normal exit and verified owned cleanup. Two repeated-process medians per version: elapsed 1.949 → 2.069 s; process-family CPU 1.839 → 1.833 s; sampled family RSS 515,031,040 → 516,833,280 bytes; estimated full-reply cl100k tokens 2,063 → 2,064 (7,642 bytes in both). PIDs/timestamps vary. These small samples establish no speedup or full-agent token benefit; no true cold run was performed.

Linux permission behavior is verified through deterministic procfs boundary fixtures, not a real Linux permission policy; Docker was unavailable. Windows is unverified. Native readdir has no cancellation option, so shutdown awaits an issued read rather than abandoning it. Context7 and external DeepWiki were used for navigation and checked against the pinned Node filesystem API. Sampling remains sampled, not event-complete. The separate ownership race #4322 remains open.

<!-- gh-comment-id:6261842122 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on `main` in [ba866a26ecbe179a717e746cbc0d1605ae616ad5](https://github.com/morluto/rea/commit/ba866a26ecbe179a717e746cbc0d1605ae616ad5), including the related finalization gap #5194. Procfs reads now distinguish expected disappearance from unavailable observations. Typed read outcomes compose successful rows/children with a count, first diagnostic and internal interruption fact, so denied stat/command/task reads and ordinary I/O failures cannot silently erase their meaning or readable siblings. Root token-read failures are retained. Linux fallback uses `ps` only to discover candidate edges, then checks identity/parentage and commands through procfs; a failed fallback preserves the stable root. Contradictory PID payloads fail attribution. Shutdown awaits issued operations and retains completed identity brackets. The existing public `sampling_failures` counts failed read observations plus at most one interrupted pass; its first diagnostic preserves an actual read failure and appends interruption context when both occur. Partial coverage is derived once from the producer facts. No capture-schema field was added: the previous strict reader accepts a fresh CLI capture and an interrupted-coverage schema fixture. Coverage consumers and documentation were updated together; the superseded untyped readers and duplicate root preflight were removed. Validation: nine selected regressions fail on baseline `e8da7e1ed`; 22 sampler regressions pass on the fix. The final selection passes 123 tests across 12 files, including real macOS CLI/MCP capture, cancellation, cleanup, process-only comparison uncertainty, and converted MCP schema checks. Full `npm run check`, `npm run docs:check` (5 consistency tests), and discovery (1,635 files exactly once) pass. Independent source review found no remaining concrete counterexample in this slice. Six alternating completed CLI captures checked identical fixture/executable inputs, terminal markers, normal exit and verified owned cleanup. Two repeated-process medians per version: elapsed 1.949 → 2.069 s; process-family CPU 1.839 → 1.833 s; sampled family RSS 515,031,040 → 516,833,280 bytes; estimated full-reply cl100k tokens 2,063 → 2,064 (7,642 bytes in both). PIDs/timestamps vary. These small samples establish no speedup or full-agent token benefit; no true cold run was performed. Linux permission behavior is verified through deterministic procfs boundary fixtures, not a real Linux permission policy; Docker was unavailable. Windows is unverified. Native `readdir` has no cancellation option, so shutdown awaits an issued read rather than abandoning it. Context7 and external DeepWiki were used for navigation and checked against the [pinned Node filesystem API](https://nodejs.org/download/release/v24.18.0/docs/api/fs.html#fspromisesreaddirpath-options). Sampling remains sampled, not event-complete. The separate ownership race #4322 remains open.
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#3118
No description provided.