Menu

#66 JAGS 5.0.0-beta: LOGIC ERROR in basic_string::compare via dic.samples / pD=TRUE

v1.0_(example)
open
nobody
None
1
4 hours ago
2026-07-09
Yu-Sung Su
No

'm writing to report a reproducible bug in JAGS 5.0.0-beta that triggers a

libstdc++ assertion failure when computing DIC with pD=TRUE.

Error message

LOGIC ERROR:

basic_string::compare: __pos (which is 4294967295) > this->size() (which is 8)

Please send a bug report to martyn_plummer@users.sourceforge.net

How to reproduce

Any call path that invokes dic.samples() with type="pD" can trigger it.

Minimal R example:

library(rjags)

model <- jags.model(textConnection("model { x ~ dnorm(0,1) }"),

data = list(x = 0), n.chains = 2, quiet = TRUE)

update(model, 100)

dic.samples(model, n.iter = 100, type = "pD")

Also triggered via R2jags::jags(..., pD = TRUE).

The error is intermittent — it does not fire on every single run, but appears

reliably within a few attempts.  Observed with 2 and 3 chains on the 8-schools

model and on trivial single-node models.

Root cause

The bug is in src/modules/diag/DensityEnums.cc, in the functions isWeighted()

and isTotal().  Both use the std::string::compare(size_type pos, size_type n,

const char* s) overload with string::npos as an argument.  Under GCC 13.3.0

(libstdc++, Ubuntu 24.04), the npos value propagates into the __pos parameter

inside this overload, triggering the out_of_range assertion:

isWeighted (line 56):

return stat.compare(0, 4, "loo_") == 0;

// If stat.length() < 4, undefined behaviour in some implementations.

isTotal (lines 65–69):

unsigned long pos = stat.find_last_of("_");

if (pos == string::npos) return false;

return stat.compare(pos, string::npos, "_total") == 0;

// string::npos as the length argument can leak into __pos.

The second one is the primary culprit.  Even though find_last_of guards against

npos, the compare(pos, npos, "_total") call exhibits UB in some optimisation

levels where npos (the length argument) is mis-routed into the position parameter

of the internal implementation.

Proposed fix

Replace both fragile compare calls with safer alternatives:

isTotal — use rfind for the full suffix instead of find_last_of + compare:

string::size_type pos = stat.rfind("_total");

return pos != string::npos && pos + 6 == stat.length();

isWeighted — add a length guard:

if (stat.length() < 4) return false;

return stat.compare(0, 4, "loo_") == 0;

I have tested this fix against JAGS 5.0.0-beta by recompiling the diag module

and running 10 rounds of pD=TRUE + dic.samples() on the 8-schools model — all

passed with no errors.

Environment

JAGS:     5.0.0-beta (source: JAGS-5.0.0-beta.tar.gz)

rjags:    5-2

R:        4.6.0

OS:       Ubuntu 24.04.4 LTS (x86_64)

Compiler: GCC 13.3.0 (g++ -std=gnu++11)

libstdc++: 6.0.33

The fixed DensityEnums.cc is attached for reference.

Thank you for JAGS — it remains an indispensable tool.

Best regards,

Yu-Sung Su

1 Attachments

Related

Bug reports: #66

Discussion

  • Martyn Plummer

    Martyn Plummer - 4 days ago

    Thanks Yu-Sung. I am currently unable to reproduce the bug. Can you please described the compilation options you used?

     
    • Yu-Sung Su

      Yu-Sung Su - 1 day ago

      Dear Dr Plummer,

      I have attached the beta version of R2jags. You should be able to
      reproduce the error using the example in the jags.Rd file.

      Thanks!

      YS

      On Tue, Jul 21, 2026 at 2:50 AM Martyn Plummer martyn_plummer@users.sourceforge.net wrote:

      Thanks Yu-Sung. I am currently unable to reproduce the bug. Can you please
      described the compilation options you used?


      [bugs:#66] https://sourceforge.net/p/mcmc-jags/bugs/66/ JAGS
      5.0.0-beta: LOGIC ERROR in basic_string::compare via dic.samples / pD=TRUE

      Status: open
      Group: v1.0_(example)
      Created: Thu Jul 09, 2026 09:15 AM UTC by Yu-Sung Su
      Last Updated: Thu Jul 09, 2026 09:15 AM UTC
      Owner: nobody
      Attachments:

      'm writing to report a reproducible bug in JAGS 5.0.0-beta that triggers a

      libstdc++ assertion failure when computing DIC with pD=TRUE.
      Error message

      LOGIC ERROR:

      basic_string::compare: __pos (which is 4294967295) > this->size() (which is 8)

      Please send a bug report to martyn_plummer@users.sourceforge.net
      How to reproduce

      Any call path that invokes dic.samples() with type="pD" can trigger it.

      Minimal R example:

      library(rjags)

      model <- jags.model(textConnection("model { x ~ dnorm(0,1) }"),

      data = list(x = 0), n.chains = 2, quiet = TRUE)

      update(model, 100)

      dic.samples(model, n.iter = 100, type = "pD")

      Also triggered via R2jags::jags(..., pD = TRUE).

      The error is intermittent — it does not fire on every single run, but appears

      reliably within a few attempts. Observed with 2 and 3 chains on the 8-schools

      model and on trivial single-node models.
      Root cause

      The bug is in src/modules/diag/DensityEnums.cc, in the functions isWeighted()

      and isTotal(). Both use the std::string::compare(size_type pos, size_type n,

      const char* s) overload with string::npos as an argument. Under GCC 13.3.0

      (libstdc++, Ubuntu 24.04), the npos value propagates into the __pos parameter

      inside this overload, triggering the out_of_range assertion:

      isWeighted (line 56):

      return stat.compare(0, 4, "loo_") == 0;

      // If stat.length() < 4, undefined behaviour in some implementations.

      isTotal (lines 65–69):

      unsigned long pos = stat.find_last_of("_");

      if (pos == string::npos) return false;

      return stat.compare(pos, string::npos, "_total") == 0;

      // string::npos as the length argument can leak into __pos.

      The second one is the primary culprit. Even though find_last_of guards against

      npos, the compare(pos, npos, "_total") call exhibits UB in some optimisation

      levels where npos (the length argument) is mis-routed into the position parameter

      of the internal implementation.
      Proposed fix

      Replace both fragile compare calls with safer alternatives:

      isTotal — use rfind for the full suffix instead of find_last_of + compare:

      string::size_type pos = stat.rfind("_total");

      return pos != string::npos && pos + 6 == stat.length();

      isWeighted — add a length guard:

      if (stat.length() < 4) return false;

      return stat.compare(0, 4, "loo_") == 0;

      I have tested this fix against JAGS 5.0.0-beta by recompiling the diag module

      and running 10 rounds of pD=TRUE + dic.samples() on the 8-schools model — all

      passed with no errors.
      Environment

      JAGS: 5.0.0-beta (source: JAGS-5.0.0-beta.tar.gz)

      rjags: 5-2

      R: 4.6.0

      OS: Ubuntu 24.04.4 LTS (x86_64)

      Compiler: GCC 13.3.0 (g++ -std=gnu++11)

      libstdc++: 6.0.33

      The fixed DensityEnums.cc is attached for reference.

      Thank you for JAGS — it remains an indispensable tool.

      Best regards,

      Yu-Sung Su

      Sent from sourceforge.net because you indicated interest in
      https://sourceforge.net/p/mcmc-jags/bugs/66/

      To unsubscribe from further messages, please visit
      https://sourceforge.net/auth/subscriptions/

      --
      Yu-Sung Su

       

      Related

      Bug reports: #66

      • Martyn Plummer

        Martyn Plummer - 4 hours ago

        I'm sorry but I cannot reproduce this. I have both Ubuntu 24.4 and Fedora 44. I have run the 8 schools example and a minimal example through an instrumentalized version of JAGS 5.0.0 beta (CXX="g++ -fsanitize=undefined,address,bounds") and can find no issues.

         
  • Martyn Plummer

    Martyn Plummer - 3 days ago

    This seems to be a compiler bug which I can't reproduce. I agree that this code could be strengthened so I have committed a fix which avoids the casual use of string::npos and includes length checks:

    //starts with "loo_"
    return stat.length() >= 4 && stat.compare(0, 4, "loo_") == 0;
    

    and

     //ends with "_total"
    return stat.length() >= 6 && 
       stat.compare(stat.length() - 6, 6, "_total") == 0;
    
     
    • Martyn Plummer

      Martyn Plummer - 3 days ago

      C++20 has basic_string::starts_with and basic_string::ends_with but I would rather stick with C++11 for now.

       

Anonymous
Anonymous

Add attachments
Cancel





Monday.com Logo