Uploaded image for project: 'Ruby Driver'
  1. Ruby Driver
  2. RUBY-3438

BSON 5.0 regression, ObjectID generation broken after year 2038 (signed int)

    • Type: Icon: Bug Bug
    • Resolution: Duplicate
    • Priority: Icon: Major - P3 Major - P3
    • None
    • Affects Version/s: None
    • Component/s: BSON
    • Labels:
      None
    • Ruby Drivers

      As I started testing BSON 5.0 in my ruby codebase I noticed some failing tests using far-future dates (that I introduced on purpuse in my codebase to detect when libraries accidentally breaks supports for them, good thing I did ☺). I noticed one such problem with BSON 5.0 which now raises an exception when we try to generate a BSON::ObjectID after 2038 (the famous signed int timestamp limit)

      I know the BSON specs only supports 4 bytes so limits to year 2106 when using unsigned int. After that it's supposed to wrap around back to 0 (1970) I suppose, which is not perfect but good enough for the 70 coming years IMO

      But here the problem is much worse, not only it doesn't wrap around when using ObjectId.new (it raises instead), but also there must be a signed int somewhere because it fails at 2038 instead of 2106.

      The problem was instroduced in https://github.com/mongodb/bson-ruby/commit/9484d2a0caa1414c4ec1e8cfb6cddf42e6e71cd4 for https://github.com/mongodb/bson-ruby/pull/311 in my opinion.

      I tried to write the smallest reproducible example possible (`bson-5.0-2038bug.rb`):

      #!/usr/bin/env ruby
      require 'bundler/inline'
      
      # example:
      # ./bson-5.0-2038bug.rb 2039
      
      gemfile do
        source 'https://rubygems.org'
        gem 'bson', '5.0.0'
        gem 'activesupport', '7.0.8'
      end
      
      year = (ARGV.first || 2222).to_i
      
      puts "BSON: #{BSON::VERSION}"
      puts "YEAR: #{year}"
      
      # stub Time.now
      require 'active_support/testing/time_helpers'
      include ActiveSupport::Testing::TimeHelpers
      travel_to(Time.utc(year, 1, 1))
      
      puts "Time.now: #{Time.now} (#{Time.now.to_i})"
      
      puts "\nBSON::ObjectId.from_time(Time.now) =>"
      id = BSON::ObjectId.from_time(Time.now)
      puts "  #{id.inspect} (time: #{id.generation_time})"
      
      puts "\nBSON::ObjectId.new =>"
      id = BSON::ObjectId.new
      puts "  #{id.inspect} (time: #{id.generation_time})"
      

      Here are some results with BSON 5.0:

      # ./bson-5.0-2038bug.rb 2038
      BSON: 5.0.0
      YEAR: 2038
      Time.now: 2038-01-01 01:00:00 +0100 (2145916800)
      
      BSON::ObjectId.from_time(Time.now) =>
        BSON::ObjectId('7fe817800000000000000000') (time: 2038-01-01 00:00:00 UTC)
      
      BSON::ObjectId.new =>
        BSON::ObjectId('7fe817802dba73a68165bc96') (time: 2038-01-01 00:00:00 UTC)
      
      # ./bson-5.0-2038bug.rb 2039
      BSON: 5.0.0
      YEAR: 2039
      Time.now: 2039-01-01 01:00:00 +0100 (2177452800)
      
      BSON::ObjectId.from_time(Time.now) =>
        BSON::ObjectId('81c94b000000000000000000') (time: 2039-01-01 00:00:00 UTC)
      
      BSON::ObjectId.new =>
      /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:246:in `next_object_id': integer 2177452800 too big to convert to `int' (RangeError)
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:246:in `generate_data'
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:192:in `to_s'
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:135:in `inspect'
      	from ../bson-5.0-2038bug.rb:31:in `<main>'
      
      # ./bson-5.0-2038bug.rb 2107
      BSON: 5.0.0
      YEAR: 2107
      Time.now: 2107-01-01 01:00:00 +0100 (4323283200)
      
      BSON::ObjectId.from_time(Time.now) =>
        BSON::ObjectId('01b011000000000000000000') (time: 1970-11-24 17:31:44 UTC)
      
      BSON::ObjectId.new =>
      /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:246:in `next_object_id': integer 4323283200 too big to convert to `int' (RangeError)
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:246:in `generate_data'
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:192:in `to_s'
      	from /home/bigbourin/.rbenv/versions/3.1.2/lib/ruby/gems/3.1.0/gems/bson-5.0.0/lib/bson/object_id.rb:135:in `inspect'
      	from ../bson-5.0-2038bug.rb:31:in `<main>'
      

      We can see that:

      • before 2037 all is well
      • after 2037 `ObjectId.new` starts failing due the signed int problem (I suppose because you're using `NUM2INT` instead of `NUM2UINT`). We can see `from_time` is still working though, because it's not using the same C code.
      • after 2106 we can see `from_time` correctly wraps around the integer overflow and goes back to 1970, while ObjectId.new is still broken.

      So the problem is twofold IMO:

      • Should use `NUM2UINT` to support properly times up to 2106
      • After that `NUM2UINT` will also fail so I guess you should add a modulo before the UINT conversion or something like that, so it wraps around without raising.

      Just to be clear, in BSON 4.15 I didn't have the issue but that's only because the C code was not calling to ruby's Time.now so my time stubbing in specs was not taken into account. It's harder to stub the C time() function used in BSON 4.15 so I didn't try but from what I read in the C code, BSON 4.15 doesn't have this bug (that's why I'm calling it a regression) and should work at least until 2106:

      # ./bson-5.0-2038bug.rb 2107
      BSON: 4.15.0
      YEAR: 2107
      Time.now: 2107-01-01 01:00:00 +0100 (4323283200)
      
      BSON::ObjectId.from_time(Time.now) =>
        BSON::ObjectId('01b011000000000000000000') (time: 1970-11-24 17:31:44 UTC)
      
      BSON::ObjectId.new =>
        BSON::ObjectId('661550fbb2c79a93f3d1f59c') (time: 2024-04-09 14:30:19 UTC)
      

      ps: it would be nice to have a link on the github readme for BSON about where to post issues. I know because I've used your jira hundreds of times unfortunately, but for other users, it's pretty cumbersome to find with the issues tab disabled on github and no link in the readme.

      Thanks!

            Assignee:
            dmitry.rybakov@mongodb.com Dmitry Rybakov
            Reporter:
            bigbourin@gmail.com Adrien Jarthon
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated:
              Resolved: