Why is static value getting corrupted inside IRB?

Viewed 88

I am writing a Ruby method that calls Linux's utsname.h.

extconf.rb:

#!/usr/bin/env ruby
require 'mkmf'

abort('Not a linux') unless have_const('linux') || RbConfig::CONFIG['arch'].to_s[/linux/]
abort('No utsname.h') unless have_header('sys/utsname.h')

create_makefile 'utsname'

Here's the C code, (which isn't ideal, but I am still following the bad practice to learn why the values are getting corrupted inside irb):

#include <sys/utsname.h>
#include "ruby.h"

static struct utsname buf ;
static VALUE machine, nodename ;

void init() {
    short status = uname(&buf) ;
    if (status > -1) {
        machine = rb_str_new_cstr(buf.machine) ;
        nodename = rb_str_new_cstr(buf.nodename) ;
    } else {
        machine = rb_str_new_cstr("") ;
        nodename = rb_str_new_cstr("") ;
    }
}

static VALUE getMachine(VALUE obj) {
    return machine ;
}

static VALUE getNodename(VALUE obj) {
    return nodename ;
}

int Init_utsname() {
    init() ;
    rb_define_global_function("machine", getMachine, 0) ;
    rb_define_global_function("nodename", getNodename, 0) ;
}

Now after compiling and running in ruby interpreter, without irb:

$ ruby extconf.rb && make && ruby -r ./utsname.so -e "p [machine, nodename, machine, machine, nodename]"
checking for linux... yes
checking for sys/utsname.h... yes
creating Makefile
linking shared-object utsname.so
["x86_64", "archlinux", "x86_64", "x86_64", "archlinux"]

But when I am running that same thing inside irb:

$ irb
irb(main):001:0> require_relative File.join(__dir__, 'utsname.so')
=> true

irb(main):002:0> p [machine, nodename, machine, machine, nodename]
["x86_64", "archlinux", "x86_64", "x86_64", "archlinux"]
=> ["x86_64", "archlinux", "x86_64", "x86_64", "archlinux"]

irb(main):003:0> a = ''.match //
=> #<MatchData "">

irb(main):004:0> p [machine, nodename, machine, machine, nodename]
[[], ["irb(main):003:0> "], [], [], ["irb(main):003:0> "]]
=> [[], ["irb(main):003:0> "], [], [], ["irb(main):003:0> "]]

That happens randomly Sometimes the nodename is a string, sometimes, it's a matchdata, sometimes, it's something else, and sometimes I get the whole Ruby interpreter to crash (segmentation fault).

Now if I modify my C code, make the machine and nodename a local variable and that works reliably:

#include <sys/utsname.h>
#include "ruby.h"

static struct utsname buf ;
static short status ;

void init() {
    status = uname(&buf) ;
}

static VALUE getMachine(VALUE obj) {
    VALUE machine = status < 0 ? rb_str_new_cstr("") : rb_str_new_cstr(buf.machine) ;
    return machine ;
}

static VALUE getNodename(VALUE obj) {
    VALUE nodename = status < 0 ? rb_str_new_cstr("") : rb_str_new_cstr(buf.nodename) ;
    return nodename ;
}

int Init_utsname() {
    init() ;
    rb_define_global_function("machine", getMachine, 0) ;
    rb_define_global_function("nodename", getNodename, 0) ;
}

So what's going on? Why the first C code is buggy?

2 Answers

You can get this to work by ensuring Ruby keeps a reference to your objects and doesn’t try to GC (or compact) them. The simplest way is probably to use rb_global_variable:

Notice: GC should know about global variables which refer to Ruby's objects, but are not exported to the Ruby world. You need to protect them by

void rb_global_variable(VALUE *var)

In your case it might look like this:

void Init_utsname() {
    init() ;
    rb_global_variable(&machine);
    rb_global_variable(&nodename);
    rb_define_global_function("machine", getMachine, 0);
    rb_define_global_function("nodename", getNodename, 0);
}

The value inside first code is garbage collected.

https://docs.ruby-lang.org/en/2.4.0/extension_rdoc.html#label-Appendix+E.+RB_GC_GUARD+to+protect+from+premature+GC

C Ruby currently uses conservative garbage collection, thus VALUE variables must remain visible on the stack or registers to ensure any associated data remains usable.


So, I can make it deliberately spit out garbage values if I run GC.compact on it:

$ ruby extconf.rb && make && ruby -r ./utsname.so -e "p nodename ; GC.compact ; p nodename"

I get:

checking for linux... yes
checking for sys/utsname.h... yes
creating Makefile
linking shared-object utsname.so
"archlinux"
"), check your files list"

I can also disable the GC to make it work flawlessly, it's terrible, but it works:

ruby -r ./utsname.so -e "GC.disable ; p nodename ; GC.compact ; p nodename"

Here compact doesn't work because GC is disabled. The code runs just as intended.


I can also make it crash with GC.start:

ruby -r ./utsname.so -e "p nodename;GC.start ;p nodename"

I get:

`-e:1: [BUG] Segmentation fault at 0x0000000000000090`

  • The 2nd C code runs just fine because the VALUE nodename, and machine is assigned every time the function is called. GC is called on Ruby's objects assigned with VALUE, so buf ; doesn't get GC-ed and it doesn't get corrupted.
Related