Skip to content

A state with no entities evaluates to unknown instead of true, silently voiding every Ubuntu USN result #2396

Description

@pw201

This is an issue with the scanner itself, not with SCAP Workbench, SCAP Security Guide or the Anaconda
addon. It concerns how oscap evaluates a state, not the content being evaluated.

Description of Problem:

When an OVAL state contains no entities, eval_item() records no results for it and
ores_get_result_byopr() maps the empty result set to OVAL_RESULT_UNKNOWN. A state which imposes no
conditions is satisfied by any item, so this should be true.

This is not a theoretical edge case. Canonical's Ubuntu OVAL feeds now ship the singleton
family_state with no entities, so the family test in every Ubuntu inventory definition returns
unknown. Because each USN definition ANDs an applicability check against that inventory definition,
no USN definition can ever evaluate to true: those which should be true become unknown, and the
scan reports no vulnerabilities at all while exiting 0.

The result is a silent false negative for anyone scanning Ubuntu with the feeds from
https://security-metadata.canonical.com/oval/. I have reported the feed change to Canonical as well
(https://bugs.launchpad.net/ubuntu-cve-tracker/+bug/2161980, filed independently by another reporter),
but the handling of an entity-less state looks wrong regardless of what they do, and no released
OpenSCAP version behaves differently.

OpenSCAP Version:

Reproduced on 1.4.4, built from the release tarball.

Operating System & Version:

Scanner running on Ubuntu 22.04 (jammy), x86_64.

Targets are mounted Ubuntu 18.04 (bionic) and 22.04 (jammy) disk images, scanned offline via
OSCAP_PROBE_ROOT. The reproducer below needs no target system at all.

Steps to Reproduce:

  1. Save this as empty-state-repro.oval.xml. def:1 uses an entity-less family_state; def:2 is
    the identical test against a state which names the family, as a control.
<?xml version="1.0" encoding="UTF-8"?>
<oval_definitions xmlns="http://oval.mitre.org/XMLSchema/oval-definitions-5"
 xmlns:oval="http://oval.mitre.org/XMLSchema/oval-common-5"
 xmlns:ind="http://oval.mitre.org/XMLSchema/oval-definitions-5#independent"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <generator>
    <oval:product_name>empty family_state reproducer</oval:product_name>
    <oval:schema_version>5.11.1</oval:schema_version>
    <oval:timestamp>2026-07-28T00:00:00</oval:timestamp>
  </generator>
  <definitions>
    <definition class="inventory" id="oval:repro:def:1" version="1">
      <metadata>
        <title>family_state with no entities</title>
        <description>Should be true: a state with no entities imposes no conditions.</description>
      </metadata>
      <criteria>
        <criterion test_ref="oval:repro:tst:1" comment="family test against an entity-less state"/>
      </criteria>
    </definition>
    <definition class="inventory" id="oval:repro:def:2" version="1">
      <metadata>
        <title>family_state with one entity (control)</title>
        <description>Control case: the same test against a state which names the family.</description>
      </metadata>
      <criteria>
        <criterion test_ref="oval:repro:tst:2" comment="family test against a populated state"/>
      </criteria>
    </definition>
  </definitions>
  <tests>
    <ind:family_test id="oval:repro:tst:1" version="1" check="at least one"
                     check_existence="at_least_one_exists" comment="entity-less state">
      <ind:object object_ref="oval:repro:obj:1"/>
      <ind:state state_ref="oval:repro:ste:1"/>
    </ind:family_test>
    <ind:family_test id="oval:repro:tst:2" version="1" check="at least one"
                     check_existence="at_least_one_exists" comment="populated state">
      <ind:object object_ref="oval:repro:obj:1"/>
      <ind:state state_ref="oval:repro:ste:2"/>
    </ind:family_test>
  </tests>
  <objects>
    <ind:family_object id="oval:repro:obj:1" version="1" comment="The singleton family object"/>
  </objects>
  <states>
    <ind:family_state id="oval:repro:ste:1" version="1" comment="no entities"/>
    <ind:family_state id="oval:repro:ste:2" version="1" comment="control">
      <ind:family>unix</ind:family>
    </ind:family_state>
  </states>
</oval_definitions>
  1. Confirm the document is valid, which it is. oscap oval validate runs Schematron by default in
    1.4.4 and exits 0 with no findings:
oscap oval validate empty-state-repro.oval.xml
  1. Evaluate it. No probe root or privileges are needed:
oscap oval eval empty-state-repro.oval.xml

Actual Results:

Definition oval:repro:def:2: true
Definition oval:repro:def:1: unknown
Evaluation done.

Expected Results:

Both definitions true.

Additional Information / Debugging Steps:

Cause. In src/OVAL/results/oval_resultTest.c, eval_item() iterates the state's contents and
accumulates one result per entity:

    ores_clear(&ste_ores);

    state_contents_itr = oval_state_get_contents(state);
    while (oval_state_content_iterator_has_more(state_contents_itr)) {
        ...
        ores_add_res(&ste_ores, ste_ent_res);
    }
    ...
    operator = oval_state_get_operator(state);
    result = ores_get_result_byopr(&ste_ores, operator);

With no entities the loop body never executes, so every counter stays at zero and
ores_get_result_byopr() returns before the operator is considered:

    if (ores->true_cnt == 0 &&
        ores->false_cnt == 0 &&
        ores->error_cnt == 0 &&
        ores->unknown_cnt == 0 &&
        ores->notappl_cnt == 0 &&
        ores->noteval_cnt == 0)
            return OVAL_RESULT_UNKNOWN;

"Nothing to evaluate" is being conflated with "cannot determine the answer".

Why true is the correct result. The OVAL 5.11.1 schema documentation for StateType says:

When evaluating a particular state against an object, one should evaluate each individual entity
separately. The individual results are then combined by the operator to produce an overall result.

The state operator defaults to AND, and an AND over an empty set is vacuously true. The schema
permits the empty case explicitly (family_state declares its entity minOccurs="0"), and no
Schematron rule requires a state to carry entities, so content like this is legal and an implementation
has to decide what it means. unknown is the one answer that makes the document unusable.

Impact in practice. Ubuntu USN definitions have the shape:

definition (class="patch")
+- criteria (AND)
   +- extend_definition -> inventory def:100, applicability_check="true"
   |  +- criteria (AND)
   |     +- criterion tst:100  family_test        <-- unknown
   |     +- criterion tst:101  textfilecontent54  <-- true on a real Ubuntu system
   +- criterion tst:NNN  dpkginfo_test            <-- the actual version comparison

Given AND semantics where false dominates and unknown blocks true:

  • def:100 becomes AND(unknown, true) = unknown
  • a USN definition whose package test is false stays false (the unknown is masked)
  • a USN definition whose package test is true becomes unknown

so a vulnerable system reports nothing. Measured on one Ubuntu 18.04 image, same binary, same package
set, only the feed differing:

Feed family_test true false unknown exit
generated 2024-10-29, with <family>unix</family> true 318 1980 0 0
current, entity-less state unknown 0 2560 393 0

The 393 unknown definitions are exactly the vulnerabilities that should have been reported: 406 USNs
and 1870 CVEs on that image became zero. Note the exit code is 0 in both cases, and the HTML report
shows a fail count of 0 alongside an unknown count of 393, so nothing signals that the scan found
nothing because it could not evaluate anything.

Suggested fix. Return true when the state has no contents, leaving ores_get_result_byopr() alone
since it is shared with criteria combination:

    state_contents_itr = oval_state_get_contents(state);
    if (!oval_state_content_iterator_has_more(state_contents_itr)) {
        /* A state with no entities places no conditions on the item, so every
         * item satisfies it. */
        oval_state_content_iterator_free(state_contents_itr);
        return OVAL_RESULT_TRUE;
    }
    while (oval_state_content_iterator_has_more(state_contents_itr)) {

With this applied, def:1 above evaluates to true, and the Ubuntu bionic feed goes from 0 true / 393
unknown back to 393 true / 0 unknown.

Note this deliberately does not make the test pass when nothing was collected. The guard is inside the
per-item comparison, which only runs when an item exists, so check_existence still decides that case.
Verified with an entity-less dpkginfo_state against an absent package: the test is false, with no
tested items, while the same state against an installed package gives true.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions