Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ArgumentOutOfRangeException in PagedWriterBuffer #146

Open
CoderNate opened this issue Sep 14, 2022 · 2 comments
Open

ArgumentOutOfRangeException in PagedWriterBuffer #146

CoderNate opened this issue Sep 14, 2022 · 2 comments

Comments

@CoderNate
Copy link
Contributor

I'm getting the following error:

System.ArgumentOutOfRangeException: Specified argument was out of the range of valid values.
Parameter name: start
   at System.ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument argument)
   at Amazon.IonDotnet.Internals.PagedWriterBuffer.WriteBytes(ReadOnlySpan`1 bytes)
   at Amazon.IonDotnet.Internals.PagedWriterBuffer.WriteUtf8(ReadOnlySpan`1 chars, Int32 length)
   at Amazon.IonDotnet.Internals.Binary.RawBinaryWriter.WriteString(String value)

In the constructor the blockSize gets set like this:

        protected PagedWriterBuffer(int intendedBlockSize)
        {
            ...
            this.currentBlock = ArrayPool<byte>.Shared.Rent(intendedBlockSize);
            ...
            this.blockSize = this.currentBlock.Length;
        }

Note that the ArrayPool can return a larger array than what you ask for. Later on currentBlock gets replaced in AllocateNewBlock:

        private void AllocateNewBlock() {
            ...
            var newBlock = ArrayPool<byte>.Shared.Rent(this.blockSize);
            this.bufferBlocks.Add(newBlock);
            this.currentBlock = newBlock;
        }

What I'm running into is:

  1. The blockSize is set to 512 in the constructor and currentBlock starts out having length 512
  2. Later in AllocateNewBlock the ArrayPool returns a block that's 1024 and it's assigned to currentBlock
  3. The WriteUtf8 method writes bytes as long as it doesn't go past this.currentBlock.Length which is 1024
  4. The WriteBytes method looks at blockSize instead of currentBlock.Length:
        public void WriteBytes(ReadOnlySpan<byte> bytes)
        {
            ...
            var bytesToWrite = bytes.Length;
            while (bytesToWrite > 0)
            {
                // If blockSize is 512 and runningIndex is greater than 512 then "left" will be negative:
                var left = this.blockSize - this.runningIndex;  
                var bytesWritten = bytesToWrite > left ? left : bytesToWrite;
                // A negative number for bytesWritten causes the following to throw the ArgumentOutOfRangeException
                bytes.Slice(0, bytesWritten).CopyTo(new Span<byte>(this.currentBlock, this.runningIndex, bytesWritten));
                ...
            }
        }

I assume that WriteBytes should refer to currentBlock.Length like all the other methods do instead of blockSize. Perhaps the blockSize field should be removed. However, AllocateNewBlock needs to know what size to pass to ArrayPool.Rent so maybe the intendedBlockSize argument from the constructor should be stored in a readonly field for that purpose in place of using blockSize.

Thanks for making this library available!

CoderNate added a commit to CoderNate/ion-dotnet that referenced this issue Sep 14, 2022
CoderNate added a commit to CoderNate/ion-dotnet that referenced this issue Sep 14, 2022
@CoderNate
Copy link
Contributor Author

I can consistently reproduce this in .NET framework 4.8 with the code below running as a console app. It doesn't appear to happen in .NET core 3.1.

    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription);
            for (var i = 0; i < 20; i++)
            {
                RunTest();
            }
        }
        static void RunTest()
        {
            using var ms = new System.IO.MemoryStream();
            using var writer = Amazon.IonDotnet.Builders.IonBinaryWriterBuilder.Build(ms);
            var r = new Random(0); // Always use the same seed so we get the same sequence of values out
            writer.StepIn(Amazon.IonDotnet.IonType.List);
            for (var i = 0; i < 100_000; i++)
            {
                var strLen = r.Next(10, 40);
                try
                {
                    writer.WriteString(new string('a', strLen));
                }
                catch (ArgumentOutOfRangeException ex)
                {
                    Console.WriteLine($"Exception on i == {i}");
                    return;
                }
            }
        }
    }

This produces:

.NET Framework 4.8.4510.0
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979
Exception on i == 979

I got this same result on a second machine running .NET 4.8.4110.0. I don't know if this could reliably be made into a unit test though because the bug is triggered by using the ArrayPool that's shared by all threads.

@tgregg tgregg closed this as completed in ff73b85 Oct 17, 2022
@tgregg
Copy link
Contributor

tgregg commented Oct 17, 2022

The related fix has been merged; keeping this open so we can consider adding repeatable tests for this case.

@tgregg tgregg reopened this Oct 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants