diff --git a/src/process/LinuxProcessStatus.test.ts b/src/process/LinuxProcessStatus.test.ts index 46f7688447..5854e68225 100644 --- a/src/process/LinuxProcessStatus.test.ts +++ b/src/process/LinuxProcessStatus.test.ts @@ -1,13 +1,15 @@ import { expect, it } from "vitest"; -import { parseLinuxProcessEffectiveUid } from "./LinuxProcessStatus.js"; +import { + linuxEnvironmentRestriction, + parseLinuxProcessIds, +} from "./LinuxProcessStatus.js"; -it("parses the effective UID from the single native status Uid row", () => { - expect( - parseLinuxProcessEffectiveUid( - "Name:\tworker\nUid:\t1000\t1001\t1002\t1003\nGid:\t20\t20\t20\t20\n", - ), - ).toBe(1001); +it("parses the single native status Uid and Gid rows", () => { + const status = + "Name:\tworker\nUid:\t1000\t1001\t1002\t1003\nGid:\t20\t21\t22\t23\n"; + expect(parseLinuxProcessIds(status, "Uid")).toEqual([1000, 1001, 1002, 1003]); + expect(parseLinuxProcessIds(status, "Gid")).toEqual([20, 21, 22, 23]); }); it.each([ @@ -16,6 +18,55 @@ it.each([ "Uid:\t1000\t1001\t1002\t1003\nUid:\t1000\t1001\t1002\t1003\n", "Uid:\t1000\t-1\t1002\t1003\n", "Uid:\t1000\t9007199254740992\t1002\t1003\n", -])("does not infer an effective UID from malformed status: %s", (status) => { - expect(parseLinuxProcessEffectiveUid(status)).toBeUndefined(); + "Uid:\t1000\t1001\t1002\t1003 extra\n", +])("does not infer IDs from malformed status: %s", (status) => { + expect(parseLinuxProcessIds(status, "Uid")).toBeUndefined(); +}); + +const caller = { callerUid: 1000, callerGid: 1000 } as const; +const matching = [1000, 1000, 1000, 1000] as const; + +it.each([ + [ + "a setgid program", + { uids: matching, gids: [1000, 2, 2, 2], procfsOwnerUid: 1000 }, + "credentials", + ], + [ + "a saved UID from another account", + { uids: [1000, 1000, 0, 1000], gids: matching, procfsOwnerUid: 1000 }, + "credentials", + ], + [ + "a non-dumpable process", + { uids: matching, gids: matching, procfsOwnerUid: 0 }, + "non-dumpable", + ], + [ + "an inspectable process", + { uids: matching, gids: matching, procfsOwnerUid: 1000 }, + undefined, + ], + [ + "a process with unknown procfs ownership and GIDs", + { uids: matching, gids: undefined, procfsOwnerUid: undefined }, + undefined, + ], +] as const)( + "derives the kernel environment restriction for %s", + (_name, facts, expected) => { + expect(linuxEnvironmentRestriction({ ...caller, ...facts })).toBe(expected); + }, +); + +it("does not derive a credential restriction without caller credentials", () => { + expect( + linuxEnvironmentRestriction({ + uids: [0, 0, 0, 0], + gids: [0, 0, 0, 0], + procfsOwnerUid: 0, + callerUid: undefined, + callerGid: undefined, + }), + ).toBeUndefined(); }); diff --git a/src/process/LinuxProcessStatus.ts b/src/process/LinuxProcessStatus.ts index 60d8f2809e..33d52dbceb 100644 --- a/src/process/LinuxProcessStatus.ts +++ b/src/process/LinuxProcessStatus.ts @@ -1,19 +1,60 @@ -/** Effective UID parsed from one Linux `/proc//status` record. */ -export const parseLinuxProcessEffectiveUid = ( +/** Real, effective, saved and filesystem IDs from one Linux status row. */ +export type LinuxProcessIds = readonly [number, number, number, number]; + +/** Parse the single `Uid:` or `Gid:` row of one Linux `/proc//status` record. */ +export const parseLinuxProcessIds = ( value: string, -): number | undefined => { - let effectiveUid: number | undefined; + row: "Uid" | "Gid", +): LinuxProcessIds | undefined => { + let ids: LinuxProcessIds | undefined; for (const line of value.split("\n")) { - if (!line.startsWith("Uid:")) continue; - if (effectiveUid !== undefined) return undefined; - const match = /^Uid:\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s*$/u.exec( - line, - ); + if (!line.startsWith(`${row}:`)) continue; + if (ids !== undefined) return undefined; + const match = + /^[UG]id:\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s+([0-9]+)\s*$/u.exec(line); if (match === null) return undefined; - const values = match.slice(1).map(Number); - if (values.some((uid) => !Number.isSafeInteger(uid) || uid < 0)) + const [real, effective, saved, filesystem] = match.slice(1).map(Number); + if ( + real === undefined || + effective === undefined || + saved === undefined || + filesystem === undefined || + [real, effective, saved, filesystem].some( + (id) => !Number.isSafeInteger(id) || id < 0, + ) + ) return undefined; - effectiveUid = values[1]; + ids = [real, effective, saved, filesystem]; } - return effectiveUid; + return ids; +}; + +/** + * Why Linux withholds `/proc//environ` from a caller without + * CAP_SYS_PTRACE (PTRACE_MODE_READ_FSCREDS): the target's real, effective or + * saved UID/GID differs from the caller's, or the target is non-dumpable, which + * procfs reports as root ownership of `/proc//environ`. + */ +export type LinuxEnvironmentRestriction = "credentials" | "non-dumpable"; + +/** Derive the kernel's environment restriction from one generation's procfs facts. */ +export const linuxEnvironmentRestriction = (facts: { + readonly uids: LinuxProcessIds; + readonly gids: LinuxProcessIds | undefined; + readonly procfsOwnerUid: number | undefined; + readonly callerUid: number | undefined; + readonly callerGid: number | undefined; +}): LinuxEnvironmentRestriction | undefined => { + const { uids, gids, procfsOwnerUid, callerUid, callerGid } = facts; + if ( + (callerUid !== undefined && + uids.slice(0, 3).some((uid) => uid !== callerUid)) || + (callerGid !== undefined && + gids !== undefined && + gids.slice(0, 3).some((gid) => gid !== callerGid)) + ) + return "credentials"; + return procfsOwnerUid !== undefined && procfsOwnerUid !== uids[1] + ? "non-dumpable" + : undefined; }; diff --git a/src/process/ProcessOwnership.identity.test.ts b/src/process/ProcessOwnership.identity.test.ts index 28d4ce9e4f..8076515f16 100644 --- a/src/process/ProcessOwnership.identity.test.ts +++ b/src/process/ProcessOwnership.identity.test.ts @@ -219,6 +219,76 @@ it }, ); +it + .skipIf(typeof process.getuid !== "function") + .each(["credentials", "non-dumpable"] as const)( + "records an unrelated same-UID Linux process whose environment is withheld for %s", + async (restriction) => { + const diagnostic = "EACCES: permission denied, open '/proc/908/environ'"; + const verify = async ( + processGroupId: number, + environmentRestriction: typeof restriction | undefined, + ) => { + const row: ProcessTableEntry = { + pid: 908, + parentPid: 1, + processGroupId, + uid: process.getuid!(), + state: "S", + command: "unrelated-process", + }; + const host: ProcessOwnershipHost = { + platform: "linux", + signalGroup: vi.fn(), + listProcesses: () => Promise.resolve([row]), + environment: () => Promise.resolve({}), + runTokens: () => + Promise.resolve( + new Map([ + [908, { state: "unavailable", reason: diagnostic } as const], + ]), + ), + processIdentities: (entries) => + Promise.resolve( + new Map( + entries.map((entry) => [ + entry.pid, + { + ...observedIdentity(entry, "current-generation"), + effectiveUid: process.getuid!(), + ...(environmentRestriction === undefined + ? {} + : { environmentRestriction }), + }, + ]), + ), + ), + }; + return verifyNoTokenOwnedProcesses("run-token", host, undefined, { + leaderPid: 100, + processGroupId: 100, + evidenceOwner: new ProcessOwnershipEvidenceOwner(), + }); + }; + + await expect(verify(908, restriction)).resolves.toMatchObject({ + cleaned: true, + unverified: [{ pid: 908, diagnostic }], + }); + // EACCES alone and a withheld member of the owned group stay fail-closed. + for (const [processGroupId, environmentRestriction] of [ + [908, undefined], + [100, restriction], + ] as const) + await expect( + verify(processGroupId, environmentRestriction), + ).resolves.toMatchObject({ + cleaned: false, + failures: [{ pid: 908, reason: "environment-unreadable" }], + }); + }, +); + it.each(["omitted", "throws"] as const)( "keeps a matching token unresolved when the first native identity is %s", async (firstRead) => { diff --git a/src/process/ProcessOwnership.procIdentity.test.ts b/src/process/ProcessOwnership.procIdentity.test.ts index fff53db724..fd519081d6 100644 --- a/src/process/ProcessOwnership.procIdentity.test.ts +++ b/src/process/ProcessOwnership.procIdentity.test.ts @@ -1,12 +1,18 @@ +import { performance } from "node:perf_hooks"; + import { expect, it, vi } from "vitest"; const filesystem = vi.hoisted(() => ({ readFile: vi.fn(), + // Host procfs ownership must never leak into fixture PIDs. + stat: vi.fn(async () => { + throw Object.assign(new Error("ENOENT"), { code: "ENOENT" }); + }), })); vi.mock("node:fs/promises", async (importOriginal) => { const original = await importOriginal(); - return { ...original, readFile: filesystem.readFile }; + return { ...original, readFile: filesystem.readFile, stat: filesystem.stat }; }); import { @@ -34,22 +40,66 @@ const procStat = ( parentPid = 1, processGroupId = 42, pid = 42, + flags = 0, ) => { - const fields = ["S", String(parentPid), String(processGroupId), "42"]; + const fields = [ + "S", + String(parentPid), + String(processGroupId), + "42", + "0", + "0", + String(flags), + ]; while (fields.length < 19) fields.push("0"); fields.push(String(startTime)); return `${String(pid)} (fixture) ${fields.join(" ")}`; }; -it("binds Linux effective UID to the same native process generation", async () => { +/** + * Select REA's caller credentials for the simulated Linux host. Windows has no + * `process.geteuid`/`getegid`, so define them for the call and restore the + * original properties (or their absence) afterwards. + */ +const withCaller = async ( + uid: number, + gid: number, + run: () => Promise, +): Promise => { + const names = ["geteuid", "getegid"] as const; + const originals = names.map((name) => + Object.getOwnPropertyDescriptor(process, name), + ); + names.forEach((name, index) => + Object.defineProperty(process, name, { + configurable: true, + writable: true, + value: () => (index === 0 ? uid : gid), + }), + ); + try { + return await run(); + } finally { + names.forEach((name, index) => { + const original = originals[index]; + if (original === undefined) Reflect.deleteProperty(process, name); + else Object.defineProperty(process, name, original); + }); + } +}; + +it("binds Linux credentials to the same native process generation", async () => { const host = createSystemProcessOwnershipHost("linux", {}); filesystem.readFile .mockResolvedValueOnce(procStat(100, 10, 42)) .mockResolvedValueOnce("Name:\tfixture\nUid:\t1000\t1001\t1002\t1003\n") .mockResolvedValueOnce(procStat(100, 11, 43)); + filesystem.stat.mockResolvedValueOnce({ uid: 1001 } as never); await expect( - host.processIdentities?.([entry], undefined, new Set([entry.pid])), + withCaller(1001, 20, async () => + host.processIdentities?.([entry], undefined, new Set([entry.pid])), + ), ).resolves.toEqual( new Map([ [ @@ -58,6 +108,8 @@ it("binds Linux effective UID to the same native process generation", async () = state: "readable", identity: "linux-starttime:100", effectiveUid: 1001, + // Kernel ptrace checks also compare the real and saved UIDs. + environmentRestriction: "credentials", lifecycle: { state: "observed", lifecycle: "non-zombie" }, relation: { state: "observed", @@ -73,8 +125,41 @@ it("binds Linux effective UID to the same native process generation", async () = "/proc/42/status", "/proc/42/stat", ]); + expect(filesystem.stat).toHaveBeenLastCalledWith("/proc/42/environ"); }); +it.each([ + [ + "a non-dumpable process", + 0, + "Gid:\t1000\t1000\t1000\t1000\n", + "non-dumpable", + ], + ["a setgid process", 1000, "Gid:\t1000\t5\t5\t5\n", "credentials"], + ["an inspectable process", 1000, "Gid:\t1000\t1000\t1000\t1000\n", undefined], +] as const)( + "reports the Linux environment restriction for %s", + async (_name, procfsOwnerUid, gidRow, expected) => { + const host = createSystemProcessOwnershipHost("linux", {}); + filesystem.readFile + .mockResolvedValueOnce(procStat(100)) + .mockResolvedValueOnce(`Uid:\t1000\t1000\t1000\t1000\n${gidRow}`) + .mockResolvedValueOnce(procStat(100)); + filesystem.stat.mockResolvedValueOnce({ uid: procfsOwnerUid } as never); + + const identity = ( + await withCaller(1000, 1000, async () => + host.processIdentities?.([entry], undefined, new Set([entry.pid])), + ) + )?.get(entry.pid); + + expect(identity).toMatchObject({ state: "readable", effectiveUid: 1000 }); + if (expected === undefined) + expect(identity).not.toHaveProperty("environmentRestriction"); + else expect(identity).toHaveProperty("environmentRestriction", expected); + }, +); + it("keeps native identity when status UID is unavailable", async () => { const host = createSystemProcessOwnershipHost("linux", {}); filesystem.readFile @@ -167,6 +252,45 @@ it("reads Linux effective UID only for token-unavailable rows", async () => { ); }); +it("keeps the Linux environment restriction while an exiting candidate settles", async () => { + let identityReads = 0; + filesystem.readFile.mockImplementation(async (path) => { + if (String(path) === "/proc/42/status") + return "Uid:\t1000\t1000\t1000\t1000\nGid:\t1000\t1000\t1000\t1000\n"; + if (String(path) !== "/proc/42/stat") + throw new Error(`unexpected path: ${String(path)}`); + identityReads += 1; + // PF_EXITING is set after the first identity read. + return procStat(100, 1, 42, 42, identityReads === 1 ? 0 : 4); + }); + filesystem.stat.mockResolvedValue({ uid: 0 } as never); + const systemHost = createSystemProcessOwnershipHost("linux", {}); + const host: ProcessOwnershipHost = { + ...systemHost, + runTokens: async () => + new Map([[entry.pid, { state: "unavailable", reason: "EACCES" }]]), + }; + + try { + const [fact] = await withCaller(1000, 1000, async () => + readProcessOwnershipFacts([entry], host, undefined, { + exitSettlement: { deadline: performance.now() + 30 }, + }), + ); + + expect(fact?.beforeExitSettlement).toBeDefined(); + expect(fact?.identityAfter).toMatchObject({ + state: "readable", + lifecycle: { state: "observed", lifecycle: "exiting" }, + environmentRestriction: "non-dumpable", + }); + expect(identityReads).toBeGreaterThan(3); + } finally { + filesystem.stat.mockReset(); + filesystem.stat.mockRejectedValue(filesystemError("ENOENT")); + } +}); + it("reports absence only when /proc lookup and signal-zero both report ESRCH", async () => { const host = createSystemProcessOwnershipHost("linux", {}); const signalProbe = vi.spyOn(process, "kill").mockImplementation(() => { diff --git a/src/process/ProcessOwnership.ts b/src/process/ProcessOwnership.ts index 98dc2480c6..2038c92971 100644 --- a/src/process/ProcessOwnership.ts +++ b/src/process/ProcessOwnership.ts @@ -14,6 +14,7 @@ import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; import { descendantsOf, liveProcesses } from "./ProcessOwnershipProcessTree.js"; import { readProcessRunTokens } from "./ProcessRunTokenObservations.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; +import type { LinuxEnvironmentRestriction } from "./LinuxProcessStatus.js"; import { ProcessOwnershipEvidenceOwner } from "./ProcessOwnershipEvidence.js"; import { @@ -90,6 +91,8 @@ export type ProcessIdentityObservation = readonly identity: string; /** Effective UID from the same native observation, when that boundary reports it. */ readonly effectiveUid?: number; + /** Linux procfs reason the kernel withholds this generation's environment from REA. */ + readonly environmentRestriction?: LinuxEnvironmentRestriction; /** Kept separate from the start identity; optional for older/custom hosts. */ readonly lifecycle?: ProcessLifecycleObservation; /** Native relation facts read with this identity, never copied from a process-table row. */ @@ -184,8 +187,8 @@ export interface ProcessOwnershipHost { processIdentities?( processes: readonly ProcessTableEntry[], signal?: AbortSignal, - /** Linux UID enrichment is selective; omitted means UID remains unknown. */ - effectiveUidPids?: ReadonlySet, + /** Linux credential enrichment is selective; omitted means UID and environment restriction remain unknown. */ + credentialPids?: ReadonlySet, ): Promise>; /** Snapshot identities before launch, honoring startup cancellation. */ captureBaseline?(signal?: AbortSignal): Promise; @@ -1422,15 +1425,19 @@ const currentProcessUid = (): number | undefined => * Whether the operating system provably withheld this candidate's run token: * current macOS omits the environment of Apple platform binaries and * CS_RESTRICT processes from KERN_PROCARGS2, and fails with EINVAL for - * processes owned by another account. These classes cannot be token-verified - * by this caller. An unrelated candidate is recorded as unverified instead - * of failing an otherwise clean capture; every related or otherwise - * unreadable candidate stays fail-closed. + * processes owned by another account. Linux denies `/proc//environ` for + * another account, mismatched real/saved IDs (setgid and setuid programs) and + * non-dumpable processes. These classes cannot be token-verified by this + * caller. An unrelated candidate is recorded as unverified instead of failing + * an otherwise clean capture; every related or otherwise unreadable candidate + * stays fail-closed. */ const environmentWithheldFromInspection = ( diagnostic: string, observedUid: number | undefined, + restriction?: LinuxEnvironmentRestriction, ): boolean => { + if (restriction !== undefined) return true; const uid = currentProcessUid(); if (uid !== undefined && observedUid !== undefined && observedUid !== uid) return true; @@ -1692,14 +1699,18 @@ const scanWithOwnershipEvidence = async ( relationAfter?.state === "observed" && relationBefore.parentPid === relationAfter.parentPid && relationBefore.processGroupId === relationAfter.processGroupId; - const observedUid = + const observed = fact.state === "same-generation" && fact.identityAfter.state === "readable" - ? fact.identityAfter.effectiveUid + ? fact.identityAfter : undefined; if ( knownUnrelated && - environmentWithheldFromInspection(fact.token.reason, observedUid) + environmentWithheldFromInspection( + fact.token.reason, + observed?.effectiveUid, + observed?.environmentRestriction, + ) ) { unverified.push({ pid: fact.process.pid, diff --git a/src/process/ProcessOwnershipObservation.ts b/src/process/ProcessOwnershipObservation.ts index 24608c2e3f..17fbe6de37 100644 --- a/src/process/ProcessOwnershipObservation.ts +++ b/src/process/ProcessOwnershipObservation.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { readFile, stat as statPath } from "node:fs/promises"; import { performance } from "node:perf_hooks"; import { setTimeout as delay } from "node:timers/promises"; import { @@ -9,7 +9,11 @@ import { type ProcessExitSettlementBudget, } from "./ProcessLifecycle.js"; import { parseLinuxProcessStat } from "./LinuxProcessStat.js"; -import { parseLinuxProcessEffectiveUid } from "./LinuxProcessStatus.js"; +import { + linuxEnvironmentRestriction, + parseLinuxProcessIds, + type LinuxEnvironmentRestriction, +} from "./LinuxProcessStatus.js"; import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js"; import { launcherIdentityFailure } from "./ProcessOwnershipIdentity.js"; @@ -679,7 +683,7 @@ const readIdentityFacts = async ( host: ProcessOwnershipHost, phase: "before" | "after", signal?: AbortSignal, - effectiveUidPids?: ReadonlySet, + credentialPids?: ReadonlySet, ): Promise> => { signal?.throwIfAborted(); if (host.processIdentities === undefined) @@ -696,7 +700,7 @@ const readIdentityFacts = async ( const observations = await host.processIdentities( processes, signal, - effectiveUidPids, + credentialPids, ); signal?.throwIfAborted(); return new Map( @@ -733,6 +737,8 @@ export const settleExitingProcessIdentities = async ( initial: ReadonlyMap, signal?: AbortSignal, budget: ProcessExitSettlementBudget = { deadline: undefined }, + /** Keep the initial read's selective Linux credential facts on refresh. */ + credentialPids?: ReadonlySet, ): Promise> => { signal?.throwIfAborted(); let pending = processes.filter(({ pid }) => @@ -755,7 +761,13 @@ export const settleExitingProcessIdentities = async ( signal?.throwIfAborted(); throw cause; } - const refreshed = await readNativeProcessIdentityMap(pending, host, signal); + const refreshed = await readIdentityFacts( + pending, + host, + "before", + signal, + credentialPids, + ); for (const { pid } of pending) identities.set(pid, identityFact(refreshed, pid, "after")); pending = pending.filter(({ pid }) => { @@ -818,7 +830,7 @@ export const readProcessOwnershipFacts = async ( } signal?.throwIfAborted(); - const effectiveUidPids = new Set( + const credentialPids = new Set( processes .filter(({ pid }) => tokens.get(pid)?.state !== "readable") .map(({ pid }) => pid), @@ -828,7 +840,7 @@ export const readProcessOwnershipFacts = async ( host, "after", signal, - effectiveUidPids, + credentialPids, ); const identityAfter = await settleExitingProcessIdentities( processes, @@ -836,6 +848,7 @@ export const readProcessOwnershipFacts = async ( observedAfter, signal, options.exitSettlement, + credentialPids, ); signal?.throwIfAborted(); const deriveFact = ( @@ -958,7 +971,7 @@ export const createSystemProcessOwnershipHost = ( }; const processIdentities: NonNullable< ProcessOwnershipHost["processIdentities"] - > = async (processes, signal, effectiveUidPids) => { + > = async (processes, signal, credentialPids) => { if (processes.length === 0) return new Map(); if (platform === "linux") return new Map( @@ -987,10 +1000,14 @@ export const createSystemProcessOwnershipHost = ( const readableIdentity = ( facts: typeof parsed, effectiveUid?: number, + environmentRestriction?: LinuxEnvironmentRestriction, ) => ({ state: "readable" as const, identity: `linux-starttime:${facts.startTime}`, ...(effectiveUid === undefined ? {} : { effectiveUid }), + ...(environmentRestriction === undefined + ? {} + : { environmentRestriction }), lifecycle: facts.lifecycle === undefined ? { @@ -1008,7 +1025,7 @@ export const createSystemProcessOwnershipHost = ( }, }); - if (!effectiveUidPids?.has(pid)) + if (!credentialPids?.has(pid)) return [pid, readableIdentity(parsed)] as const; let status: string; @@ -1022,9 +1039,26 @@ export const createSystemProcessOwnershipHost = ( return [pid, readableIdentity(parsed)] as const; } signal?.throwIfAborted(); - const effectiveUid = parseLinuxProcessEffectiveUid(status); - if (effectiveUid === undefined) + const uids = parseLinuxProcessIds(status, "Uid"); + if (uids === undefined) return [pid, readableIdentity(parsed)] as const; + // procfs reports root ownership of environ while a process is non-dumpable. + let procfsOwnerUid: number | undefined; + try { + procfsOwnerUid = ( + await statPath(`/proc/${String(pid)}/environ`) + ).uid; + } catch (cause: unknown) { + void cause; + } + signal?.throwIfAborted(); + const environmentRestriction = linuxEnvironmentRestriction({ + uids, + gids: parseLinuxProcessIds(status, "Gid"), + procfsOwnerUid, + callerUid: process.geteuid?.(), + callerGid: process.getegid?.(), + }); let afterStatus: string; try { @@ -1052,7 +1086,10 @@ export const createSystemProcessOwnershipHost = ( ] as const; if (verified === undefined || verified.pid !== pid) return [pid, readableIdentity(parsed)] as const; - return [pid, readableIdentity(verified, effectiveUid)] as const; + return [ + pid, + readableIdentity(verified, uids[1], environmentRestriction), + ] as const; } catch (cause: unknown) { if (isExpectedAbort(cause, signal)) throw cause; if (