bitwalker / uniq

Provides UUID generation, parsing, and formatting. Supports RFC 4122, and the v6 draft extension
Apache License 2.0
96 stars 14 forks source link

Parsing versions 1, 3, 4, 5 fails on OTP 26 #13

Closed boringcactus closed 1 year ago

boringcactus commented 1 year ago

I'm trying to update my application that uses Uniq to Erlang/OTP 26, but Uniq.UUID.valid?/1 is incorrectly returning false for valid UUIDs. I ran the Uniq tests (after updating the locked Ecto to v3.9.5), and I'm seeing 16 failures, all of which are related to parsing but none of which specifically parse versions 6 or 7.

Exact output ``` ❯ mix test 1) test can autogenerate primary keys (Uniq.Ecto.Test) test/ecto_test.exs:47 match (=) failed code: assert {:ok, %UUID{version: 4}} = UUID.parse(uuid) left: {:ok, %Uniq.UUID{version: 4}} right: {:error, {:invalid_format, <<240>>, 2, 6, 14}} stacktrace: test/ecto_test.exs:52: (test) 2) test generating can generate version 3 (Uniq.Test) test/uniq_test.exs:147 match (=) failed code: assert {:ok, %UUID{format: :default, version: 3}} = UUID.parse(default) left: {:ok, %Uniq.UUID{format: :default, version: 3}} right: {:error, {:invalid_format, "W", 2, 6, 14}} stacktrace: test/uniq_test.exs:154: (test) 3) property parsing can parse any 22-byte base64-encoded string which represents a valid uuid (Uniq.Test) test/uniq_test.exs:95 Failed with generated values (after 0 successful runs): * Clause: {version, variant, uuid} <- valid_uuid(:slug) Generated: {3, <<2::size(2)>>, "AAAAAAAAMACAAAAAAAAAAA"} match (=) failed The following variables were pinned: version = 3 variant = <<2::size(2)>> code: assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) left: {:ok, %Uniq.UUID{version: ^version, variant: ^variant}} right: {:error, {:invalid_format, <<0>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:97: anonymous fn/4 in Uniq.Test."property parsing can parse any 22-byte base64-encoded string which represents a valid uuid"/1 (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6 (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7 test/uniq_test.exs:96: (test) . 4) test generating can generate version 1 (Uniq.Test) test/uniq_test.exs:139 match (=) failed code: assert {:ok, %UUID{format: :default, version: 1}} = UUID.parse(default) left: {:ok, %Uniq.UUID{format: :default, version: 1}} right: {:error, {:invalid_format, <<218>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:143: (test) . 5) test generating can generate version 4 (Uniq.Test) test/uniq_test.exs:164 match (=) failed code: assert {:ok, %UUID{format: :default, version: 4}} = UUID.parse(default) left: {:ok, %Uniq.UUID{format: :default, version: 4}} right: {:error, {:invalid_format, "3", 2, 6, 14}} stacktrace: test/uniq_test.exs:168: (test) 6) doctest Uniq.UUID.info/2 (1) (Uniq.Test) test/uniq_test.exs:6 Doctest failed doctest: iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword) {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf", binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, type: :default, version: 4, variant: :rfc4122]} code: Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword) === {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf", binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, type: :default, version: 4, variant: :rfc4122]} left: {:error, {:invalid_format, "\f", 2, 6, 14}} right: { :ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf", binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, type: :default, version: 4, variant: :rfc4122] } stacktrace: lib/uuid.ex:364: Uniq.UUID (module) 7) doctest Uniq.UUID.info/2 (2) (Uniq.Test) test/uniq_test.exs:6 Doctest failed doctest: iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf") {:ok, %Uniq.UUID{ format: :default, version: 4, variant: <<2::2>>, time: 326283406408022248, seq: 790, node: <<129, 224, 137, 184, 194, 207>>, bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, }} code: Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf") === {:ok, %Uniq.UUID{ format: :default, version: 4, variant: <<2::2>>, time: 326283406408022248, seq: 790, node: <<129, 224, 137, 184, 194, 207>>, bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, }} left: {:error, {:invalid_format, "\f", 2, 6, 14}} right: {:ok, #UUIDv4<870df8e8-3107-4487-8316-81e089b8c2cf>} stacktrace: lib/uuid.ex:371: Uniq.UUID (module) .. 8) property parsing can parse any 128-bit binary with valid version/variant values (Uniq.Test) test/uniq_test.exs:71 Failed with generated values (after 0 successful runs): * Clause: {version, variant, uuid} <- valid_uuid() Generated: {3, <<2::size(2)>>, <<0, 0, 0, 0, 0, 0, 48, 0, 128, 0, 0, 0, 0, 0, 0, 0>>} match (=) failed The following variables were pinned: version = 3 variant = <<2::size(2)>> code: assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) left: {:ok, %Uniq.UUID{version: ^version, variant: ^variant}} right: {:error, {:invalid_format, <<0>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:73: anonymous fn/4 in Uniq.Test."property parsing can parse any 128-bit binary with valid version/variant values"/1 (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6 (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7 test/uniq_test.exs:72: (test) 9) test generating can generate version 5 (Uniq.Test) test/uniq_test.exs:172 match (=) failed code: assert {:ok, %UUID{format: :default, version: 5}} = UUID.parse(default) left: {:ok, %Uniq.UUID{format: :default, version: 5}} right: {:error, {:invalid_format, <<243>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:179: (test) .. 10) test parsing can parse version 5 (Uniq.Test) test/uniq_test.exs:59 match (=) failed The following variables were pinned: version = 5 code: assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version]) left: {:ok, %Uniq.UUID{format: :raw, version: ^version}} right: {:error, {:invalid_format, "_", 2, 6, 14}} stacktrace: test/uniq_test.exs:207: Uniq.Test.parse/2 test/uniq_test.exs:60: (test) 11) property parsing can parse any 32-byte hex string which represents a valid uuid (Uniq.Test) test/uniq_test.exs:83 Failed with generated values (after 0 successful runs): * Clause: {version, variant, uuid} <- valid_uuid(:hex) Generated: {3, <<2::size(2)>>, "00000000000030008000000000000000"} match (=) failed The following variables were pinned: version = 3 variant = <<2::size(2)>> code: assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid) left: {:ok, %Uniq.UUID{version: ^version, variant: ^variant}} right: {:error, {:invalid_format, <<0>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:85: anonymous fn/4 in Uniq.Test."property parsing can parse any 32-byte hex string which represents a valid uuid"/1 (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6 (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7 test/uniq_test.exs:84: (test) ...... 12) doctest Uniq.UUID.parse/1 (3) (Uniq.Test) test/uniq_test.exs:6 match (=) failed code: {:ok, uuid} = Uniq.UUID.parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6") left: {:ok, uuid} right: {:error, {:invalid_format, <<157>>, 2, 6, 14}} stacktrace: (for doctest at) lib/uuid.ex:443: (test) . 13) test parsing can parse version 1 (Uniq.Test) test/uniq_test.exs:47 match (=) failed The following variables were pinned: version = 1 code: assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version]) left: {:ok, %Uniq.UUID{format: :raw, version: ^version}} right: {:error, {:invalid_format, <<226>>, 2, 6, 14}} stacktrace: test/uniq_test.exs:207: Uniq.Test.parse/2 test/uniq_test.exs:48: (test) . 14) test parsing can parse version 4 (Uniq.Test) test/uniq_test.exs:55 match (=) failed The following variables were pinned: version = 4 code: assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version]) left: {:ok, %Uniq.UUID{format: :raw, version: ^version}} right: {:error, {:invalid_format, "`", 2, 6, 14}} stacktrace: test/uniq_test.exs:207: Uniq.Test.parse/2 test/uniq_test.exs:56: (test) 15) test parsing can parse version 3 (Uniq.Test) test/uniq_test.exs:51 match (=) failed The following variables were pinned: version = 3 code: assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version]) left: {:ok, %Uniq.UUID{format: :raw, version: ^version}} right: {:error, {:invalid_format, "B", 2, 6, 14}} stacktrace: test/uniq_test.exs:207: Uniq.Test.parse/2 test/uniq_test.exs:52: (test) . 16) doctest Uniq.UUID.parse/1 (4) (Uniq.Test) test/uniq_test.exs:6 Doctest failed doctest: iex> match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse()) true code: match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse()) === true left: false right: true stacktrace: lib/uuid.ex:456: Uniq.UUID (module) . Finished in 15.1 seconds (0.1s async, 15.0s sync) 4 doctests, 6 properties, 22 tests, 16 failures Randomized with seed 100798 ```

I looked at the code and the Erlang/OTP 26 release notes to see if I could spot the problem, but I can't.

alappe commented 1 year ago

I can confirm the issue with OTP v26 and v0.6.0 still exists:

  1) test can autogenerate primary keys (Uniq.Ecto.Test)
     test/ecto_test.exs:47
     match (=) failed
     code:  assert {:ok, %UUID{version: 4}} = UUID.parse(uuid)
     left:  {:ok, %Uniq.UUID{version: 4}}
     right: {:error, {:invalid_format, <<3>>, 2, 6, 14}}
     stacktrace:
       test/ecto_test.exs:52: (test)

.

  2) test parsing can parse version 1 (Uniq.Test)
     test/uniq_test.exs:47
     match (=) failed
     The following variables were pinned:
       version = 1
     code:  assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version])
     left:  {:ok, %Uniq.UUID{format: :raw, version: ^version}}
     right: {:error, {:invalid_format, <<226>>, 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:207: Uniq.Test.parse/2
       test/uniq_test.exs:48: (test)

...

  3) doctest Uniq.UUID.parse/1 (3) (Uniq.Test)
     test/uniq_test.exs:6
     match (=) failed
     code:  {:ok, uuid} = Uniq.UUID.parse("f81d4fae-7dec-11d0-a765-00a0c91e6bf6")
     left:  {:ok, uuid}
     right: {:error, {:invalid_format, <<157>>, 2, 6, 14}}
     stacktrace:
       (for doctest at) lib/uuid.ex:443: (test)

...

  4) doctest Uniq.UUID.info/2 (2) (Uniq.Test)
     test/uniq_test.exs:6
     Doctest failed
     doctest:
       iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf")
       {:ok, %Uniq.UUID{
        format: :default,
        version: 4,
        variant: <<2::2>>,
        time: 326283406408022248,
        seq: 790,
        node: <<129, 224, 137, 184, 194, 207>>,
        bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
       }}
     code:  Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf") === {:ok, %Uniq.UUID{
             format: :default,
             version: 4,
             variant: <<2::2>>,
             time: 326283406408022248,
             seq: 790,
             node: <<129, 224, 137, 184, 194, 207>>,
             bytes: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
            }}
     left:  {:error, {:invalid_format, "\f", 2, 6, 14}}
     right: {:ok, #UUIDv4<870df8e8-3107-4487-8316-81e089b8c2cf>}
     stacktrace:
       lib/uuid.ex:371: Uniq.UUID (module)

.

  5) test parsing can parse version 5 (Uniq.Test)
     test/uniq_test.exs:59
     match (=) failed
     The following variables were pinned:
       version = 5
     code:  assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version])
     left:  {:ok, %Uniq.UUID{format: :raw, version: ^version}}
     right: {:error, {:invalid_format, "_", 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:207: Uniq.Test.parse/2
       test/uniq_test.exs:60: (test)

  6) doctest Uniq.UUID.info/2 (1) (Uniq.Test)
     test/uniq_test.exs:6
     Doctest failed
     doctest:
       iex> Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword)
       {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf",
        binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
        type: :default,
        version: 4,
        variant: :rfc4122]}
     code:  Uniq.UUID.info("870df8e8-3107-4487-8316-81e089b8c2cf", :keyword) === {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf",
             binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>,
             type: :default,
             version: 4,
             variant: :rfc4122]}
     left:  {:error, {:invalid_format, "\f", 2, 6, 14}}
     right: {:ok, [uuid: "870df8e8-3107-4487-8316-81e089b8c2cf", binary: <<135, 13, 248, 232, 49, 7, 68, 135, 131, 22, 129, 224, 137, 184, 194, 207>>, type: :default, version: 4, variant: :rfc4122]}
     stacktrace:
       lib/uuid.ex:364: Uniq.UUID (module)

