ruby/ruby · critical

RUBY_DEBUG_LOG=mem is not specified.

Error message

RUBY_DEBUG_LOG=mem is not specified.

What it means

Raised by Gem::Installer#verify_spec (second bindir check) when File.expand_path(File.join(gem_dir, spec.bindir)) falls outside the expanded gem directory — i.e. bindir contains '..' segments or another traversal that escapes the install root. This blocks path-traversal attacks where a crafted gem writes executables outside its own directory. A legitimate bindir like 'bin' or 'exe' can never trip it.

Source

Thrown at debug.c:687

static void
debug_log_dump(FILE *out, unsigned int n)
{
    if (ruby_debug_log_mode & ruby_debug_log_memory) {
        unsigned int size = debug_log.cnt > MAX_DEBUG_LOG ? MAX_DEBUG_LOG : debug_log.cnt;
        unsigned int current_index = debug_log.cnt % MAX_DEBUG_LOG;
        if (n == 0) n = size;
        if (n > size) n = size;

        for (unsigned int i=0; i<n; i++) {
            int index = current_index - size + i;
            if (index < 0) index += MAX_DEBUG_LOG;
            VM_ASSERT(index <= MAX_DEBUG_LOG);
            const char *mesg = RUBY_DEBUG_LOG_MEM_ENTRY(index);
            fprintf(out, "%4u: %s\n", debug_log.cnt - size + i, mesg);
        }
    }
    else {
        fprintf(stderr, "RUBY_DEBUG_LOG=mem is not specified.");
    }
}

// for debuggers

void
ruby_debug_log_print(unsigned int n)
{
    debug_log_dump(stderr, n);
}

void
ruby_debug_log_dump(const char *fname, unsigned int n)
{
    FILE *fp = fopen(fname, "w");
    if (fp == NULL) {
        fprintf(stderr, "can't open %s. give up.\n", fname);
    }

View on GitHub (pinned to 0e5b888e1c)

Solutions

  1. Use a plain in-gem directory: spec.bindir = 'bin' (or 'exe') and rebuild
  2. Audit any gemspec template that interpolates paths into bindir
  3. If the gem is third-party and contains traversal bindir, treat it as malicious and report/remove it

Example fix

# before
spec.bindir = '../bin'
# after
spec.bindir = 'bin'
Defensive patterns

Strategy: validation

Validate before calling

expanded = File.expand_path(File.join(gem_dir, spec.bindir))
inside = expanded == gem_dir || expanded.start_with?(gem_dir + '/')
abort 'bindir escapes gem dir' unless inside

Type guard

->(s, dir) { e = File.expand_path(File.join(dir, s.bindir)); e == File.expand_path(dir) || e.start_with?(File.expand_path(dir) + '/') }

Try / catch

begin
  Gem::Installer.new(path).install
rescue Gem::InstallError => e
  raise SecurityError, "possible traversal gem: #{e.message}" if e.message.include?('bindir')
end

Prevention

When it happens

Trigger: Installing a gem whose spec has bindir = '../bin', bindir = 'bin/../../share/bin', or any value whose expansion is not gem_dir or under gem_dir + '/'. Constructed maliciously, or accidentally when a build script interpolates a computed path like "#{ENV['HOME']}/bin" into bindir.

Common situations: Malicious gems planted on mirrors or vendored paths attempting to overwrite system executables; automation that generates gemspecs from templates with unescaped relative paths; gems assembled by hand with `tar` and a custom spec.

Related errors


AI-assisted analysis of ruby/ruby@0e5b888e1c (2026-08-21). Data as JSON: /api/errors/4b5b5b431a58a229. Report an issue: GitHub.