Skip to content

[R-pkg-devel] Subset assignment rule changes in R-devel

8 messages · Konrad Rudolph, Dirk Eddelbuettel, Simon Urbanek +2 more

#
Hi all,

One of my packages is failing on CRAN in R-devel [1], and I was requested
to fix it. However, it is *only* failing on one specific configuration,
'r-devel-linux-x86_64-debian-gcc'. All other combinations ? clang on
Debian, both clang and GCC on Fedora, and Windows ? keep running just fine.
As my package is not using compiled code or anything OS-specific, I am at a
loss to explain this highly specific failure. Before attempting to build a
container image with this specific configuration locally (? are these
configurations available as ready-made images?), I wanted to check if there
was an obvious change in R-devel which might explain the issue.

There are two failures, both with the same error message: ?cannot change
value of locked binding for '*tmp*'?. My package?s code isn?t attempting to
directly change `*tmp*`, but it is using eval() to perform subset
assignment to a complex expression inside an active binding. Here?s a
minimal code snippet that *should* be equivalent to one of the two
failures, and which should therefore also fail (in the last line):

   x = data.frame(a = 1 : 2, b = c('a', 'b'))

   f = function (value) {
     if (missing(value)) {
       evalq(x[1L, ], .GlobalEnv)
     } else {
       assign_expr = substitute(x[1L, ] <- value, list(value = value))
       eval(assign_expr, .GlobalEnv)
     }
   }

   makeActiveBinding('ax', f, .GlobalEnv)

   ax[1L] = 3L

I had a look at the changes in in R-devel, but I couldn?t find anything
obviously relevant. In particular, the code of R_MakeActiveBinding() hasn?t
been touched in literally decades, and similar for the code that (as far as
I understand) performs subset assignment, applydefine().

Does anybody have an idea what might be going on here, or how to debug this
issue?

Cheers,
Konrad

[1] https://cran.r-project.org/web/checks/check_results_aka.html
#
On 14 December 2025 at 22:50, Konrad Rudolph wrote:
| One of my packages is failing on CRAN in R-devel [1], and I was requested
| to fix it. However, it is *only* failing on one specific configuration,
| 'r-devel-linux-x86_64-debian-gcc'. All other combinations ? clang on
| Debian, both clang and GCC on Fedora, and Windows ? keep running just fine.
| As my package is not using compiled code or anything OS-specific, I am at a
| loss to explain this highly specific failure. Before attempting to build a
| container image with this specific configuration locally (? are these
| configurations available as ready-made images?), I wanted to check if there
| was an obvious change in R-devel which might explain the issue.

My rocker/drd (for "daily R-devel"; but it now rebuilds weekly) fits that
bill. Access R-devel inside as either Rdevel or via alias RD.
 
| There are two failures, both with the same error message: ?cannot change
| value of locked binding for '*tmp*'?. My package?s code isn?t attempting to
| directly change `*tmp*`, but it is using eval() to perform subset
| assignment to a complex expression inside an active binding. Here?s a
| minimal code snippet that *should* be equivalent to one of the two
| failures, and which should therefore also fail (in the last line):
| 
|    x = data.frame(a = 1 : 2, b = c('a', 'b'))
| 
|    f = function (value) {
|      if (missing(value)) {
|        evalq(x[1L, ], .GlobalEnv)
|      } else {
|        assign_expr = substitute(x[1L, ] <- value, list(value = value))
|        eval(assign_expr, .GlobalEnv)
|      }
|    }
| 
|    makeActiveBinding('ax', f, .GlobalEnv)
| 
|    ax[1L] = 3L
| 
| I had a look at the changes in in R-devel, but I couldn?t find anything
| obviously relevant. In particular, the code of R_MakeActiveBinding() hasn?t
| been touched in literally decades, and similar for the code that (as far as
| I understand) performs subset assignment, applydefine().
| 
| Does anybody have an idea what might be going on here, or how to debug this
| issue?

It does fail for me under that drd container [1], and as you noted, not in my
usual R (4.5.2) on Ubuntu. As for the error, I have no idea...

Dirk

[1] Your example:
if (missing(value)) {
       evalq(x[1L, ], .GlobalEnv)
     } else {
       assign_expr = substitute(x[1L, ] <- value, list(value = value))
       eval(assign_expr, .GlobalEnv)
     }
   }
Error in ax[1L] = 3L : cannot change value of locked binding for '*tmp*'

  
    
#
Konrad,

I can reproduce this is current R-devel on any platform, so this should be very easy to reproduce, it is not OS-specific at all - all R-devel checks will flag it eventually. I would guess this is from:

r89121 | luke | 2025-12-09 04:28:24 +1300 (Tue, 09 Dec 2025) | 3 lines

Mark values returned by active binding functions as not mutable to
prevent unintended mutation in complex assignments.

So it looks like it is intentional. May need some discussion on whether this requires some re-design of your package to make it safer or if it is a valid use-cases that may need further consideration.

Cheers,
Simon
#
I received the following valgrind report that looks possibly relevant:

at or around 2025-12-14 12:36:00 UTC

[1] "R Under development (unstable) (2025-08-08 r88534)"

... < my package setup >

test_queue_overflow_adversarial.R    8 tests OK ==1713== Warning: set
address range perms: large range [0x32ed0028, 0x43cf1fc8) (noaccess)
test_queue_overflow_adversarial.R   32 tests OK 2.4s
All ok, 127 results (2m 38.0s)
==1713==
==1713== HEAP SUMMARY:
==1713==     in use at exit: 111,563,401 bytes in 15,822 blocks
==1713==   total heap usage: 1,472,538 allocs, 1,456,716 frees,
4,581,974,138 bytes allocated
==1713==
==1713== 2,688 bytes in 8 blocks are possibly lost in loss record 267 of 1,370
==1713==    at 0x484DA83: calloc (in
/usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1713==    by 0x40147D9: calloc (rtld-malloc.h:44)
==1713==    by 0x40147D9: allocate_dtv (dl-tls.c:375)
==1713==    by 0x40147D9: _dl_allocate_tls (dl-tls.c:634)
==1713==    by 0x4DAD7B4: allocate_stack (allocatestack.c:430)
==1713==    by 0x4DAD7B4: pthread_create@@GLIBC_2.34 (pthread_create.c:647)
==1713==    by 0x580A25F: ??? (in /usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0)
==1713==    by 0x5800A10: GOMP_parallel (in
/usr/lib/x86_64-linux-gnu/libgomp.so.1.0.0)
==1713==    by 0x114BC738: convertNegAndZeroIdx (subset.c:150)
==1713==    by 0x495BAC5: R_doDotCall (dotcode.c:763)
==1713==    by 0x49CFE24: bcEval_loop (eval.c:8668)
==1713==    by 0x49B8BA1: bcEval (eval.c:7501)
==1713==    by 0x49A8C4B: Rf_eval (eval.c:1167)
==1713==    by 0x49ABC8F: R_execClosure (eval.c:2393)
==1713==    by 0x49AB8BB: applyClosure_core (eval.c:2306)
On Mon, 15 Dec 2025 at 09:49, Dirk Eddelbuettel <edd at debian.org> wrote:
#
On Sun, 14 Dec 2025, Simon Urbanek wrote:

            
Looks like it is a side effect of that bug fix that it is waking up a
misfeature of the interpreted complex assignment code. The compiled
version of the complex assignment code is cleaner and does not have
this issue, so you could use

f <- function (value) {
     if (missing(value)) {
         evalq(x[1L, ], .GlobalEnv)
     } else {
         assign_expr <- substitute(x[1L, ] <- value, list(value = value))
         cmp_assign_expr <- compiler::compile(assign_expr)
         eval(cmp_assign_expr, .GlobalEnv)
     }
}

Depending on how close this is to what you are really doing Konrad,
you can also use .GlobalEnv$x and avoid eval():

f <- function (value) {
     if (missing(value))
         .GlobalEnv$x[1L, ]
     else
         .GlobalEnv$x[1L, ] <- value
}

I'll see if I can figure out what is going on in the interpreted
assignment code.

Best,

luke

  
    
#
Hi Luke,

Thanks for the sleuthing and the suggested workaround. Unfortunately the
actual package code is a bit more complex, and surprisingly that breaks the
workaround via compiler::compile() (it fails just like the interpreted
version). In particular, my code is trying to guard against the (very
unlikely) scenario in which the calling code has redefined (or undefined)
`<-`, by explicitly qualifying it as base::`<-`():

f <- function (value) {
     if (missing(value)) {
         evalq(x[1L, ], .GlobalEnv)
     } else {
         assign_expr <- substitute(base::`<-`(x[1L, ], value), list(value =
value))
         cmp_assign_expr <- compiler::compile(assign_expr)
         eval(cmp_assign_expr, .GlobalEnv)
     }
}

(Incidentally I agree that this could be written much simpler, without
eval(); however, the purpose of the package is to provide a generic
mechanism to allow defining aliases to complex expressions via NSE; e.g.
you might write `ax := x[1L, ]`, and `ax` would henceforth operate as an
alias for that expression.)

Cheers,
Konrad
On Mon, 15 Dec 2025 at 04:26, <luke-tierney at uiowa.edu> wrote:

            

  
    
#
On Mon, 15 Dec 2025, Konrad Rudolph wrote:

            
This doesn't protect you against redefining or undefining `<-`. It
does protect against masking the base definition with one in an
environment frame. But you could check for that and throw an error if
it has happened. Then you could keep the rest of your code more sane.
I committed a small change to R-devel in r89182 that makes handling of
the internal `*tmp*` variable a bit more robust, so your examples
should no longer throw errors.
Not sure this is a realistic goal but good luck with that.

Best,

luke

  
    
7 days later
#
Hi Luke,

I keep forgetting to reply. Just briefly: your change made the code work
once more, thanks.

Cheers,
Konrad