mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-04 07:27:20 +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": {
|
||||
"fingerprint": "792229965a726d2c6b527f9ee65440a2b3023839ee71cb51522fc30e2f2cb454",
|
||||
"scaling_budget": 1.5,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,16 @@ function langToolkit(langId) {
|
|||
// ---- synthetic generators (one cost dimension each) ----
|
||||
|
||||
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',
|
||||
small: 60,
|
||||
|
|
|
|||
|
|
@ -290,6 +290,9 @@ export class TsHarvester {
|
|||
// `pattern`); plain JS puts the pattern directly in formal_parameters.
|
||||
const pattern = p.childForFieldName('pattern') ?? p;
|
||||
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 (
|
||||
this.hasGuardCandidate &&
|
||||
pattern.type === 'identifier' &&
|
||||
|
|
|
|||
|
|
@ -85,6 +85,11 @@ export const taintSummariesPhase: PipelinePhase<TaintSummariesOutput> = {
|
|||
const maxEdges = ctx.options?.pdgMaxInterprocEdges ?? DEFAULT_PDG_MAX_INTERPROC_EDGES;
|
||||
|
||||
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) =>
|
||||
logger.warn(m),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -177,11 +177,16 @@ export function createFunctionRefinement(cfg: FunctionCfg): FunctionRefinement {
|
|||
calls.set(`${block.index}:${index}:${call.site}`, call);
|
||||
}
|
||||
}
|
||||
const guards = computeGuardRefinements(cfg, {
|
||||
eligible: cfg.refinementSafe === true,
|
||||
primitiveBindings: cfg.primitiveBindings ?? [],
|
||||
predicates,
|
||||
});
|
||||
const guards = computeGuardRefinements(
|
||||
cfg,
|
||||
{
|
||||
eligible: cfg.refinementSafe === true,
|
||||
primitiveBindings: cfg.primitiveBindings ?? [],
|
||||
predicates,
|
||||
},
|
||||
undefined,
|
||||
definitions,
|
||||
);
|
||||
const active = cfg.refinementSafe === true && guards.status !== 'fallback';
|
||||
return {
|
||||
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(
|
||||
cfg: FunctionCfg,
|
||||
facts?: GuardFacts,
|
||||
limits?: { maxVisits?: number; maxGuards?: number; maxWords?: number },
|
||||
definitions?: ReadonlyMap<number, ValueExpression>,
|
||||
): GuardRefinements {
|
||||
if (!facts?.predicates?.length) return NO_GUARDS;
|
||||
let visits = 0;
|
||||
|
|
@ -344,11 +354,25 @@ export function computeGuardRefinements(
|
|||
if (!isInitial) written.add(binding);
|
||||
}
|
||||
for (const binding of statement.uses) {
|
||||
if (++visits > maxVisits) return fallback();
|
||||
if (!subjects.has(binding)) continue;
|
||||
let blocks = uses.get(binding);
|
||||
if (!blocks) uses.set(binding, (blocks = new Set()));
|
||||
blocks.add(block.index);
|
||||
// Argument reconstruction expands aliases at the sink. Certify the
|
||||
// subjects reached through those same definitions at that use block.
|
||||
const pending: ValueExpression[] = [{ kind: 'binding', binding }];
|
||||
const seen = new Set<number>();
|
||||
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-');
|
||||
|
||||
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 () => {
|
||||
const cases: Record<string, string> = {
|
||||
bypass:
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { parse } from '../../helpers/ts-cfg-harness.js';
|
||||
import { cfgOf, parse } from '../../helpers/ts-cfg-harness.js';
|
||||
import {
|
||||
createRefinementFileContext,
|
||||
readPredicate,
|
||||
|
|
@ -38,6 +38,20 @@ function predicate(source: string) {
|
|||
}
|
||||
|
||||
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', () => {
|
||||
const root = parse(`const SHA_RE = /^[0-9a-f]{7,40}$/;
|
||||
function f(ref: string) {
|
||||
|
|
|
|||
|
|
@ -109,6 +109,20 @@ describe('accepted-edge guard proof', () => {
|
|||
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', () => {
|
||||
const { cfg, facts, subject, sink } = fixture(
|
||||
'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) ──────────────────────────
|
||||
|
||||
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', () => {
|
||||
const r = analyze(
|
||||
`import { execFileSync } from 'node:child_process';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue