mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
refactor(process): share paired prlimit ceiling ownership (#6352)
* refactor(process): share paired prlimit ceiling ownership Route firmware, Wakaru, and pcap paired ceilings through LinuxResourceLimits without changing their byte or CPU budgets. Keep EVMole's soft-only limits as a dated architecture-guard exception. Co-authored-by: morluto <morluto@users.noreply.github.com> * test(pcap): admit capture limits without host prlimit The ceiling test only checks the shared argument prefix, so it uses a temporary executable instead of /usr/bin/prlimit. Co-authored-by: morluto <morluto@users.noreply.github.com> * style(pcap): format the capture ceiling test oxfmt rejected the object literal wrapping in PcapNativeBound.test.ts. Co-authored-by: morluto <morluto@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: morluto <morluto@users.noreply.github.com>
This commit is contained in:
12 files changed
+222
-46
No files matched your search
@@ -89,23 +89,49 @@ check(
|
||||
["src/domain/javascript/javascriptSemanticTraversal.ts"],
|
||||
);
|
||||
|
||||
// A producer that pins both the soft and hard ceiling of every resource must take
|
||||
// the vector from one owner. Audited 2026-10-13 against the other prlimit lanes:
|
||||
// src/pcap/PcapNativeBound.ts pairs soft and hard but budgets 300 CPU seconds and
|
||||
// has no resident-input ceiling; src/evm/EvmoleInterfaceProvider.ts,
|
||||
// src/firmware/FirmwareCommand.ts and src/javascript/recovery/WakaruCommand.ts
|
||||
// spell soft-only forms (`--core=0:`, `--core=0`). Those four stay as they are;
|
||||
// a new paired producer calls prlimitArgumentVector instead of spelling options.
|
||||
// A producer that pins both the soft and hard ceiling of every resource takes
|
||||
// the vector from one owner. prlimitCeilingArguments is the command-less prefix
|
||||
// of prlimitArgumentVector; both live in src/process/LinuxResourceLimits.ts.
|
||||
// Re-audited 2026-10-22 with util-linux 2.39.3: a bare value sets both soft and
|
||||
// hard, the same as soft:hard when the numbers are equal. `soft:` leaves the
|
||||
// inherited hard ceiling unchanged.
|
||||
// FirmwareCommand and WakaruCommand call prlimitArgumentVector and keep their
|
||||
// own file-size and CPU budgets (120 CPU seconds; firmware file size is the
|
||||
// call output budget or the 8 MiB report allowance; Wakaru file size is the
|
||||
// 128 MiB output allowance). Their 1 GiB address space is
|
||||
// BOUNDED_PRODUCER_LIMITS.address_space_bytes. PcapNativeBound calls
|
||||
// prlimitCeilingArguments because PcapCommand appends the producer command.
|
||||
// Its CPU ceiling stays 300 seconds and it has no resident-input budget;
|
||||
// address space and file size use those shared fields.
|
||||
// selectEvmWorkerLimits stays a separate selector: Result failures, unit
|
||||
// checks, and a refusal when soft exceeds hard. It must not call
|
||||
// clampLinuxResourceLimits.
|
||||
check(
|
||||
"inline paired prlimit ceilings (use prlimitArgumentVector from src/process/LinuxResourceLimits)",
|
||||
files,
|
||||
/--core=0:0/,
|
||||
["src/process/LinuxResourceLimits.ts", "src/pcap/PcapNativeBound.ts"],
|
||||
["src/process/LinuxResourceLimits.ts"],
|
||||
);
|
||||
|
||||
// Census is spelling-shaped, like the rules above: a lane that expresses the same
|
||||
// ceilings differently (`1024 ** 3`, camelCase fields) is not caught here, which is
|
||||
// why the four lanes above are listed rather than left to a future reader.
|
||||
check(
|
||||
"bare prlimit value (a bare value sets both ceilings; use prlimitArgumentVector)",
|
||||
files,
|
||||
/--core=0(?!:)/,
|
||||
);
|
||||
|
||||
// 2026-10-22: soft-only `resource:` options retain the inherited hard ceiling.
|
||||
// EVMole budgets differ (3 GiB address space, 16 MiB file, 30 CPU seconds), so
|
||||
// this lane must not call prlimitArgumentVector.
|
||||
check(
|
||||
"soft-only prlimit ceilings (paired producers use prlimitArgumentVector)",
|
||||
files,
|
||||
/--core=0:(?!0)/,
|
||||
["src/evm/EvmoleInterfaceProvider.ts"],
|
||||
);
|
||||
|
||||
// Resident input is the input_bytes field of BOUNDED_PRODUCER_LIMITS. CamelCase
|
||||
// inputBytes values with a different policy (an overridable pwntools admission,
|
||||
// historical capture intake) are not this budget.
|
||||
check(
|
||||
"inline resident-input budget (use BOUNDED_PRODUCER_LIMITS from src/process/LinuxResourceLimits)",
|
||||
files,
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
firmwareProcessLimitRecovery,
|
||||
firmwareSignalLimitError,
|
||||
} from "./FirmwareFailures.js";
|
||||
import { prlimitArgumentVector } from "../process/LinuxResourceLimits.js";
|
||||
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
|
||||
import {
|
||||
spawnOwnedProviderProcess,
|
||||
ProviderProcessSupervisor,
|
||||
@@ -30,7 +32,6 @@ import {
|
||||
type ProviderProcessSnapshot,
|
||||
type SpawnedOwnedProviderProcess,
|
||||
} from "../process/ProviderProcess.js";
|
||||
import { cleanupOwnedProcessGroup } from "../process/ProcessOwnership.js";
|
||||
import { FIRMWARE_LIMITS } from "./FirmwareRelease.js";
|
||||
import {
|
||||
FirmwareFileAccess,
|
||||
@@ -159,12 +160,15 @@ export const runFirmwareCommand = async (context: {
|
||||
const spawned = await (context.launcher ?? spawnOwnedProviderProcess)({
|
||||
command: context.limiter,
|
||||
arguments: [
|
||||
`--as=${FIRMWARE_LIMITS.addressSpaceBytes}`,
|
||||
`--fsize=${context.outputBudget?.bytes ?? FIRMWARE_LIMITS.reportBytes}`,
|
||||
"--core=0",
|
||||
`--cpu=${FIRMWARE_LIMITS.cpuSeconds}`,
|
||||
"--",
|
||||
context.command,
|
||||
...prlimitArgumentVector(
|
||||
{
|
||||
address_space_bytes: FIRMWARE_LIMITS.addressSpaceBytes,
|
||||
file_bytes:
|
||||
context.outputBudget?.bytes ?? FIRMWARE_LIMITS.reportBytes,
|
||||
cpu_seconds: FIRMWARE_LIMITS.cpuSeconds,
|
||||
},
|
||||
context.command,
|
||||
),
|
||||
...context.args,
|
||||
],
|
||||
expectedCommand: null,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BOUNDED_PRODUCER_LIMITS } from "../process/LinuxResourceLimits.js";
|
||||
|
||||
/** Audited upstream sources; installed tool bytes are fingerprinted separately. */
|
||||
export const FIRMWARE_RELEASES = {
|
||||
binwalk: {
|
||||
@@ -30,6 +32,6 @@ export const FIRMWARE_LIMITS = {
|
||||
inputBytes: 128 * 1024 * 1024,
|
||||
reportBytes: 8 * 1024 * 1024,
|
||||
diagnosticBytes: 8 * 1024 * 1024,
|
||||
addressSpaceBytes: 1024 * 1024 * 1024,
|
||||
addressSpaceBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
cpuSeconds: 120,
|
||||
} as const;
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
runWakaruCommand,
|
||||
type WakaruLauncher,
|
||||
} from "./WakaruCommand.js";
|
||||
import { prlimitArgumentVector } from "../../process/LinuxResourceLimits.js";
|
||||
import { RECOVERY_LIMITS } from "./WakaruRelease.js";
|
||||
|
||||
it("names the Wakaru configuration requirement when it is not configured", async () => {
|
||||
@@ -182,6 +183,52 @@ it("enforces the shared deadline while exited output streams remain open", async
|
||||
}
|
||||
});
|
||||
|
||||
it("pins both Wakaru ceilings through the shared prlimit vector", async () => {
|
||||
let observed: readonly string[] | undefined;
|
||||
const producer = createProducer();
|
||||
const command = "/selected/wakaru";
|
||||
await expect(
|
||||
runWakaruCommand({
|
||||
command,
|
||||
limiter: "/usr/bin/prlimit",
|
||||
arguments: ["--version"],
|
||||
cwd: "/tmp/unused",
|
||||
environment: {},
|
||||
deadline: performance.now() + 5_000,
|
||||
launcher: async (options) => {
|
||||
observed = options.arguments;
|
||||
setImmediate(() => {
|
||||
producer.stdout.end();
|
||||
producer.stderr.end();
|
||||
producer.emit("exit", 0, null);
|
||||
producer.emit("close", 0, null);
|
||||
});
|
||||
return {
|
||||
process: producer,
|
||||
ownership: {
|
||||
runId: options.runId,
|
||||
leaderPid: 12345,
|
||||
processGroupId: 12345,
|
||||
expectedParentPid: process.pid,
|
||||
},
|
||||
cleanup: async () => ({ cleaned: true, signaled: false }),
|
||||
};
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject({ exit_code: 0 });
|
||||
expect(observed).toEqual([
|
||||
...prlimitArgumentVector(
|
||||
{
|
||||
address_space_bytes: RECOVERY_LIMITS.addressSpaceBytes,
|
||||
file_bytes: RECOVERY_LIMITS.outputBytes,
|
||||
cpu_seconds: RECOVERY_LIMITS.cpuSeconds,
|
||||
},
|
||||
command,
|
||||
),
|
||||
"--version",
|
||||
]);
|
||||
});
|
||||
|
||||
it("honors cancellation while waiting for output after exit", async () => {
|
||||
const controller = new AbortController();
|
||||
const { context, producer } = commandFixture((process) => {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../../domain/providerCleanupError.js";
|
||||
import { prlimitArgumentVector } from "../../process/LinuxResourceLimits.js";
|
||||
import { cleanupOwnedProcessGroup } from "../../process/ProcessOwnership.js";
|
||||
import {
|
||||
ProviderProcessSupervisor,
|
||||
@@ -118,12 +119,14 @@ export const runWakaruCommand = async (context: WakaruCommandContext) => {
|
||||
const spawned = await (context.launcher ?? spawnOwnedProviderProcess)({
|
||||
command: context.limiter,
|
||||
arguments: [
|
||||
`--as=${String(RECOVERY_LIMITS.addressSpaceBytes)}`,
|
||||
`--fsize=${String(RECOVERY_LIMITS.outputBytes)}`,
|
||||
"--core=0",
|
||||
"--cpu=120",
|
||||
"--",
|
||||
context.command,
|
||||
...prlimitArgumentVector(
|
||||
{
|
||||
address_space_bytes: RECOVERY_LIMITS.addressSpaceBytes,
|
||||
file_bytes: RECOVERY_LIMITS.outputBytes,
|
||||
cpu_seconds: RECOVERY_LIMITS.cpuSeconds,
|
||||
},
|
||||
context.command,
|
||||
),
|
||||
...context.arguments,
|
||||
],
|
||||
expectedCommand: null,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BOUNDED_PRODUCER_LIMITS } from "../../process/LinuxResourceLimits.js";
|
||||
|
||||
/** Unchanged upstream source pin; executed bytes are identified separately. */
|
||||
export const WAKARU_RELEASE = {
|
||||
version: "1.14.0",
|
||||
@@ -34,7 +36,8 @@ export const RECOVERY_LIMITS = {
|
||||
outputBytes: 128 * 1024 * 1024,
|
||||
outputEntries: 10000,
|
||||
reportBytes: 8 * 1024 * 1024,
|
||||
addressSpaceBytes: 1024 * 1024 * 1024,
|
||||
addressSpaceBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
cpuSeconds: 120,
|
||||
timeoutMs: 120000,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { chmod, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import type { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
|
||||
import {
|
||||
BOUNDED_PRODUCER_LIMITS,
|
||||
prlimitCeilingArguments,
|
||||
type LinuxResourceLimits,
|
||||
} from "../process/LinuxResourceLimits.js";
|
||||
import { preparePcapNativeBound } from "./PcapNativeBound.js";
|
||||
|
||||
const resources = {
|
||||
readLimits: (limits: LinuxResourceLimits) => Promise.resolve(limits),
|
||||
} as ArtifactAnalysisResources;
|
||||
|
||||
const withPlatform = async (
|
||||
platform: NodeJS.Platform,
|
||||
run: () => Promise<void>,
|
||||
) => {
|
||||
const previousPlatform = process.platform;
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
value: platform,
|
||||
});
|
||||
try {
|
||||
await run();
|
||||
} finally {
|
||||
Object.defineProperty(process, "platform", {
|
||||
configurable: true,
|
||||
value: previousPlatform,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
it("uses the shared 1 GiB ceiling for the Windows job memory limit", async () => {
|
||||
await withPlatform("win32", async () => {
|
||||
await expect(preparePcapNativeBound({}, resources)).resolves.toMatchObject({
|
||||
nativeBound: {
|
||||
memoryLimitBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("pins capture ceilings through the shared prlimit prefix and keeps 300 CPU seconds", async () => {
|
||||
const root = await createTestTempDirectory("rea-pcap-prlimit-");
|
||||
const limiter = join(root, "prlimit");
|
||||
await writeFile(limiter, "#!/bin/sh\n");
|
||||
await chmod(limiter, 0o700);
|
||||
const limits = {
|
||||
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
|
||||
cpu_seconds: 300,
|
||||
};
|
||||
await withPlatform("linux", async () => {
|
||||
const prepared = await preparePcapNativeBound(
|
||||
{ REA_TSHARK_PRLIMIT_COMMAND: limiter },
|
||||
resources,
|
||||
);
|
||||
expect(prepared.nativeBound.limiter?.arguments).toEqual(
|
||||
prlimitCeilingArguments(limits),
|
||||
);
|
||||
expect(prepared.limitations[0]).toContain(String(limits.cpu_seconds));
|
||||
});
|
||||
});
|
||||
+13
-11
@@ -4,9 +4,15 @@ import { isAbsolute } from "node:path";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
|
||||
import type { ArtifactAnalysisResources } from "../process/ArtifactAnalysisResources.js";
|
||||
import {
|
||||
BOUNDED_PRODUCER_LIMITS,
|
||||
prlimitCeilingArguments,
|
||||
} from "../process/LinuxResourceLimits.js";
|
||||
import type { runPcapCommand } from "./PcapCommand.js";
|
||||
|
||||
const PRLIMIT_COMMAND_ENV = "REA_TSHARK_PRLIMIT_COMMAND";
|
||||
/** Capture CPU ceiling, in seconds. */
|
||||
const PCAP_CPU_SECONDS = 300;
|
||||
|
||||
/** Own native capture admission and the exact enforced resource profile. */
|
||||
export const preparePcapNativeBound = async (
|
||||
@@ -18,7 +24,9 @@ export const preparePcapNativeBound = async (
|
||||
}> => {
|
||||
if (process.platform === "win32") {
|
||||
return {
|
||||
nativeBound: { memoryLimitBytes: 1024 ** 3 },
|
||||
nativeBound: {
|
||||
memoryLimitBytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
},
|
||||
limitations: [
|
||||
"Each owned Windows tool tree has a 1 GiB Job Object committed-memory limit; this is not a machine-wide quota.",
|
||||
],
|
||||
@@ -41,20 +49,14 @@ export const preparePcapNativeBound = async (
|
||||
const limiter = await realpath(configured);
|
||||
await access(limiter, constants.X_OK);
|
||||
const limits = await resources.readLimits({
|
||||
address_space_bytes: 1024 ** 3,
|
||||
file_bytes: 64 * 1024 ** 2,
|
||||
cpu_seconds: 300,
|
||||
address_space_bytes: BOUNDED_PRODUCER_LIMITS.address_space_bytes,
|
||||
file_bytes: BOUNDED_PRODUCER_LIMITS.file_bytes,
|
||||
cpu_seconds: PCAP_CPU_SECONDS,
|
||||
});
|
||||
const nativeBound = {
|
||||
limiter: {
|
||||
command: limiter,
|
||||
arguments: [
|
||||
`--as=${limits.address_space_bytes}:${limits.address_space_bytes}`,
|
||||
`--fsize=${limits.file_bytes}:${limits.file_bytes}`,
|
||||
`--cpu=${limits.cpu_seconds}:${limits.cpu_seconds}`,
|
||||
"--core=0:0",
|
||||
"--",
|
||||
],
|
||||
arguments: prlimitCeilingArguments(limits),
|
||||
},
|
||||
};
|
||||
const limitations = [
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
BOUNDED_PRODUCER_LIMITS,
|
||||
clampLinuxResourceLimits,
|
||||
prlimitArgumentVector,
|
||||
prlimitCeilingArguments,
|
||||
readLinuxResourceLimits,
|
||||
} from "./LinuxResourceLimits.js";
|
||||
|
||||
@@ -87,9 +88,11 @@ it("pins the shared producer budget in one place", () => {
|
||||
});
|
||||
});
|
||||
it("pairs each effective ceiling with itself before the producer argv", () => {
|
||||
expect(
|
||||
prlimitArgumentVector(BOUNDED_PRODUCER_LIMITS, "/bin/producer"),
|
||||
).toEqual([
|
||||
const vector = prlimitArgumentVector(
|
||||
BOUNDED_PRODUCER_LIMITS,
|
||||
"/bin/producer",
|
||||
);
|
||||
expect(vector).toEqual([
|
||||
"--as=1073741824:1073741824",
|
||||
"--fsize=67108864:67108864",
|
||||
"--cpu=30:30",
|
||||
@@ -97,6 +100,9 @@ it("pairs each effective ceiling with itself before the producer argv", () => {
|
||||
"--",
|
||||
"/bin/producer",
|
||||
]);
|
||||
expect(prlimitCeilingArguments(BOUNDED_PRODUCER_LIMITS)).toEqual(
|
||||
vector.slice(0, -1),
|
||||
);
|
||||
expect(
|
||||
prlimitArgumentVector(
|
||||
clampLinuxResourceLimits(BOUNDED_PRODUCER_LIMITS, report("10 20")),
|
||||
|
||||
@@ -24,19 +24,26 @@ export const BOUNDED_PRODUCER_LIMITS = {
|
||||
cpu_seconds: 30,
|
||||
} as const;
|
||||
|
||||
/** Pin soft and hard ceilings together, then hand the producer argv to the limiter. */
|
||||
export const prlimitArgumentVector = (
|
||||
/**
|
||||
* Pin soft and hard ceilings together, ending at the prlimit option terminator.
|
||||
* Callers that append the producer command themselves use this prefix.
|
||||
*/
|
||||
export const prlimitCeilingArguments = (
|
||||
limits: LinuxResourceLimits,
|
||||
command: string,
|
||||
): string[] => [
|
||||
`--as=${limits.address_space_bytes}:${limits.address_space_bytes}`,
|
||||
`--fsize=${limits.file_bytes}:${limits.file_bytes}`,
|
||||
`--cpu=${limits.cpu_seconds}:${limits.cpu_seconds}`,
|
||||
"--core=0:0",
|
||||
"--",
|
||||
command,
|
||||
];
|
||||
|
||||
/** Pin soft and hard ceilings together, then hand the producer argv to the limiter. */
|
||||
export const prlimitArgumentVector = (
|
||||
limits: LinuxResourceLimits,
|
||||
command: string,
|
||||
): string[] => [...prlimitCeilingArguments(limits), command];
|
||||
|
||||
/** Never raise either inherited soft policy or hard ceiling. */
|
||||
export const clampLinuxResourceLimits = (
|
||||
configured: LinuxResourceLimits,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
} from "../../fixtures/firmware/provider.js";
|
||||
import { firmwareResultSchemas } from "../../../src/domain/firmware/firmwareAnalysis.js";
|
||||
import { FirmwareAnalysisService } from "../../../src/application/firmware/FirmwareAnalysisService.js";
|
||||
import { FIRMWARE_LIMITS } from "../../../src/firmware/FirmwareRelease.js";
|
||||
import { FirmwareProvider } from "../../../src/firmware/FirmwareProvider.js";
|
||||
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
|
||||
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
||||
@@ -109,7 +110,11 @@ it("binds region observations to exact bytes and preserves provider-reported unc
|
||||
report: [{ Analysis: { file_map: [{ id: "hit" }] } }],
|
||||
});
|
||||
expect(fixture.launches[1]?.args).toEqual(
|
||||
expect.arrayContaining(["--as=1073741824", "--threads", "1"]),
|
||||
expect.arrayContaining([
|
||||
`--as=${String(FIRMWARE_LIMITS.addressSpaceBytes)}:${String(FIRMWARE_LIMITS.addressSpaceBytes)}`,
|
||||
"--threads",
|
||||
"1",
|
||||
]),
|
||||
);
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { projectAnalysisError } from "../../src/domain/analysisErrorProjection.j
|
||||
import { ProviderProcessSupervisor } from "../../src/process/ProviderProcess.js";
|
||||
import { finishAnalysisCommand } from "../../src/cli/finishAnalysisCommand.js";
|
||||
import { AnalysisOutputError } from "../../src/domain/analysisErrorCore.js";
|
||||
import { FIRMWARE_LIMITS } from "../../src/firmware/FirmwareRelease.js";
|
||||
import {
|
||||
firmwareFixture,
|
||||
assertFirmwareCleanup,
|
||||
@@ -279,7 +280,11 @@ test("uses the selected phase deadline inside an owned command beyond the former
|
||||
});
|
||||
expect(fixture.launches).toHaveLength(2);
|
||||
expect(
|
||||
fixture.launches.every((launch) => launch.args.includes("--cpu=120")),
|
||||
fixture.launches.every((launch) =>
|
||||
launch.args.includes(
|
||||
`--cpu=${String(FIRMWARE_LIMITS.cpuSeconds)}:${String(FIRMWARE_LIMITS.cpuSeconds)}`,
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
await fixture.close();
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
|
||||
Reference in new issue
Block a user