mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
fix(taint): preserve guard proof identities
Reject destructured/rest formals before they inherit a whole-argument proof, and include transitive alias uses in bounded guard certificates. Add worker regressions, argument-heavy scaling coverage and global refinement fallback diagnostics. Refs #3703
This commit is contained in:
parent
7b4c7b33bd
commit
6ee7ce1cc4
9 changed files with 185 additions and 12 deletions
|
|
@ -1,4 +1,14 @@
|
||||||
{
|
{
|
||||||
|
"guard-free-arguments": {
|
||||||
|
"fingerprint": "819f475c96ced034846e1c7d8aa08613386b258f6df276a1e4058944fffa9231",
|
||||||
|
"scaling_budget": 1.8,
|
||||||
|
"disk_bytes_budget": 1.2,
|
||||||
|
"heap_budget": 1.3,
|
||||||
|
"rd_scaling_budget": 2.0,
|
||||||
|
"facts_large_min": 8000,
|
||||||
|
"disk_bytes_large_max": 6000000,
|
||||||
|
"_note": "#3703 review: 1000/4000 guard-free functions with const composition and argument forwarding exercise value-fact capture even without predicates. Initial normalized time/disk/heap/def-use ratios: 0.878/1.012/0.999/0.834; large payload 5,306,959 bytes. A separate five-pair comparison at the reviewed head versus the merge base measured +11.26% collection time and +27.0% serialized bytes on the same argument-heavy shape (different file/spacing). This exceeds the plan's proposed 5% guard-free target; the earlier ~1% samples do not cover this capture path. Keep the cost explicit; this gate checks scaling and payload growth, not a validated <=5% overhead claim. Existing scenario budgets remain unchanged."
|
||||||
|
},
|
||||||
"straight-line": {
|
"straight-line": {
|
||||||
"fingerprint": "792229965a726d2c6b527f9ee65440a2b3023839ee71cb51522fc30e2f2cb454",
|
"fingerprint": "792229965a726d2c6b527f9ee65440a2b3023839ee71cb51522fc30e2f2cb454",
|
||||||
"scaling_budget": 1.5,
|
"scaling_budget": 1.5,
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,16 @@ function langToolkit(langId) {
|
||||||
// ---- synthetic generators (one cost dimension each) ----
|
// ---- synthetic generators (one cost dimension each) ----
|
||||||
|
|
||||||
const SCENARIOS = [
|
const SCENARIOS = [
|
||||||
|
{
|
||||||
|
name: 'guard-free-arguments',
|
||||||
|
small: 1000,
|
||||||
|
large: 4000,
|
||||||
|
gen: (n) =>
|
||||||
|
Array.from(
|
||||||
|
{ length: n },
|
||||||
|
(_, i) => `function f${i}(x: string) { const y = 'prefix ' + x; return consume${i}(y); }`,
|
||||||
|
).join('\n'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'guarded-commands',
|
name: 'guarded-commands',
|
||||||
small: 60,
|
small: 60,
|
||||||
|
|
|
||||||
|
|
@ -290,6 +290,9 @@ export class TsHarvester {
|
||||||
// `pattern`); plain JS puts the pattern directly in formal_parameters.
|
// `pattern`); plain JS puts the pattern directly in formal_parameters.
|
||||||
const pattern = p.childForFieldName('pattern') ?? p;
|
const pattern = p.childForFieldName('pattern') ?? p;
|
||||||
this.declarePattern(pattern, 'param', this.root, true, formalIndex);
|
this.declarePattern(pattern, 'param', this.root, true, formalIndex);
|
||||||
|
// A formal index identifies the argument slot, not a value extracted
|
||||||
|
// from it. Extraction/default/rest effects are not modeled as identity.
|
||||||
|
if (pattern.type !== 'identifier') this.refinementSafe = false;
|
||||||
if (
|
if (
|
||||||
this.hasGuardCandidate &&
|
this.hasGuardCandidate &&
|
||||||
pattern.type === 'identifier' &&
|
pattern.type === 'identifier' &&
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,11 @@ export const taintSummariesPhase: PipelinePhase<TaintSummariesOutput> = {
|
||||||
const maxEdges = ctx.options?.pdgMaxInterprocEdges ?? DEFAULT_PDG_MAX_INTERPROC_EDGES;
|
const maxEdges = ctx.options?.pdgMaxInterprocEdges ?? DEFAULT_PDG_MAX_INTERPROC_EDGES;
|
||||||
|
|
||||||
const solved = solveInterprocTaint(summaryMap, callEdges, { maxFindings, maxHops });
|
const solved = solveInterprocTaint(summaryMap, callEdges, { maxFindings, maxHops });
|
||||||
|
if (solved.refinementFallback) {
|
||||||
|
logger.warn(
|
||||||
|
'[taint-interproc] refinement memory budget exhausted; resumed conservative analysis',
|
||||||
|
);
|
||||||
|
}
|
||||||
const emit = emitInterprocTaint(ctx.graph, solved.findings, { maxEdges }, (m) =>
|
const emit = emitInterprocTaint(ctx.graph, solved.findings, { maxEdges }, (m) =>
|
||||||
logger.warn(m),
|
logger.warn(m),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -177,11 +177,16 @@ export function createFunctionRefinement(cfg: FunctionCfg): FunctionRefinement {
|
||||||
calls.set(`${block.index}:${index}:${call.site}`, call);
|
calls.set(`${block.index}:${index}:${call.site}`, call);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const guards = computeGuardRefinements(cfg, {
|
const guards = computeGuardRefinements(
|
||||||
eligible: cfg.refinementSafe === true,
|
cfg,
|
||||||
primitiveBindings: cfg.primitiveBindings ?? [],
|
{
|
||||||
predicates,
|
eligible: cfg.refinementSafe === true,
|
||||||
});
|
primitiveBindings: cfg.primitiveBindings ?? [],
|
||||||
|
predicates,
|
||||||
|
},
|
||||||
|
undefined,
|
||||||
|
definitions,
|
||||||
|
);
|
||||||
const active = cfg.refinementSafe === true && guards.status !== 'fallback';
|
const active = cfg.refinementSafe === true && guards.status !== 'fallback';
|
||||||
return {
|
return {
|
||||||
guards,
|
guards,
|
||||||
|
|
@ -258,11 +263,16 @@ export function createMemoizedRefinement(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Removing an accepted edge must make the use unreachable from ENTRY. */
|
/**
|
||||||
|
* Removing an accepted edge must make the use unreachable from ENTRY.
|
||||||
|
* Definitions are the validated value expressions reconstructed by the caller.
|
||||||
|
* Expanding their uses shares the traversal cap; no certificates survive exhaustion.
|
||||||
|
*/
|
||||||
export function computeGuardRefinements(
|
export function computeGuardRefinements(
|
||||||
cfg: FunctionCfg,
|
cfg: FunctionCfg,
|
||||||
facts?: GuardFacts,
|
facts?: GuardFacts,
|
||||||
limits?: { maxVisits?: number; maxGuards?: number; maxWords?: number },
|
limits?: { maxVisits?: number; maxGuards?: number; maxWords?: number },
|
||||||
|
definitions?: ReadonlyMap<number, ValueExpression>,
|
||||||
): GuardRefinements {
|
): GuardRefinements {
|
||||||
if (!facts?.predicates?.length) return NO_GUARDS;
|
if (!facts?.predicates?.length) return NO_GUARDS;
|
||||||
let visits = 0;
|
let visits = 0;
|
||||||
|
|
@ -344,11 +354,25 @@ export function computeGuardRefinements(
|
||||||
if (!isInitial) written.add(binding);
|
if (!isInitial) written.add(binding);
|
||||||
}
|
}
|
||||||
for (const binding of statement.uses) {
|
for (const binding of statement.uses) {
|
||||||
if (++visits > maxVisits) return fallback();
|
// Argument reconstruction expands aliases at the sink. Certify the
|
||||||
if (!subjects.has(binding)) continue;
|
// subjects reached through those same definitions at that use block.
|
||||||
let blocks = uses.get(binding);
|
const pending: ValueExpression[] = [{ kind: 'binding', binding }];
|
||||||
if (!blocks) uses.set(binding, (blocks = new Set()));
|
const seen = new Set<number>();
|
||||||
blocks.add(block.index);
|
while (pending.length) {
|
||||||
|
if (++visits > maxVisits) return fallback();
|
||||||
|
const value = pending.pop()!;
|
||||||
|
if (value.kind === 'concat') pending.push(...value.parts);
|
||||||
|
else if (value.kind === 'binding' && !seen.has(value.binding)) {
|
||||||
|
seen.add(value.binding);
|
||||||
|
if (subjects.has(value.binding)) {
|
||||||
|
let blocks = uses.get(value.binding);
|
||||||
|
if (!blocks) uses.set(value.binding, (blocks = new Set()));
|
||||||
|
blocks.add(block.index);
|
||||||
|
}
|
||||||
|
const definition = definitions?.get(value.binding);
|
||||||
|
if (definition) pending.push(definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,69 @@ import { createTempDirPool } from '../../helpers/temp-dir-pool.js';
|
||||||
const repos = createTempDirPool('gn-guard-taint-');
|
const repos = createTempDirPool('gn-guard-taint-');
|
||||||
|
|
||||||
describe('guard-aware command taint (#3490)', () => {
|
describe('guard-aware command taint (#3490)', () => {
|
||||||
|
it('preserves alias proofs across blocks and retains destructuring findings in real workers', async () => {
|
||||||
|
const repo = repos.dir();
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(repo, 'aliases.ts'),
|
||||||
|
`
|
||||||
|
import {execSync} from 'node:child_process';
|
||||||
|
function indexed({9: command}: any) { execSync(command); }
|
||||||
|
function defaulted(context: any, {command = context.query.command}: any) { execSync(command); }
|
||||||
|
function forward(command: string, log: boolean) {
|
||||||
|
const alias = command;
|
||||||
|
if (log) console.log('validated');
|
||||||
|
execSync(alias);
|
||||||
|
}
|
||||||
|
export function handleIndexed(req: any) {
|
||||||
|
const ref: string = req.query.ref;
|
||||||
|
if (!/^[a-z]{1,40}$/.test(ref)) return;
|
||||||
|
indexed('git show ' + ref);
|
||||||
|
}
|
||||||
|
export function handleDefaulted(req: any) {
|
||||||
|
const ref: string = req.query.ref;
|
||||||
|
if (!/^[a-z]{1,40}$/.test(ref)) return;
|
||||||
|
defaulted(req, 'git show ' + ref);
|
||||||
|
}
|
||||||
|
export function handleAlias(req: any, log: boolean) {
|
||||||
|
const ref: string = req.query.ref;
|
||||||
|
if (!/^[0-9a-f]{7,40}$/.test(ref)) return;
|
||||||
|
const args = 'show ' + ref;
|
||||||
|
const command = 'git ' + args;
|
||||||
|
if (log) console.log('validated');
|
||||||
|
execSync(command);
|
||||||
|
forward(command, log);
|
||||||
|
}
|
||||||
|
export function handleBypass(req: any, log: boolean) {
|
||||||
|
const ref: string = req.query.ref;
|
||||||
|
if (log) { if (!/^[0-9a-f]{7,40}$/.test(ref)) return; }
|
||||||
|
const command = 'git show ' + ref;
|
||||||
|
if (log) console.log('unchecked');
|
||||||
|
execSync(command);
|
||||||
|
forward(command, log);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
const result = await runPipelineFromRepo(repo, () => {}, { pdg: true, workerPoolSize: 1 });
|
||||||
|
const paths = [...result.graph.iterRelationships()]
|
||||||
|
.filter((edge) => edge.type === 'TAINT_PATH')
|
||||||
|
.map((edge) => result.graph.getNode(edge.sourceId)?.properties.name);
|
||||||
|
expect(paths).toContain('handleIndexed');
|
||||||
|
expect(paths).toContain('handleDefaulted');
|
||||||
|
expect(paths).toContain('handleBypass');
|
||||||
|
expect(paths).not.toContain('handleAlias');
|
||||||
|
const localSources = [...result.graph.iterRelationships()]
|
||||||
|
.filter((edge) => edge.type === 'TAINTED')
|
||||||
|
.map((edge) => result.graph.getNode(edge.sourceId)?.properties.startLine);
|
||||||
|
const source = fs.readFileSync(path.join(repo, 'aliases.ts'), 'utf8').split('\n');
|
||||||
|
// TAINTED starts at the request read, immediately after the declaration.
|
||||||
|
const aliasLine =
|
||||||
|
source.findIndex((line) => line.startsWith('export function handleAlias')) + 2;
|
||||||
|
const bypassLine =
|
||||||
|
source.findIndex((line) => line.startsWith('export function handleBypass')) + 2;
|
||||||
|
expect(localSources).not.toContain(aliasLine);
|
||||||
|
expect(localSources).toContain(bypassLine);
|
||||||
|
}, 120_000);
|
||||||
|
|
||||||
it('retains findings for bypasses, mutation, uncertain types and unsupported sink uses', async () => {
|
it('retains findings for bypasses, mutation, uncertain types and unsupported sink uses', async () => {
|
||||||
const cases: Record<string, string> = {
|
const cases: Record<string, string> = {
|
||||||
bypass:
|
bypass:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { parse } from '../../helpers/ts-cfg-harness.js';
|
import { cfgOf, parse } from '../../helpers/ts-cfg-harness.js';
|
||||||
import {
|
import {
|
||||||
createRefinementFileContext,
|
createRefinementFileContext,
|
||||||
readPredicate,
|
readPredicate,
|
||||||
|
|
@ -38,6 +38,20 @@ function predicate(source: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('normalized predicate and expression facts', () => {
|
describe('normalized predicate and expression facts', () => {
|
||||||
|
it.each([
|
||||||
|
'{command}',
|
||||||
|
'{9: command}',
|
||||||
|
'[command]',
|
||||||
|
'[, command]',
|
||||||
|
'...command',
|
||||||
|
'{command = fallback()}',
|
||||||
|
'{nested: {command}}',
|
||||||
|
])('keeps non-identity formal bindings conservative: %s', (pattern) => {
|
||||||
|
const cfg = cfgOf(`function f(${pattern}: any) { consume(command); }`);
|
||||||
|
expect(cfg.refinementSafe).toBe(false);
|
||||||
|
expect(createFunctionRefinement(cfg).guards.status).toBe('fallback');
|
||||||
|
});
|
||||||
|
|
||||||
it('preserves guard and value identity through embedded coordinate mappings', () => {
|
it('preserves guard and value identity through embedded coordinate mappings', () => {
|
||||||
const root = parse(`const SHA_RE = /^[0-9a-f]{7,40}$/;
|
const root = parse(`const SHA_RE = /^[0-9a-f]{7,40}$/;
|
||||||
function f(ref: string) {
|
function f(ref: string) {
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,20 @@ describe('accepted-edge guard proof', () => {
|
||||||
expect(computeGuardRefinements(cfg, facts).at(subject, sink)).toBeUndefined();
|
expect(computeGuardRefinements(cfg, facts).at(subject, sink)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('charges transitive alias expansion to the same traversal budget', () => {
|
||||||
|
const { cfg, facts, subject } = fixture();
|
||||||
|
const aliases = new Map(
|
||||||
|
Array.from({ length: 200 }, (_, index) => [
|
||||||
|
subject + index,
|
||||||
|
{ kind: 'binding' as const, binding: subject + index + 1 },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
const proof = computeGuardRefinements(cfg, facts, { maxVisits: 128 }, aliases);
|
||||||
|
expect(proof.status).toBe('fallback');
|
||||||
|
expect(proof.visits).toBeLessThanOrEqual(129);
|
||||||
|
expect(proof.at(subject, cfg.entryIndex)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('does not certify reassigned parameters or unknown primitive identity', () => {
|
it('does not certify reassigned parameters or unknown primitive identity', () => {
|
||||||
const { cfg, facts, subject, sink } = fixture(
|
const { cfg, facts, subject, sink } = fixture(
|
||||||
'if (!valid(x)) return; x = input(); consume(x);',
|
'if (!valid(x)) return; x = input(); consume(x);',
|
||||||
|
|
|
||||||
|
|
@ -546,6 +546,36 @@ describe('multi-source identity — distinct sources do not merge at one def', (
|
||||||
// ── kind-set exclusion model (real built-in model) ──────────────────────────
|
// ── kind-set exclusion model (real built-in model) ──────────────────────────
|
||||||
|
|
||||||
describe('kind-set exclusions — sanitizers neutralize their kinds only', () => {
|
describe('kind-set exclusions — sanitizers neutralize their kinds only', () => {
|
||||||
|
it.each([
|
||||||
|
['direct alias', 'const command = "git show " + ref;', 'command', 0],
|
||||||
|
[
|
||||||
|
'transitive alias',
|
||||||
|
'const args = "show " + ref; const command = "git " + args;',
|
||||||
|
'command',
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
['template alias', 'const command = `git show ${ref}`;', 'command', 0],
|
||||||
|
['unsafe template', 'const command = `git show ;${ref}`;', 'command', 1],
|
||||||
|
['bypassed guard', 'const command = "git show " + ref;', 'command', 1],
|
||||||
|
['mutated subject', 'ref = req.query.other; const command = "git show " + ref;', 'command', 1],
|
||||||
|
['mutated alias', 'let command = "git show " + ref; command = req.query.other;', 'command', 1],
|
||||||
|
])('keeps certificate semantics across blocks: %s', (name, definition, argument, findings) => {
|
||||||
|
const guard = 'if (!/^[0-9a-f]{7,40}$/.test(ref)) return;';
|
||||||
|
const r = analyze(
|
||||||
|
`import {execSync} from 'node:child_process';
|
||||||
|
function f(req: any, log: boolean) {
|
||||||
|
${name === 'mutated subject' ? 'let' : 'const'} ref: string = req.query.ref;
|
||||||
|
${name === 'bypassed guard' ? `if (log) { ${guard} }` : guard}
|
||||||
|
${definition}
|
||||||
|
if (log) console.log('validated');
|
||||||
|
execSync(${argument});
|
||||||
|
}`,
|
||||||
|
{ spec: TS_JS_TAINT_MODEL },
|
||||||
|
);
|
||||||
|
expect(r.status).toBe('computed');
|
||||||
|
expect(r.findings).toHaveLength(findings);
|
||||||
|
});
|
||||||
|
|
||||||
it('built-in model catches argv-form child_process command sinks', () => {
|
it('built-in model catches argv-form child_process command sinks', () => {
|
||||||
const r = analyze(
|
const r = analyze(
|
||||||
`import { execFileSync } from 'node:child_process';
|
`import { execFileSync } from 'node:child_process';
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue