Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d76b6f9
feat(rules): express Spring whole-object source and sink taint via th…
misonijnik Jul 23, 2026
c4e95a4
refactor(rules): field-sensitive java.io.File model and $*VAR syntax
misonijnik Jul 23, 2026
0bb5f51
test(querylang): coverage for the changed passthrough config entries
misonijnik Jul 23, 2026
5d8674b
test(querylang): java.nio buffer passthrough coverage before the rule…
misonijnik Jul 23, 2026
09e2a17
refactor(config): split NameClassPair name/className/nameInNamespace
misonijnik Jul 23, 2026
42ac7f2
test(e2e): behavioural coverage for the 9 rule-storage cleanup fixes
misonijnik Jul 23, 2026
66f14cf
fix(config): close BasicControl#getID whole-object leak (star ctrlSink)
misonijnik Jul 23, 2026
e5cd46c
test(phase3): probe DateFormatSymbols generic set./get. whole-object …
misonijnik Jul 23, 2026
7865979
fix(config): close DateFormatSymbols set./get. whole-object leak
misonijnik Jul 23, 2026
48d460f
test(config): pin taint isolation for 8 more split bean classes
misonijnik Jul 23, 2026
0e857d7
test(config): reframe ScriptContext key-insensitivity as accepted, cl…
misonijnik Jul 23, 2026
389f6af
test(querylang): pin that a starred source reaches a field-sensitive …
misonijnik Jul 24, 2026
bd60e0c
fix(analyzer): Field based default get
Saloed Aug 11, 2026
a252311
refactor(dataflow): drop the <rule-storage> unroll exception
misonijnik Aug 12, 2026
e54f79d
fix(dataflow): apply the default get model only when no rule matched
misonijnik Aug 12, 2026
9b96ae2
refactor(dataflow): delete the String bytes clean special case
misonijnik Aug 13, 2026
33c0d46
fix(querylang): honour focus-metavariable on sanitizers
misonijnik Aug 19, 2026
6219b74
fix(dataflow): answer the field-unfold request on fact-to-fact edges
misonijnik Aug 19, 2026
550b9bd
refactor(dataflow): delete the array-element mechanism
misonijnik Aug 27, 2026
960cc12
test(dataflow): pin that only the star gives element taint
misonijnik Aug 30, 2026
2e5482e
test(dataflow): cover interprocedural any-field unfolding
Saloed Sep 14, 2026
3cfe8a8
reduce mark request handling workload
Saloed Sep 14, 2026
422ebe9
reduce mark request handling workload 2.0
Saloed Sep 15, 2026
002ef48
reduce mark request handling workload 3.0
Saloed Sep 16, 2026
f676961
Fix exclusion
Saloed Sep 17, 2026
eeaed05
refactor(dataflow): hold the unfold demand per analysed method
Saloed Sep 19, 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