Menu

#306 Line-oriented and -L query results are lossy on larger files: dropped rows, duplicated caller attributions, and drifted line numbers (cscope 15.9)

open
nobody
None
5
2026-07-20
2026-07-19
Fox Chen
No

While building a tool that consumes cscope's cross-reference database, we
differential-tested cscope's own query engine (-L / line-oriented mode)
against a direct parse of cscope.out. The crossref database itself is
consistently correct; the query side shows three reproducible bug
classes on larger real-world files. Verified on cscope 15.9 built from
vanilla upstream source (the Homebrew build on macOS/arm64 — the formula
applies no patches, source is the SourceForge cscope-15.9.tar.gz);
databases built with -bkR and -bckR (both the compressed and the
uncompressed -c format are affected, interestingly dropping different
subsets — see class 2).

All examples below use the wpa_supplicant source tree (any recent
checkout reproduces).

Class 1: duplicated caller attribution on multi-line definitions

cscope -bckR -f cs.out
cscope -d -f cs.out -L3 radius_msg_get_attr_ptr

For call sites inside:

static RadiusRxResult
radius_das_disconnect(struct radius_das_data *das, ...)

the same call site is reported twice, once with the caller
radius_das_disconnect (correct) and once with the caller
RadiusRxResult (the return type). The crossref contains a single,
correct $radius_das_disconnect function mark — the duplication happens
in the query engine's caller derivation.

Class 2: dropped result rows

src/radius/radius_das.c contains 8 call sites of
radius_msg_get_attr_ptr (crossref records at lines 76, 88, 100, 106,
121, 127, 133, 139 — verifiable by inspecting cscope.out directly).
-L3 radius_msg_get_attr_ptr returns only 5 of them against the
compressed database, and a different subset against the uncompressed
(-c) database. Lines 100 and 106 are dropped in both modes.

Class 3: line-number / file drift

cscope -bckR -f cs.out
cscope -d -f cs.out -L3 fst_group_get_id | grep fst_internal

reports a hit at src/fst/fst_internal.h:1255 — that header is only
49 lines long; the real site is fst_session.c:1255 (the adjacent
compilation unit which #includes the header). Reproduces on a fresh
database every time in our testing.

A second variant: against the default (compressed) database,
-L3 os_free reports a hit at crypto_internal.c:223 — line 223 is
blank in the source, the crossref has no record for line 223, and the
actual call is at line 217 (also reported). Incidentally the same query
illustrates class 2 at larger magnitude: the uncompressed database
returns 10 rows for this file, the compressed one 32.

Notes

Discussion

  • Hans-Bernhard Broeker

    Hmmm... checked out git current source straight from their original upstream.

    Class 1:
    No two-line function definition for radius_das_disconnect matching your description seems to exist in there. The definition has looked like this, for the last 14 years and counting:

    static struct radius_msg * radius_das_disconnect(struct radius_das_data *das,
                                                     struct radius_msg *msg,
                                                     const char *abuf,
                                                     int from_port)
    

    What gives?

    The issue itself does exist, and is most likely an instance of the well-known and documented limitation about function pointers (like those in the definicition of struct radius_das_data) throwing cscope off-track.

    Class2:
    The number of actual call sites for radius_msg_get_attr_ptr in radius_das.c is not 8, but 17. cscope finds all of them (some twice), with no difference between default and -c modes, when run from the top-level or 'src' directory. Only when run from the 'src/radius' directory itself does it drop to 8 call sites located.

    Class 3:
    Completely fails to reproduce here.

     
  • Fox Chen

    Fox Chen - 2026-07-20

    Thanks — your function-pointer instinct was exactly right, so we chased
    it into the scanner and have a root cause, a 6-line repro, and a
    candidate patch
    .

    6-line self-contained repro (no wpa_supplicant needed)

    /* a.h */
    void register_handler(RxResult (*handler)(void), void *data);
    
    /* b.c */
    static int dispatch(void)
    {
        probe_call();
        return 0;
    }
    
    $ cscope -bk a.h b.c -f cs.out
    $ cscope -d -f cs.out -L3 probe_call
    b.c RxResult 3 probe_call();      <-- phantom caller, from a DIFFERENT file
    b.c dispatch 3 probe_call();      <-- correct
    

    Root cause (fscanner.l)

    cscope -d -f cs.out -L1 RxResult shows it: the lexer records the
    return type of a function-pointer parameter as a function
    definition
    . In the <WAS_IDENTIFIER> rule, a declarator of shape
    T (*f)(args) matches the "a function definition" pattern — the
    identifier T is followed by (, and the if (braces == 0 ...) test
    passes. That phantom function has no body, so fcndef/braces leave
    its scope open; it then swallows the FCNCALL attribution of every
    subsequent file, producing the cross-file duplicate callers.

    This is precisely the case the FIXME HBB 20001003 comment right above
    that rule anticipated ("the parsing bug concerning function pointer
    usage").

    Candidate patch (attached: cscope-306-fscanner.patch)

    Adds a shape guard in that rule: if the text after the identifier is
    ( * ... ) ( — i.e. a function-pointer declarator — treat it as a
    function call/use, not a definition (goto fcncal). Verified:

    • the 6-line repro is fully clean after the patch (no phantom RxResult
      definition, no duplicate caller, -L1 RxResult empty);
    • no regression on ordinary defs: int f(int a){...} and its calls are
      still detected; a genuine fn-ptr-returning definition
      int (*g(int x))(void){...} is unaffected.

    Verified on the real wpa tree (stock vs patched): all three
    originally-reported classes trace to this one phantom scope and are
    resolved by the single patch —

    symptom stock patched
    duplicate RadiusRxResult caller on radius_das.c sites 5 0
    correct sites returned (of 8) 5/8 8/8
    phantom fst_group_get_idfst_internal.h:1255 (49-line file) present gone
    $RadiusRxResult phantom marks in the crossref 2 0

    The multi-line form in radius_client.h (RadiusRxResult (*handler)
    with the arg parens on the next line) is covered too — regress-306.sh
    includes it as case 1b.

    Your three points

    1. radius_das_disconnect unchanged — agreed; we never claimed the
      source changed, only the query output, and the repro above removes
      the tree dependency entirely.
    2. 17 vs 8 — 17 is the whole-tree count; our 8 was the in-file count
      for src/radius/radius_das.c. Same numbers, different scope.
    3. cannot reproduce class 3 — our platform is macOS/arm64, cscope
      15.9 from the vanilla SourceForge tarball (Homebrew applies no
      patches). The 6-line repro above should be deterministic
      cross-platform; if it isn't for you, that itself is useful data.

    Classes 2 and 3 (dropped/drifted rows in radius_das.c) turned out to be
    downstream of exactly this same phantom scope — the table above shows
    them clearing together with class 1 under the one patch, which we take
    as confirmation of the shared root cause.

    Regression harness

    Since 15.9 ships no test suite (make check is a no-op), the patch
    comes with a small standalone regression script (attached:
    regress-306.sh) covering three cases: the bug (fn-ptr param must not
    create a phantom def), an ordinary function def+call must still be
    detected, and a genuine fn-ptr-returning definition must still be a
    def. Against stock 15.9 it reports case 1 FAIL; against the patched
    build, ALL PASS. Usage: ./regress-306.sh /path/to/cscope.

     

Log in to post a comment.