Skip to content

Commit 166f386

Browse files
authored
Fixed a few buffer overrun bugs due to having assumed a "char" was just 1 byte (it is 2 bytes in .NET), and fixed a loop index bug that caused GetStringAscii to return a string that was just the first character repeated multiple times. (#154)
1 parent 11b727b commit 166f386

3 files changed

Lines changed: 148 additions & 6 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/*
2+
* Copyright 2014 - 2026 Adaptive Financial Consulting Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
using System.Text;
18+
using Adaptive.Agrona.Concurrent;
19+
using NUnit.Framework;
20+
21+
namespace Adaptive.Agrona.Tests
22+
{
23+
/// <summary>
24+
/// Regression tests for a Java-to-.NET porting bug in <see cref="UnsafeBuffer"/> and
25+
/// <see cref="ExpandableArrayBuffer"/> where ASCII string helpers used
26+
/// <c>*(char*)(buf + i) = c</c> in a byte-indexed loop. Because a .NET <c>char</c>
27+
/// is 2 bytes, that:
28+
/// (1) overran the bounds-checked region by one byte in
29+
/// <c>PutStringWithoutLengthAscii</c>, and
30+
/// (2) read the same 2 bytes every iteration in
31+
/// <c>GetStringAscii(int, StringBuilder)</c> because the offset
32+
/// expression mistakenly used <c>index</c> instead of the loop cursor.
33+
/// </summary>
34+
[TestFixture]
35+
public class AsciiStringBufferCharWidthTests
36+
{
37+
// ---- UnsafeBuffer ----
38+
39+
[Test]
40+
public void UnsafeBuffer_PutStringWithoutLengthAscii_DoesNotOverrunBoundsByOneByte()
41+
{
42+
byte[] backing = { 0xAA, 0xAA, 0xAA, 0xAA };
43+
var buffer = new UnsafeBuffer(backing);
44+
45+
int written = buffer.PutStringWithoutLengthAscii(0, "abc");
46+
47+
Assert.AreEqual(3, written);
48+
Assert.AreEqual((byte)'a', backing[0]);
49+
Assert.AreEqual((byte)'b', backing[1]);
50+
Assert.AreEqual((byte)'c', backing[2]);
51+
// With the pre-fix code this byte was clobbered to 0x00 by the
52+
// trailing high-byte of the final 2-byte char write.
53+
Assert.AreEqual(0xAA, backing[3], "sentinel byte at offset==length must not be overwritten");
54+
}
55+
56+
[Test]
57+
public void UnsafeBuffer_PutStringWithoutLengthAscii_WithValueOffsetAndLength_DoesNotOverrun()
58+
{
59+
byte[] backing = { 0xAA, 0xAA, 0xAA, 0xAA, 0xAA };
60+
var buffer = new UnsafeBuffer(backing);
61+
62+
int written = buffer.PutStringWithoutLengthAscii(0, "xabcdx", 1, 4);
63+
64+
Assert.AreEqual(4, written);
65+
Assert.AreEqual((byte)'a', backing[0]);
66+
Assert.AreEqual((byte)'b', backing[1]);
67+
Assert.AreEqual((byte)'c', backing[2]);
68+
Assert.AreEqual((byte)'d', backing[3]);
69+
Assert.AreEqual(0xAA, backing[4], "sentinel byte at offset==length must not be overwritten");
70+
}
71+
72+
[Test]
73+
public void UnsafeBuffer_GetStringAscii_StringBuilder_AdvancesReadOffset()
74+
{
75+
byte[] backing = new byte[16];
76+
var buffer = new UnsafeBuffer(backing);
77+
78+
buffer.PutStringAscii(0, "abcd");
79+
80+
var sb = new StringBuilder();
81+
buffer.GetStringAscii(0, sb);
82+
83+
// With the pre-fix code, every loop iteration read from `index`
84+
// (the length prefix), so the result was 4 copies of ''.
85+
Assert.AreEqual("abcd", sb.ToString());
86+
}
87+
88+
// ---- ExpandableArrayBuffer ----
89+
90+
[Test]
91+
public void ExpandableArrayBuffer_PutStringWithoutLengthAscii_DoesNotOverrunBoundsByOneByte()
92+
{
93+
var buffer = new ExpandableArrayBuffer(16);
94+
byte[] backing = buffer.ByteArray;
95+
for (int i = 0; i < backing.Length; i++)
96+
{
97+
backing[i] = 0xAA;
98+
}
99+
100+
int written = buffer.PutStringWithoutLengthAscii(0, "abc");
101+
102+
Assert.AreEqual(3, written);
103+
Assert.AreEqual((byte)'a', backing[0]);
104+
Assert.AreEqual((byte)'b', backing[1]);
105+
Assert.AreEqual((byte)'c', backing[2]);
106+
Assert.AreEqual(0xAA, backing[3], "sentinel byte at offset==length must not be overwritten");
107+
}
108+
109+
[Test]
110+
public void ExpandableArrayBuffer_PutStringWithoutLengthAscii_WithValueOffsetAndLength_DoesNotOverrun()
111+
{
112+
var buffer = new ExpandableArrayBuffer(16);
113+
byte[] backing = buffer.ByteArray;
114+
for (int i = 0; i < backing.Length; i++)
115+
{
116+
backing[i] = 0xAA;
117+
}
118+
119+
int written = buffer.PutStringWithoutLengthAscii(0, "xabcdx", 1, 4);
120+
121+
Assert.AreEqual(4, written);
122+
Assert.AreEqual((byte)'a', backing[0]);
123+
Assert.AreEqual((byte)'b', backing[1]);
124+
Assert.AreEqual((byte)'c', backing[2]);
125+
Assert.AreEqual((byte)'d', backing[3]);
126+
Assert.AreEqual(0xAA, backing[4], "sentinel byte at offset==length must not be overwritten");
127+
}
128+
129+
[Test]
130+
public void ExpandableArrayBuffer_GetStringAscii_StringBuilder_AdvancesReadOffset()
131+
{
132+
var buffer = new ExpandableArrayBuffer(16);
133+
134+
buffer.PutStringAscii(0, "abcd");
135+
136+
var sb = new StringBuilder();
137+
buffer.GetStringAscii(0, sb);
138+
139+
Assert.AreEqual("abcd", sb.ToString());
140+
}
141+
}
142+
}

src/Adaptive.Agrona/Concurrent/UnsafeBuffer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -828,7 +828,7 @@ public int GetStringAscii(int index, int length, StringBuilder appendable)
828828
{
829829
for (int i = index + BitUtil.SIZE_OF_INT, limit = index + BitUtil.SIZE_OF_INT + length; i < limit; i++)
830830
{
831-
char c = *(char*)(_pBuffer + index);
831+
char c = (char)*(_pBuffer + i);
832832
appendable.Append(c > (char)127 ? '?' : c);
833833
}
834834

@@ -879,7 +879,7 @@ public int PutStringWithoutLengthAscii(int index, string value)
879879
c = '?';
880880
}
881881

882-
*(char*)(_pBuffer + index + i) = c;
882+
*(_pBuffer + index + i) = (byte)c;
883883
}
884884

885885
return length;
@@ -899,7 +899,7 @@ public int PutStringWithoutLengthAscii(int index, string value, int valueOffset,
899899
c = '?';
900900
}
901901

902-
*(char*)(_pBuffer + index + i) = c;
902+
*(_pBuffer + index + i) = (byte)c;
903903
}
904904

905905
return len;

src/Adaptive.Agrona/ExpandableArrayBuffer.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ public int GetStringAscii(int index, int length, StringBuilder appendable)
258258
{
259259
for (int i = index + BitUtil.SIZE_OF_INT, limit = index + BitUtil.SIZE_OF_INT + length; i < limit; i++)
260260
{
261-
char c = *(char*)(_pBuffer + index);
261+
char c = (char)*(_pBuffer + i);
262262
appendable.Append(c > (char)127 ? '?' : c);
263263
}
264264

@@ -472,7 +472,7 @@ public int PutStringWithoutLengthAscii(int index, string value)
472472
c = '?';
473473
}
474474

475-
*(char*)(_pBuffer + index + i) = c;
475+
*(_pBuffer + index + i) = (byte)c;
476476
}
477477

478478
return length;
@@ -493,7 +493,7 @@ public int PutStringWithoutLengthAscii(int index, string value, int valueOffset,
493493
c = '?';
494494
}
495495

496-
*(char*)(_pBuffer + index + i) = c;
496+
*(_pBuffer + index + i) = (byte)c;
497497
}
498498

499499
return len;

0 commit comments

Comments
 (0)