Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 5 additions & 17 deletions src/abstract-interpretation/domains/multi-value-state-domain.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import type { NodeId } from '../../r-bridge/lang-4.x/ast/model/processing/node-id';
import { Bottom } from './lattice';
import { type AbstractProduct, PartialProductDomain } from './partial-product-domain';
import { type AbstractProduct, type ProductReduction, PartialProductDomain } from './partial-product-domain';
import { type StateDomainLift, StateAbstractDomain } from './state-abstract-domain';

/**
* A reduction function for abstract values of a product domain.
*/
export type Reduction<Product extends AbstractProduct> = (value: Product) => Product;

/**
* A multi-value state abstract domain that maps AST node IDs to multiple abstract values from different abstract domains.
* @template Product - Type of the abstract product of the multi-value domain combining multiple abstract values
Expand All @@ -16,7 +11,7 @@ export type Reduction<Product extends AbstractProduct> = (value: Product) => Pro
export class MultiValueStateDomain<Product extends AbstractProduct, Value extends StateDomainLift<MultiValueDomain<Product>> = StateDomainLift<MultiValueDomain<Product>>>
extends StateAbstractDomain<MultiValueDomain<Product>, Value> {

constructor(value: Value, domain: Required<Product>, reductions: readonly Reduction<Product>[] = []) {
constructor(value: Value, domain: Required<Product>, reductions: readonly ProductReduction<Product>[] = []) {
super(value, new MultiValueDomain(domain, domain, reductions));
}

Expand Down Expand Up @@ -54,23 +49,16 @@ export class MultiValueStateDomain<Product extends AbstractProduct, Value extend
export class MultiValueDomain<Product extends AbstractProduct>
extends PartialProductDomain<Product> {

public readonly reductions: readonly Reduction<Product>[];

constructor(value: Product, domain: Required<Product>, reductions: readonly Reduction<Product>[] = []) {
super(value, domain);
this.reductions = reductions;
constructor(value: Product, domain: Required<Product>, reductions: readonly ProductReduction<Product>[] = []) {
super(value, domain, reductions);
}

public create(value: Product): this;
public create(value: Product): MultiValueDomain<Product> {
return new MultiValueDomain(value, this.domain, this.reductions);
}

public static top<Product extends AbstractProduct>(domain: Required<Product>, reductions: readonly Reduction<Product>[] = []): MultiValueDomain<Product> {
public static top<Product extends AbstractProduct>(domain: Required<Product>, reductions: readonly ProductReduction<Product>[] = []): MultiValueDomain<Product> {
return new MultiValueDomain({} as Product, domain, reductions);
}

protected reduce(value: Product): Product {
return this.reductions.reduce((current, reduction) => reduction(current), value);
}
}
15 changes: 11 additions & 4 deletions src/abstract-interpretation/domains/partial-product-domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ export type ConcreteProductOf<Product extends AbstractProduct> = {
[Key in keyof Product]: Product[Key] extends AbstractDomain<infer Concrete, unknown, unknown, unknown> ? Concrete : never;
};

/** A reduction function of a reduced product domain refining the abstract value based on the values of its sub abstract domains. */
export type ProductReduction<Product extends AbstractProduct> = (value: Product) => Product;

/**
* A partial product abstract domain as named Cartesian product of (optional) sub abstract domains.
* The sub abstract domains are represented by a (partial) record mapping property names to abstract domains.
Expand All @@ -22,10 +25,13 @@ export type ConcreteProductOf<Product extends AbstractProduct> = {
export abstract class PartialProductDomain<Product extends AbstractProduct>
extends AbstractDomain<ConcreteProductOf<Product>, Product, Product, Product> {

public readonly domain: Required<Product>;
public readonly domain: Required<Product>;
public readonly reductions: readonly ProductReduction<Product>[];

constructor(value: Product, domain: Required<Product>) {
constructor(value: Product, domain: Required<Product>, reductions: readonly ProductReduction<Product>[] = []) {
super(Record.mapProperties(value, entry => entry?.create(entry.value)) as Product);
// must be assigned before reduce is called, as the (overridable) reduce may depend on the reductions
this.reductions = reductions;
(this._value as Writable<Product>) = this.reduce(this.value);
this.domain = domain;
}
Expand Down Expand Up @@ -192,9 +198,10 @@ export abstract class PartialProductDomain<Product extends AbstractProduct>
}

/**
* Optional reduction function for a reduced product domain.
* Applies the {@link reductions} of the (reduced) product domain to refine the abstract value based on its components.
* Subclasses may override this to implement a fixed reduction instead of (or in addition to) the configurable reductions.
*/
protected reduce(value: Product): Product {
return value;
return this.reductions.reduce((current, reduction) => reduction(current), value);
}
}
5 changes: 2 additions & 3 deletions src/queries/catalog/taint-query/taint-query-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@ import type { CommandCompletions } from '../../../cli/repl/core';
import { fileProtocol } from '../../../r-bridge/retriever';
import type { StateDomainLift } from '../../../abstract-interpretation/domains/state-abstract-domain';
import type { TaintInferenceResult } from '../../../taint-analysis/builder/taint-analysis';
import type {
TaintAnalysisDefinition } from '../../../taint-analysis/builder/taint-analysis-definition';




