Skip to content
4 changes: 2 additions & 2 deletions .github/workflows/r.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,13 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Set up R
uses: r-lib/actions/setup-r@d3c5be51b12e724e68f33216ca3c148b66d5f0b6
uses: r-lib/actions/setup-r@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590
with:
r-version: release
use-public-rspm: true

- name: Set up R package dependencies
uses: r-lib/actions/setup-r-dependencies@d3c5be51b12e724e68f33216ca3c148b66d5f0b6
uses: r-lib/actions/setup-r-dependencies@6f6e5bc62fba3a704f74e7ad7ef7676c5c6a2590
with:
extra-packages: any::rcmdcheck
needs: check
Expand Down
4 changes: 4 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@
5. DoS μ™„ν™”λ₯Ό μœ„ν•΄ `return(1L)` 같은 κΈ°λ³Έ μŠΉμΈκ°’μ„ 넣을 λ•ŒλŠ” μΆ”μ • 기쀀척도, anchor/common item, true parameter μž¬ν˜„ 계약을 μš°νšŒν•˜μ§€ μ•ŠλŠ”μ§€ λ¨Όμ € κ²€μ¦ν•©λ‹ˆλ‹€.
6. Fail-secure μ—λŸ¬ λ©”μ‹œμ§€λŠ” ν…ŒμŠ€νŠΈμ˜ μΌλΆ€λ‘œ μ·¨κΈ‰ν•©λ‹ˆλ‹€. λ³΄μ•ˆ ν…ŒμŠ€νŠΈλŠ” μ‹€μ œ κ΅¬ν˜„ λ©”μ‹œμ§€μ™€ λ§žμ•„μ•Ό ν•˜λ©°, 였래된 `"Interactive prompt is not available"` 같은 별도 문ꡬλ₯Ό μƒˆλ‘œ λ§Œλ“€μ§€ μ•ŠμŠ΅λ‹ˆλ‹€.
7. Prompt DoS νšŒκ·€ ν…ŒμŠ€νŠΈλŠ” λͺ¨λΈ μΆ”μ • μ‹€νŒ¨μ— κΈ°λŒ€μ§€ 말고, common-item confirmation guard처럼 μ·¨μ•½ν•œ μž…λ ₯ κ²½κ³„μ—μ„œ λ°”λ‘œ λ°œμƒν•˜λŠ” fail-secure μ—λŸ¬λ₯Ό κ²€μ¦ν•©λ‹ˆλ‹€.
## 2024-06-30 - [Infinite Loop DoS Risk in Model Estimation]
**Vulnerability:** Unconstrained `while (!exists('model'))` loops used to retry model estimation upon failure could result in infinite loops and Denial of Service (DoS) if the failure is deterministic.
**Learning:** Legacy R code often relies on continuous retries. In automated environments, these unconstrained loops will hang indefinitely.
**Prevention:** Always replace unconstrained `while` loops intended for retries with bounded `for` loops (e.g., maximum 3 attempts) or timeout limits.
34 changes: 24 additions & 10 deletions R/aFIPC.R
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,15 @@ autoFIPC <-

