Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d6c861a
feat(rules): express Spring whole-object source and sink taint via th…
misonijnik Jul 23, 2026
5dd306c
refactor(rules): field-sensitive java.io.File model and $*VAR syntax
misonijnik Jul 23, 2026
cfe5aaa
test(querylang): coverage for the changed passthrough config entries
misonijnik Jul 23, 2026
56fc2c9
test(querylang): java.nio buffer passthrough coverage before the rule…
misonijnik Jul 23, 2026
d252c13
refactor(config): split NameClassPair name/className/nameInNamespace
misonijnik Jul 23, 2026
032e6c1
test(e2e): behavioural coverage for the 9 rule-storage cleanup fixes
misonijnik Jul 23, 2026
24a1d7a
fix(config): close BasicControl#getID whole-object leak (star ctrlSink)
misonijnik Jul 23, 2026
30a0685
test(phase3): probe DateFormatSymbols generic set./get. whole-object …
misonijnik Jul 23, 2026
5558090
fix(config): close DateFormatSymbols set./get. whole-object leak
misonijnik Jul 23, 2026
311a448
test(config): pin taint isolation for 8 more split bean classes
misonijnik Jul 23, 2026
7e579ba
test(config): reframe ScriptContext key-insensitivity as accepted, cl…
misonijnik Jul 23, 2026
1c4834c
test(querylang): pin that a starred source reaches a field-sensitive …
misonijnik Jul 24, 2026
9a9540d
fix(analyzer): Field based default get
Saloed Aug 11, 2026
35dace6
refactor(dataflow): drop the <rule-storage> unroll exception
misonijnik Aug 12, 2026
17b6bec
fix(dataflow): apply the default get model only when no rule matched
misonijnik Aug 12, 2026
7c2d829
refactor(dataflow): delete the String bytes clean special case
misonijnik Aug 13, 2026
8d6f4b0
fix(querylang): honour focus-metavariable on sanitizers
misonijnik Aug 19, 2026
17264fc
fix(dataflow): answer the field-unfold request on fact-to-fact edges
misonijnik Aug 19, 2026
86589ce
refactor(dataflow): delete the array-element mechanism
misonijnik Aug 27, 2026
b0d205d
test(dataflow): pin that only the star gives element taint
misonijnik Aug 30, 2026
28a9869
test(dataflow): cover interprocedural any-field unfolding
Saloed Sep 14, 2026
3d9a948
reduce mark request handling workload
Saloed Sep 14, 2026
21cf902
reduce mark request handling workload 2.0
Saloed Sep 15, 2026
11e0558
reduce mark request handling workload 3.0
Saloed Sep 16, 2026
15192f6
Fix exclusion
Saloed Sep 17, 2026
58c7ff5
refactor(dataflow): hold the unfold demand per analysed method
Saloed Sep 19, 2026
1296a8e
Align Phase3 fixtures with field and array taint
misonijnik Oct 23, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -905,7 +905,7 @@ class NormalMethodAnalyzer(
methodInitialFactBase = sub.methodInitialFactBase,
sideEffectSummaries = sideEffectSummaries,
) { currentFactAp, summaryEffect, kind ->
handler.handleFactToFact(sub.currentEdge.initialFactAp, currentFactAp, summaryEffect, kind)
handler.handleFactToFact(methodEntryPoint, sub.currentEdge.initialFactAp, currentFactAp, summaryEffect, kind)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
package org.opentaint.dataflow.ap.ifds.analysis

import org.opentaint.dataflow.ap.ifds.MethodEntryPoint
import org.opentaint.dataflow.taint.MarkUnfoldDemand

interface MethodAnalysisContext {
val methodEntryPoint: MethodEntryPoint

/**
* Accessors already demanded by answers to a `TaintMarkFieldUnfoldRequest` handled while
* analysing this method. One per analysed method, so a question is only ever conflated with
* another asked in the same frame.
*/
val markUnfoldDemand: MarkUnfoldDemand

// todo: remove, required for trace generation
val methodCallFactMapper: MethodCallFactMapper
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.opentaint.dataflow.ap.ifds.analysis

import org.opentaint.dataflow.ap.ifds.ExclusionSet
import org.opentaint.dataflow.ap.ifds.MethodEntryPoint
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement
Expand All @@ -24,6 +25,7 @@ interface MethodSideEffectSummaryHandler {
}

fun handleFactToFact(
methodEntryPoint: MethodEntryPoint,
currentInitialFactAp: InitialFactAp,
currentFactAp: FinalFactAp,
summaryEffect: SummaryEdgeApplication,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.opentaint.dataflow.taint

import org.opentaint.dataflow.ap.ifds.Accessor
import org.opentaint.dataflow.ap.ifds.MethodEntryPoint
import org.opentaint.dataflow.ap.ifds.SideEffectKind
import org.opentaint.dataflow.ap.ifds.TaintMarkAccessor
Expand All @@ -12,7 +13,8 @@ interface FactWithMarkAfterAnyAccessorResolver {
data class TaintMarkFieldUnfoldRequest(
val method: MethodEntryPoint,
val fact: InitialFactAp,
val mark: TaintMarkAccessor
val mark: TaintMarkAccessor,
val suffix: Accessor?
) : SideEffectKind

data class DefaultFactWithMarkAfterAnyFieldResolver(
Expand All @@ -21,7 +23,7 @@ data class DefaultFactWithMarkAfterAnyFieldResolver(
private val addSideEffect: (InitialFactAp, SideEffectKind) -> Unit
): FactWithMarkAfterAnyAccessorResolver {
override fun resolve(mark: TaintMarkAccessor) {
addSideEffect(initialFact, TaintMarkFieldUnfoldRequest(method, initialFact, mark))
addSideEffect(initialFact, TaintMarkFieldUnfoldRequest(method, initialFact, mark, suffix = null))
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package org.opentaint.dataflow.taint

import org.opentaint.dataflow.ap.ifds.AccessPathBase
import org.opentaint.dataflow.ap.ifds.Accessor
import org.opentaint.dataflow.ap.ifds.MethodEntryPoint
import java.util.concurrent.ConcurrentHashMap

/**
* The accessors already demanded by answers to a [TaintMarkFieldUnfoldRequest].
*
* A request asks one question: is [mark] hidden under the `[any]` of this frame's initial fact?
* Answering it asks the frame to split that abstraction on the accessors the answerer found. The
* demand for a question therefore only grows, and an answer that contributes nothing new has not
* failed to answer -- it has repeated an answer already given.
*
* Contributing an accessor a second time is not a refinement: the split it asks for produces a
* fact that ends in `[any]` again, one accessor further down, which re-raises the same question.
* On a self-similar shape -- `CharSequence#content`, or `MapKey`/`MapValue`/`Element` over erased
* generics, where the type checker has nothing to stop on -- that loop has no fixed point, and
* each round re-abstracts the whole accumulated fact tree of the base and re-broadcasts a side
* effect requirement across the asking frame's transitive callers.
*
* One instance per analysed method, held on its
* [org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext]. A request climbs through many
* frames, and each frame it passes through filters against its own record -- so two frames are
* never made to agree about a question, and nothing here is shared across the analysis.
* `ConcurrentHashMap.newKeySet().add` is still the atomic step, so workers sharing one method's
* context cannot both be told the same accessor is fresh.
*
* Within a method the question is keyed on the ASKING frame (the one the request came from, not
* the one holding this map), the base the abstraction sits on, and the mark -- NOT on the
* current refinement of the fact, which is itself the product of earlier answers. That is where
* the precision goes: `arg0.*` and `arg0.x.*` are the same question here, so a frame already
* asked to split on `y` is not asked again below `x`, and a flow needing `arg0.x.y` is not
* reported. Keying on the refinement instead makes every round of the iteration its own question
* and the filter a no-op -- which is exactly the loop it is here to cut, so the conflation is
* load-bearing rather than an oversight.
*/
class MarkUnfoldDemand {
private data class Question(
/** The frame the request came from, which is not in general the frame holding this map. */
val method: MethodEntryPoint,
val base: AccessPathBase,
val mark: Accessor,
)

private val demanded = ConcurrentHashMap<Question, MutableSet<Accessor>>()

/**
* Records [accessors] as demanded for the question and returns those that were not demanded
* before. An empty result means this answer repeats one already given.
*/
fun demand(
method: MethodEntryPoint,
base: AccessPathBase,
mark: Accessor,
accessors: Collection<Accessor>,
): List<Accessor> {
if (accessors.isEmpty()) return emptyList()

val alreadyDemanded = demanded.computeIfAbsent(Question(method, base, mark)) {
ConcurrentHashMap.newKeySet()
}

return accessors.filter { alreadyDemanded.add(it) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,140 @@ import org.opentaint.dataflow.ap.ifds.Accessor
import org.opentaint.dataflow.ap.ifds.AnalysisRunner
import org.opentaint.dataflow.ap.ifds.AnyAccessor
import org.opentaint.dataflow.ap.ifds.ExclusionSet
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils
import org.opentaint.dataflow.ap.ifds.MethodEntryPoint
import org.opentaint.dataflow.ap.ifds.MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication
import org.opentaint.dataflow.ap.ifds.SideEffectKind
import org.opentaint.dataflow.ap.ifds.access.FinalFactAp
import org.opentaint.dataflow.ap.ifds.access.InitialFactAp
import org.opentaint.dataflow.ap.ifds.analysis.MethodAnalysisContext
import org.opentaint.dataflow.ap.ifds.analysis.MethodSequentFlowFunction
import org.opentaint.dataflow.ap.ifds.analysis.MethodSideEffectSummaryHandler

interface MethodSideEffectHandlerWithAnyAccessorRequestHandling : MethodSideEffectSummaryHandler {
val runner: AnalysisRunner

/** The method being analysed -- the frame these requests are arriving at, not the one that asked. */
val analysisContext: MethodAnalysisContext

override fun handleZeroToFact(
currentFactAp: FinalFactAp,
summaryEffect: MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication,
summaryEffect: SummaryEdgeApplication,
kind: SideEffectKind
): Set<MethodSequentFlowFunction.Sequent> {
if (kind is TaintMarkFieldUnfoldRequest) {
when (summaryEffect) {
is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryApRefinement -> {
if (!summaryEffect.delta.isEmpty) {
handleMarkAfterAnyFieldRequest(summaryEffect.delta, kind)
}
}
if (kind !is TaintMarkFieldUnfoldRequest) {
return super.handleZeroToFact(currentFactAp, summaryEffect, kind)
}

handleUnfoldRequest(summaryEffect, kind)
return emptySet()
}

is MethodSummaryEdgeApplicationUtils.SummaryEdgeApplication.SummaryExclusionRefinement -> {
// taint mark requested -> mark not in initial fact, delta is empty -> mark not in fact
override fun handleFactToFact(
methodEntryPoint: MethodEntryPoint,
currentInitialFactAp: InitialFactAp,
currentFactAp: FinalFactAp,
summaryEffect: SummaryEdgeApplication,
kind: SideEffectKind
): Set<MethodSequentFlowFunction.Sequent> {
if (kind !is TaintMarkFieldUnfoldRequest) {
return super.handleFactToFact(methodEntryPoint, currentInitialFactAp, currentFactAp, summaryEffect, kind)
}

if (handleUnfoldRequest(summaryEffect, kind)) {
return emptySet()
}

val nextRequests = kind.nextRequests(summaryEffect)
val ex = when (summaryEffect) {
is SummaryEdgeApplication.SummaryApRefinement -> ExclusionSet.Empty
is SummaryEdgeApplication.SummaryExclusionRefinement -> summaryEffect.exclusion
}
val fact = currentInitialFactAp.replaceExclusions(ex)
return nextRequests.mapTo(hashSetOf()) {
MethodSequentFlowFunction.Sequent.FactSideEffect(fact, it)
}
}

private fun TaintMarkFieldUnfoldRequest.nextRequests(
effect: SummaryEdgeApplication
): List<TaintMarkFieldUnfoldRequest> = when (effect) {
is SummaryEdgeApplication.SummaryExclusionRefinement -> listOf(this)
is SummaryEdgeApplication.SummaryApRefinement -> {
if (suffix != null || effect.delta.isEmpty) {
listOf(this)
} else {
effect.delta.startAccessors().map { copy(suffix = it) }
}
}
}

private fun handleUnfoldRequest(
summaryEffect: SummaryEdgeApplication,
request: TaintMarkFieldUnfoldRequest
): Boolean {
when (summaryEffect) {
is SummaryEdgeApplication.SummaryApRefinement -> {
if (!summaryEffect.delta.isEmpty) {
return handleMarkAfterAnyFieldRequest(summaryEffect.delta, request)
}
}

is SummaryEdgeApplication.SummaryExclusionRefinement -> {
// taint mark requested -> mark not in initial fact, delta is empty -> mark not in fact
}
}

return super.handleZeroToFact(currentFactAp, summaryEffect, kind)
return false
}

private fun handleMarkAfterAnyFieldRequest(
delta: FinalFactAp.Delta,
request: TaintMarkFieldUnfoldRequest
) {
): Boolean {
val mark = request.mark
val allAccessors = delta.getAllAccessors()
if (mark !in allAccessors) return
if (mark !in allAccessors) return false

val nextAccessors = request.suffix?.let { setOf(it) }
?: delta.relevantStartAccessors(mark)

// The demand for one question only grows. An accessor already demanded for it does not
// refine the abstraction a second time -- the split it asks for ends in `[any]` again,
// one accessor further down, and re-raises the same question. See [MarkUnfoldDemand].
val newAccessors = analysisContext.markUnfoldDemand.demand(
request.method, request.fact.base, mark, nextAccessors
)

// Nothing fresh: this answer asks for a split that has already been asked for. The
// request itself is NOT consumed -- it keeps climbing, because a caller further up may
// hold an accessor nobody has contributed yet, and consuming it here measurably stalls
// the analysis instead.
if (newAccessors.isEmpty()) return false

val exclusion = newAccessors.fold(ExclusionSet.Empty as ExclusionSet, ExclusionSet::add)
runner.manager.handleCrossUnitSideEffectReq(request.method, request.fact.replaceExclusions(exclusion))

return true
}

private fun FinalFactAp.Delta.startAccessors(): Set<Accessor> {
val startAccessors = hashSetOf<Accessor>()
for (accessor in delta.getStartAccessors()) {
for (accessor in getStartAccessors()) {
if (accessor !is AnyAccessor) {
startAccessors.add(accessor)
continue
}

val anySuccessors = delta.readAccessor(accessor)?.getStartAccessors()
val anySuccessors = readAccessor(accessor)?.getStartAccessors()
?: continue

anySuccessors.filterTo(startAccessors) { it !is AnyAccessor }
}
return startAccessors
}

val relevantStartAccessors = startAccessors.filter { accessor ->
accessor == mark || delta.readAccessor(accessor)?.getAllAccessors()?.contains(mark) ?: false
private fun FinalFactAp.Delta.relevantStartAccessors(mark: Accessor): List<Accessor> =
startAccessors().filter { accessor ->
accessor == mark || readAccessor(accessor)?.getAllAccessors()?.contains(mark) ?: false
}

if (relevantStartAccessors.isEmpty()) return

val exclusion = relevantStartAccessors.fold(ExclusionSet.Empty as ExclusionSet, ExclusionSet::add)
val sideEffectRequirement = request.fact.replaceExclusions(exclusion)
runner.manager.handleCrossUnitSideEffectReq(request.method, sideEffectRequirement)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,14 @@ abstract class TaintUtil<C, Src, Sink, Trace>(val apManager: ApManager) {

abstract fun handleReachedSink(rule: Sink, factReader: FinalFactReader?, evaluatedFacts: List<InitialFactAp>)

open fun patchSinkConditionFactReader(factReaders: List<FinalFactReader>): List<FactReader> = factReaders

fun applySinkRules(
sinkRules: List<RuleWithCondition<Sink>>,
factReader: FinalFactReader?,
markAfterAnyFieldResolver: FactWithMarkAfterAnyAccessorResolver?,
) {
if (sinkRules.isEmpty()) return

val normalConditionFactReaders = factReader?.let { conditionFact(it) }.orEmpty()
val conditionFactReaders = patchSinkConditionFactReader(normalConditionFactReaders)
val conditionFactReaders = factReader?.let { conditionFact(it) }.orEmpty()

sinkRules.applyRuleWithAssumptions(
apManager,
Expand All @@ -45,7 +42,7 @@ abstract class TaintUtil<C, Src, Sink, Trace>(val apManager: ApManager) {
return@applyRuleWithAssumptions
}

factReader?.updateRefinement(normalConditionFactReaders)
factReader?.updateRefinement(conditionFactReaders)
}


Expand Down
Loading
Loading