Expand All @@ -27,7 +26,7 @@ export interface TaintQuery extends BaseQueryFormat {
}

export interface TaintQueryResult<Analyses extends string[]> extends BaseQueryResult {
readonly results: Map<Analyses[number], TaintInferenceResult<TaintAnalysisDefinition<Analyses[number]>>>
readonly results: Map<Analyses[number], TaintInferenceResult>
}

const prefix = 'definitions:';
Expand Down
98 changes: 94 additions & 4 deletions src/taint-analysis/builder/taint-analysis-definition.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
import type { AnyAbstractDomain } from '../../abstract-interpretation/domains/abstract-domain';
import type { TaintMapper } from '../function-mapper';
import type { AbsintVisitorConfiguration } from '../../abstract-interpretation/absint-visitor';
import type { AbsintVisitorConfiguration, AbstractInterpretationVisitor } from '../../abstract-interpretation/absint-visitor';
import type { AnyStateDomain } from '../../abstract-interpretation/domains/state-domain-like';
import type { TaintComponent, TaintProduct } from '../composite-taint-visitor';
import { CompositeTaintInferenceVisitor } from '../composite-taint-visitor';
import { TaintInferenceVisitor } from '../taint-visitor';
import { guard } from '../../util/assert';
import type { ProductReduction } from '../../abstract-interpretation/domains/partial-product-domain';

export type TaintAnalysisName<Definition> = Definition extends TaintAnalysisDefinition<infer Name, infer _Domain, infer _Config> ? Name : never;
export type TaintAnalysisName<Definition> =
Definition extends RunnableTaintAnalysisDefinition<infer Name> ? Name : never;
export type TaintAnalysisDomain<Definition> = Definition extends TaintAnalysisDefinition<infer _Name, infer Domain> ? Domain : never;

/**
* The common interface of all (runnable) taint analysis definitions, i.e. single {@link TaintAnalysisDefinition|definitions}
* and {@link CompositeTaintAnalysisDefinition|composite definitions}. A runnable definition knows its name, an optional
* report message, and how to create the abstract interpretation visitor that conducts the analysis.
*/
export interface RunnableTaintAnalysisDefinition<Name extends string = string> {
/** The unique name of the taint analysis. */
readonly name: Name;
/** The optional message reported when the analysis produces a finding. */
readonly msg?: string;
/** Creates the abstract interpretation visitor that conducts the taint analysis for the given visitor configuration. */
createVisitor(config: AbsintVisitorConfiguration): AbstractInterpretationVisitor<AnyStateDomain>;
}

/** Options for composing multiple taint analyses into a {@link CompositeTaintAnalysisDefinition}. */
export interface ComposeOptions {
/**
* Optional reductions turning the direct product into a reduced product.
* Each reduction may refine the inferred taints of the component analyses based on each other.
*/
reductions?: readonly ProductReduction<TaintProduct>[];
/** The optional message reported when the composite analysis produces a finding. */
report?: string;
}

/**
* Fluent builder class for defining new taint analyses.
*/
export class TaintAnalysisDefinition<Name extends string = string, Domain extends AnyAbstractDomain = AnyAbstractDomain, Config extends AbsintVisitorConfiguration = AbsintVisitorConfiguration> {
export class TaintAnalysisDefinition<Name extends string = string, Domain extends AnyAbstractDomain = AnyAbstractDomain, Config extends AbsintVisitorConfiguration = AbsintVisitorConfiguration>
implements RunnableTaintAnalysisDefinition<Name> {
public readonly domain: Domain;
public mapper: TaintMapper<Domain> = [];
public name: Name;
Expand Down Expand Up @@ -40,4 +73,61 @@ export class TaintAnalysisDefinition<Name extends string = string, Domain extend
this._msg = msg;
return this;
}
}

public createVisitor(config: AbsintVisitorConfiguration): AbstractInterpretationVisitor<AnyStateDomain> {
return new TaintInferenceVisitor(this.domain, this.mapper, { ...this.config, ...config });
}

/**
* Composes at least two taint analysis definitions into a single composite taint analysis.
* The component analyses are evaluated simultaneously during a single control-flow traversal and their taints are
* combined into a product of the lattice values per each CFG node (see {@link CompositeTaintInferenceVisitor}).
* @param name - The unique name of the resulting composite taint analysis
* @param definitions - The component taint analysis definitions to compose (must have unique names)
* @param options - Optional reductions (for a reduced product) and a report message
*/
public static compose<Name extends string>(
name: Name,
definitions: readonly TaintAnalysisDefinition<string>[],
options?: ComposeOptions
): CompositeTaintAnalysisDefinition<Name> {
return new CompositeTaintAnalysisDefinition(name, definitions, options);
}
}

/**
* A composite taint analysis definition combining multiple {@link TaintAnalysisDefinition|component analyses} into a
* product (or reduced product) taint analysis. Create instances via {@link TaintAnalysisDefinition.compose}.
*/
export class CompositeTaintAnalysisDefinition<Name extends string> implements RunnableTaintAnalysisDefinition<Name> {
public readonly name: Name;
public readonly definitions: readonly TaintAnalysisDefinition[];
public readonly reductions: readonly ProductReduction<TaintProduct>[];

public msg: string | undefined;

constructor(name: Name, definitions: readonly TaintAnalysisDefinition[], options?: ComposeOptions) {
guard(definitions.length >= 2, 'A composite taint analysis must combine at least two taint analysis definitions');
const names = definitions.map(def => def.name);
guard(new Set(names).size === names.length, 'A composite taint analysis requires unique component analysis names');

this.name = name;
this.definitions = definitions;
this.reductions = options?.reductions ?? [];
this.msg = options?.report;
}

public report(msg: string): this {
this.msg = msg;
return this;
}

public createVisitor(config: AbsintVisitorConfiguration): AbstractInterpretationVisitor<AnyStateDomain> {
const components: TaintComponent[] = this.definitions.map(def => ({
name: def.name,
domain: def.domain,
mapper: def.mapper,
}));
return new CompositeTaintInferenceVisitor(components, this.reductions, config);
}
}
40 changes: 25 additions & 15 deletions src/taint-analysis/builder/taint-analysis.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { FlowrAnalyzer, ReadonlyFlowrAnalysisProvider } from '../../project/flowr-analyzer';
import type { TaintAnalysisDefinition, TaintAnalysisDomain } from './taint-analysis-definition';
import type { TaintAnalysisDefinition, CompositeTaintAnalysisDefinition, RunnableTaintAnalysisDefinition } from './taint-analysis-definition';
import type { AnyPredefinedTaintAnalysisName } from '../predefined/predefined';
import { predefinedTaintAnalyses } from '../predefined/predefined';
import { TaintInferenceVisitor } from '../taint-visitor';
import type { AbsintVisitorConfiguration, AbstractInterpretationVisitor } from '../../abstract-interpretation/absint-visitor';
import type { AnyStateDomain } from '../../abstract-interpretation/domains/state-domain-like';