if (tryFitwholeOldItems == T) {
if (
!oldFormModel@OptimInfo$secondordertest &&
(!exists('oldFormModel', envir = environment(), inherits = FALSE) ||
!oldFormModel@OptimInfo$secondordertest) &&
!itemtype == 'ideal'
) {
message(
'Estimation failed. estimating new parameters with no prior distribution using quasi-Monte Carlo EM estimation. please be patient.'
)

try(rm(oldFormModel))
try(rm(list = 'oldFormModel', envir = environment(), inherits = FALSE), silent = TRUE)
try(
oldFormModel <-
mirt::mirt(
Expand All @@ -208,15 +209,16 @@ autoFIPC <-
}

if (
!oldFormModel@OptimInfo$secondordertest &&
(!exists('oldFormModel', envir = environment(), inherits = FALSE) ||
!oldFormModel@OptimInfo$secondordertest) &&
!itemtype == 'ideal'
) {
message(
'Estimation failed. estimating new parameters with no prior distribution using Cai\'s (2010) Metropolis-Hastings Robbins-Monro (MHRM) algorithm. please be patient.'
)

try(rm(oldFormModel))
while (!exists('oldFormModel')) {
try(rm(list = 'oldFormModel', envir = environment(), inherits = FALSE), silent = TRUE)
for (attempt in seq_len(3)) {
try(
oldFormModel <-
mirt::mirt(
Comment on lines +221 to 224
Expand All @@ -230,11 +232,16 @@ autoFIPC <-
GenRandomPars = F
)
)
if (exists('oldFormModel', envir = environment(), inherits = FALSE)) break
}
if (!exists('oldFormModel', envir = environment(), inherits = FALSE)) {
stop("Estimation failed repeatedly for oldFormModel. Exiting.")
}
}
}

if (
exists('oldFormModel', envir = environment(), inherits = FALSE) &&
!oldFormModel@OptimInfo$secondordertest &&
!itemtype == 'ideal'
) {
Expand Down Expand Up @@ -396,14 +403,15 @@ autoFIPC <-

if (tryFitwholeNewItems) {
if (
!newFormModel@OptimInfo$secondordertest &&
(!exists('newFormModel', envir = environment(), inherits = FALSE) ||
!newFormModel@OptimInfo$secondordertest) &&
!itemtype == 'ideal'
) {
message(
'Estimation failed. estimating new parameters with no prior distribution using quasi-Monte Carlo EM estimation. please be patient.'
)

try(rm(newFormModel))
try(rm(list = 'newFormModel', envir = environment(), inherits = FALSE), silent = TRUE)
try(
newFormModel <-
mirt::mirt(
Expand All @@ -420,15 +428,16 @@ autoFIPC <-
}

if (
!newFormModel@OptimInfo$secondordertest &&
(!exists('newFormModel', envir = environment(), inherits = FALSE) ||
!newFormModel@OptimInfo$secondordertest) &&
!itemtype == 'ideal'
) {
message(
'Estimation failed. estimating new parameters with no prior distribution using Cai\'s (2010) Metropolis-Hastings Robbins-Monro (MHRM) algorithm. please be patient.'
)

try(rm(newFormModel))
while (!exists('newFormModel')) {
try(rm(list = 'newFormModel', envir = environment(), inherits = FALSE), silent = TRUE)
for (attempt in seq_len(3)) {
try(
newFormModel <-
mirt::mirt(
Expand All @@ -442,11 +451,16 @@ autoFIPC <-
GenRandomPars = F
)
)
if (exists('newFormModel', envir = environment(), inherits = FALSE)) break
}
if (!exists('newFormModel', envir = environment(), inherits = FALSE)) {
stop("Estimation failed repeatedly for newFormModel. Exiting.")
}
}
}

if (
exists('newFormModel', envir = environment(), inherits = FALSE) &&
!newFormModel@OptimInfo$secondordertest &&
!itemtype == 'ideal'
) {
Expand Down
31 changes: 31 additions & 0 deletions tests/testthat/test-model-retries.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
test_that("model estimation fails cleanly after max retries", {
skip_if_not_installed("mirt")

set.seed(20260630)
old_item_names <- paste0("old_item_", 1:6)
new_item_names <- paste0("new_item_", 1:6)
old_common_items <- old_item_names[1:4]
new_common_items <- new_item_names[1:4]

# Forcing an error in mirt by passing non-sensical data shapes
old_data <- data.frame(a = c("A", "B"), b = c("C", "D"))
new_data <- data.frame(c = c("E", "F"), d = c("G", "H"))
names(old_data) <- old_item_names[1:2]
names(new_data) <- new_item_names[1:2]

expect_error(
aFIPC::autoFIPC(
newformXData = new_data,
oldformYData = old_data,
newformCommonItemNames = new_common_items[1:2],
oldformCommonItemNames = old_common_items[1:2],
itemtype = "2PL",
checkIPD = FALSE,
tryEM = FALSE,
freeMEAN = FALSE,
forceNormalZeroOne = TRUE,
confirmCommonItems = TRUE
),
"Estimation failed repeatedly"
)
})
Loading