.

  7) test generating can generate version 1 (Uniq.Test)
     test/uniq_test.exs:139
     match (=) failed
     code:  assert {:ok, %UUID{format: :default, version: 1}} = UUID.parse(default)
     left:  {:ok, %Uniq.UUID{format: :default, version: 1}}
     right: {:error, {:invalid_format, <<2>>, 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:143: (test)

....

  8) test generating can generate version 3 (Uniq.Test)
     test/uniq_test.exs:147
     match (=) failed
     code:  assert {:ok, %UUID{format: :default, version: 3}} = UUID.parse(default)
     left:  {:ok, %Uniq.UUID{format: :default, version: 3}}
     right: {:error, {:invalid_format, "W", 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:154: (test)

  9) property parsing can parse any 32-byte hex string which represents a valid uuid (Uniq.Test)
     test/uniq_test.exs:83
     Failed with generated values (after 0 successful runs):

         * Clause:    {version, variant, uuid} <- valid_uuid(:hex)
           Generated: {4, <<7::size(3)>>, "0000000000004000E000000000000000"}

     match (=) failed
     The following variables were pinned:
       version = 4
       variant = <<7::size(3)>>
     code:  assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid)
     left:  {:ok, %Uniq.UUID{version: ^version, variant: ^variant}}
     right: {:error, {:invalid_format, <<0>>, 3, 5, 13}}
     stacktrace:
       test/uniq_test.exs:85: anonymous fn/4 in Uniq.Test."property parsing can parse any 32-byte hex string which represents a valid uuid"/1
       (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6
       (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7
       test/uniq_test.exs:84: (test)

 10) property parsing can parse any 22-byte base64-encoded string which represents a valid uuid (Uniq.Test)
     test/uniq_test.exs:95
     Failed with generated values (after 0 successful runs):

         * Clause:    {version, variant, uuid} <- valid_uuid(:slug)
           Generated: {4, <<7::size(3)>>, "AAAAAAAAQADgAAAAAAAAAA"}

     match (=) failed
     The following variables were pinned:
       version = 4
       variant = <<7::size(3)>>
     code:  assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid)
     left:  {:ok, %Uniq.UUID{version: ^version, variant: ^variant}}
     right: {:error, {:invalid_format, <<0>>, 3, 5, 13}}
     stacktrace:
       test/uniq_test.exs:97: anonymous fn/4 in Uniq.Test."property parsing can parse any 22-byte base64-encoded string which represents a valid uuid"/1
       (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6
       (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7
       test/uniq_test.exs:96: (test)

 11) doctest Uniq.UUID.parse/1 (4) (Uniq.Test)
     test/uniq_test.exs:6
     Doctest failed
     doctest:
       iex> match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse())
       true
     code:  match?({:ok, %Uniq.UUID{format: :default, version: 1}}, Uniq.UUID.uuid1() |> Uniq.UUID.parse()) === true
     left:  false
     right: true
     stacktrace:
       lib/uuid.ex:456: Uniq.UUID (module)

 12) test parsing can parse version 4 (Uniq.Test)
     test/uniq_test.exs:55
     match (=) failed
     The following variables were pinned:
       version = 4
     code:  assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version])
     left:  {:ok, %Uniq.UUID{format: :raw, version: ^version}}
     right: {:error, {:invalid_format, "`", 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:207: Uniq.Test.parse/2
       test/uniq_test.exs:56: (test)

 13) test generating can generate version 4 (Uniq.Test)
     test/uniq_test.exs:164
     match (=) failed
     code:  assert {:ok, %UUID{format: :default, version: 4}} = UUID.parse(default)
     left:  {:ok, %Uniq.UUID{format: :default, version: 4}}
     right: {:error, {:invalid_format, <<220>>, 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:168: (test)

 14) property parsing can parse any 128-bit binary with valid version/variant values (Uniq.Test)
     test/uniq_test.exs:71
     Failed with generated values (after 0 successful runs):

         * Clause:    {version, variant, uuid} <- valid_uuid()
           Generated: {4, <<7::size(3)>>, <<0, 0, 0, 0, 0, 0, 64, 0, 224, 0, 0, 0, 0, 0, 0, 0>>}

     match (=) failed
     The following variables were pinned:
       version = 4
       variant = <<7::size(3)>>
     code:  assert {:ok, %UUID{version: ^version, variant: ^variant}} = UUID.parse(uuid)
     left:  {:ok, %Uniq.UUID{version: ^version, variant: ^variant}}
     right: {:error, {:invalid_format, <<0>>, 3, 5, 13}}
     stacktrace:
       test/uniq_test.exs:73: anonymous fn/4 in Uniq.Test."property parsing can parse any 128-bit binary with valid version/variant values"/1
       (stream_data 0.5.0) lib/stream_data.ex:2148: StreamData.shrink_failure/6
       (stream_data 0.5.0) lib/stream_data.ex:2108: StreamData.check_all/7
       test/uniq_test.exs:72: (test)

.

 15) test parsing can parse version 3 (Uniq.Test)
     test/uniq_test.exs:51
     match (=) failed
     The following variables were pinned:
       version = 3
     code:  assert {:ok, %UUID{format: :raw, version: ^version}} = UUID.parse(uuids[:raw][version])
     left:  {:ok, %Uniq.UUID{format: :raw, version: ^version}}
     right: {:error, {:invalid_format, "B", 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:207: Uniq.Test.parse/2
       test/uniq_test.exs:52: (test)

 16) test generating can generate version 5 (Uniq.Test)
     test/uniq_test.exs:172
     match (=) failed
     code:  assert {:ok, %UUID{format: :default, version: 5}} = UUID.parse(default)
     left:  {:ok, %Uniq.UUID{format: :default, version: 5}}
     right: {:error, {:invalid_format, <<128>>, 2, 6, 14}}
     stacktrace:
       test/uniq_test.exs:179: (test)

..
Finished in 15.2 seconds (0.1s async, 15.0s sync)
4 doctests, 6 properties, 22 tests, 16 failures

Randomized with seed 870698
bitwalker commented 1 year ago

Interesting - I wonder what happened in OTP 26 to break things, I'll take a look at this ASAP

bitwalker commented 1 year ago

Something really weird is going on here, to the point that I suspect a miscompilation.

Parsing is failing here, because the binary constructed by <<clock::unsigned-integer-size(clock_size>> = <<clock_hi::bitstring-size(clock_hi_size), clock_lo::bitstring-size(8)>> is inexplicably being truncated to 8 bits from 14 bits. For reference, I'm looking at the intermediate results when running mix test test/uniq_test:47, which tests parsing a Version 1 uuid. For that test, here's the values of the bindings in that expression:

clock_size = 14
clock_hi = <<56::size(6)>>
clock_hi_size = 6
clock_lo = <<188>>

If I open up IEx and type in the exact same expression with all of the same values, I get <<226, 60::size(6)>> as output, which is correct; but the compiled code produces <<226>>! However, if I change clock_hi_size to 6 in the code, it correctly produces <<226, 60::size(6)>> and the test passes.

I can only assume this is a JIT issue, because binary constructing/matching in the shell uses the eval_bits module in the Erlang standard library, whereas compiled code uses highly specialized bytecode instructions for the same tasks, some/all of which get JITed I would imagine.

@josevalim Has anyone reported similar issues in Elixir with OTP 26? I'm not actually sure whether this is an issue in Erlang or in Elixir. It is a bit tough to reproduce minimally because the bug here only seems to appear when none of the values are known at compile-time, so presumably optimizations are interfering in less dynamic, but equivalent programs. I would assume it's an issue in Erlang due to the difference in behavior between compiled code and interpreted code, but that might just be a coincidence. Sorry to ping you like this, again 😅, but this definitely appears to be a compiler bug of some sort or another. I'm happy to put in some effort on a minimal reproducer, but wanted to make sure this wasn't already a known issue that I'm not aware of.

josevalim commented 1 year ago

From the description it looks like an Erlang bug. So I would try 26.0.2 and, if it persists, we need a way to reproduce it so I can report it upstream.

bitwalker commented 1 year ago

Yeah I'm testing with 26.0.2, so I'll put a reproducer together and follow up. Thanks!

bitwalker commented 1 year ago

@josevalim Here's the reproducer:

# lib/app.ex
defmodule BinMatchBugOtp26 do
  defmacrop bits(n), do: quote(do: bitstring - size(unquote(n)))
  defmacrop uint(n), do: quote(do: unsigned - integer - size(unquote(n)))

  @rfc_variant <<2::2>>

  def parse(<<_::64, @rfc_variant, rest::bits(62)>>) do
    parse_raw(@rfc_variant, rest)
  end

  defp parse_raw(variant, rest) do
    variant_size = bit_size(variant)
    clock_hi_size = 8 - variant_size
    clock_size = 8 + clock_hi_size

    with <<clock_hi::bits(clock_hi_size), clock_lo::bits(8), _::bits(48)>> <-
           rest,
         <<clock::uint(clock_size)>> <-
           IO.inspect(<<clock_hi::bits(clock_hi_size), clock_lo::bits(8)>>) do
      {:ok, clock}
    else
      other ->
        {:error, {:invalid_format, other}}
    end
  end
end

With the following test harness:

# test/app_test.exs
defmodule BinMatchBugOtp26Test do
  use ExUnit.Case

  test "reproduces the bug" do
    bin = <<146, 254, 245, 214, 198, 57, 17, 235, 184, 188, 2, 66, 172, 19, 0, 3>>
    assert {:ok, 14524} = BinMatchBugOtp26.parse(bin)
  end
end

If you modify it to change variant_size = bit_size(variant) to variant_size = 2, the test passes, presumably due to optimizations, but I haven't looked at the bytecode to see for sure. I think this should be minimal enough for debugging/reporting upstream.

Just to reiterate, we're expecting that the binary which is being inspected has the value <<226, 60::size(6)>>, but the bug causes it to be <<226>>.

josevalim commented 1 year ago

Thanks, reported upstream here: https://github.com/erlang/otp/issues/7469

josevalim commented 1 year ago

Btw, it may be worth writing the alternative variant and shipping a new release to unblock others. :)

bitwalker commented 1 year ago

There isn't a great alternative unfortunately, the code I used for the repro was just for a single UUID variant, and there are quite a few. I can probably use macros to generate more specialized parsing functions that bypass the issue for the time being though.

For anyone reading, I wouldn't consider running OTP 26 in production until this bug gets patched, we're lucky that it is so easy to trigger with uniq, but that is only because I was careful to write assertions into core parts of the parsing code. I have to imagine there is a lot of parsing code out there affected by this bug, just with more subtle failure modes.

bitwalker commented 1 year ago

I've pushed a workaround in main, and I'll push a new release (0.6.1) shortly