export interface TaintInferenceResult<Analysis extends TaintAnalysisDefinition> {
visitor: TaintInferenceVisitor<TaintAnalysisDomain<Analysis>>
export interface TaintInferenceResult {
visitor: AbstractInterpretationVisitor<AnyStateDomain>
finding?: string
}

Expand All @@ -15,7 +16,7 @@ export interface TaintInferenceResult<Analysis extends TaintAnalysisDefinition>
*/
export class TaintAnalysis<Defs extends readonly string[] = []> {
private readonly analyzer: ReadonlyFlowrAnalysisProvider;
private readonly defs: TaintAnalysisDefinition<Defs[number]>[] = [];
private readonly defs: RunnableTaintAnalysisDefinition<Defs[number]>[] = [];

constructor(analyzer: ReadonlyFlowrAnalysisProvider) {
this.analyzer = analyzer;
Expand All @@ -31,20 +32,29 @@ export class TaintAnalysis<Defs extends readonly string[] = []> {
return this as unknown as TaintAnalysis<readonly [...Defs, Name]>;
}

/**
* Add a composite taint analysis that combines multiple taint analyses into a product of their lattice values.
* @see {@link TaintAnalysisDefinition.compose} to create a composite taint analysis definition.
*/
public addComposite<Name extends string>(def: CompositeTaintAnalysisDefinition<Name>): TaintAnalysis<readonly [...Defs, Name]> {
this.defs.push(def);
return this as unknown as TaintAnalysis<readonly [...Defs, Name]>;
}

/**
* Run one or multiple taint analyses.
* Note: Requires a prior call to {@link TaintAnalysis.add} or {@link TaintAnalysis.addPredefined} to add at least one taint analysis.
* Note: Requires a prior call to {@link TaintAnalysis.add}, {@link TaintAnalysis.addComposite}, or {@link TaintAnalysis.addPredefined} to add at least one taint analysis.
*/
public async run(): Promise<Map<Defs[number], TaintInferenceResult<TaintAnalysisDefinition<Defs[number]>>>> {
const results: Map<Defs[number], TaintInferenceResult<TaintAnalysisDefinition<Defs[number]>>> = new Map();
public async run(): Promise<Map<Defs[number], TaintInferenceResult>> {
const results: Map<Defs[number], TaintInferenceResult> = new Map();
const baseConfig: AbsintVisitorConfiguration = {
controlFlow: await this.analyzer.controlflow(),
ctx: this.analyzer.inspectContext(),
dfg: (await this.analyzer.dataflow()).graph,
normalizedAst: await this.analyzer.normalize()
};
for(const def of this.defs) {
const visitor = new TaintInferenceVisitor(def.domain, def.mapper, {
...def.config,
controlFlow: await this.analyzer.controlflow(),
ctx: this.analyzer.inspectContext(),
dfg: (await this.analyzer.dataflow()).graph,
normalizedAst: await this.analyzer.normalize()
});
const visitor = def.createVisitor(baseConfig);
visitor.start();

const endState = visitor.getEndState();
Expand Down
79 changes: 79 additions & 0 deletions src/taint-analysis/composite-taint-visitor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import type { AbsintVisitorConfiguration } from '../abstract-interpretation/absint-visitor';
import { AbstractInterpretationVisitor } from '../abstract-interpretation/absint-visitor';
import type { DataflowGraphVertexFunctionCall } from '../dataflow/graph/vertex';
import type { AnyAbstractDomain } from '../abstract-interpretation/domains/abstract-domain';
import type { TaintMapper } from './function-mapper';
import { mapFnCallToTaint, resolveTaint } from './function-mapper';
import type { NodeId } from '../r-bridge/lang-4.x/ast/model/processing/node-id';
import type { AbstractProduct, ProductReduction } from '../abstract-interpretation/domains/partial-product-domain';
import { type MultiValueDomain, MultiValueStateDomain } from '../abstract-interpretation/domains/multi-value-state-domain';

/**
* The abstract product mapping the name of a (component) taint analysis to its (value) abstract domain.
* Each property of the product holds the inferred taint of the respective component analysis.
*/
export type TaintProduct = AbstractProduct;

/**
* A single component of a composite taint analysis, i.e. one of the combined taint analyses.
* It bundles the analysis name (used as the product key), its (value) abstract domain, and its function mapper.
* Note that {@link TaintAnalysisDefinition} structurally conforms to this interface.
* @template Domain - The (value) abstract domain of the component analysis
*/
export interface TaintComponent<Domain extends AnyAbstractDomain = AnyAbstractDomain> {
/** The unique name of the component analysis, used as the key within the {@link TaintProduct}. */
readonly name: string;
/** The (value) abstract domain (taint lattice) of the component analysis. */
readonly domain: Domain;
/** The function mapper relating function names to the tainting behaviour of the component analysis. */
readonly mapper: TaintMapper<Domain>;
}

/**
* Abstract interpretation visitor for conducting composite taint analyses.
*
* In contrast to the single-domain {@link TaintInferenceVisitor}, this visitor evaluates multiple component taint
* analyses simultaneously during a single control-flow traversal and combines their taints into a product of the
* lattice values per each CFG node. The product is stored in a {@link MultiValueStateDomain}, so joins at CFG merge
* points (as well as widening, meet, and narrowing) are performed component-wise. Optional reductions turn the
* otherwise direct product into a reduced product, allowing the component analyses to refine each other.
*
* Please prefer using {@link TaintAnalysisDefinition.compose} together with the {@link FlowrAnalyzer.taint} method to
* create a composite taint analysis.
*/
export class CompositeTaintInferenceVisitor extends AbstractInterpretationVisitor<MultiValueStateDomain<TaintProduct>> {
private readonly components: readonly TaintComponent[];

constructor(
components: readonly TaintComponent[],
reductions: readonly ProductReduction<TaintProduct>[],
visitorConfig: AbsintVisitorConfiguration
) {
const template = Object.fromEntries(components.map(component => [component.name, component.domain])) as Required<TaintProduct>;
super(
{ ...visitorConfig, ignoreUnsupportedFunctions: false },
new MultiValueStateDomain<TaintProduct>(new Map<NodeId, MultiValueDomain<TaintProduct>>(), template, reductions)
);
this.components = components;
}

protected override onFunctionCall({ call }: { call: DataflowGraphVertexFunctionCall }): void {
super.onFunctionCall({ call });

const node = this.getNormalizedAst(call.id);

if(node === undefined) {
return;
}
const product: TaintProduct = {};

for(const component of this.components) {
const taint = mapFnCallToTaint(node, component.mapper, this.config.dfg, this.config.ctx);

// project the product state of an argument node onto the component of this analysis (defaulting to Top)
product[component.name] = resolveTaint(taint, component.domain, argId =>
this.getAbstractValue(argId)?.value[component.name] ?? component.domain.top());
}
this.currentState.set(node.info.id, this.currentState.domain.create(product));
}
}
Loading