File: StringRacyConstructor.java

package info (click to toggle)
openjdk-24 24.0.2%2B12-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 831,900 kB
  • sloc: java: 5,677,020; cpp: 1,323,154; xml: 1,320,524; ansic: 486,889; asm: 405,131; objc: 21,025; sh: 15,221; javascript: 11,049; python: 8,222; makefile: 2,504; perl: 357; awk: 351; sed: 172; pascal: 103; exp: 54; jsp: 24; csh: 3
file content (437 lines) | stat: -rw-r--r-- 16,859 bytes parent folder | download | duplicates (6)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
/*
 * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package test.java.lang.String;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.ConcurrentModificationException;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

/*
 * @test
 * @bug 8311906
 * @modules java.base/java.lang:open
 * @summary check String's racy constructors
 * @run junit/othervm -XX:+CompactStrings test.java.lang.String.StringRacyConstructor
 * @run junit/othervm -XX:-CompactStrings test.java.lang.String.StringRacyConstructor
 */

public class StringRacyConstructor {
    private static final byte LATIN1 = 0;
    private static final byte UTF16  = 1;

    private static final Field STRING_CODER_FIELD;
    private static final Field SB_CODER_FIELD;
    private static final boolean COMPACT_STRINGS;

    static {
        try {
            STRING_CODER_FIELD = String.class.getDeclaredField("coder");
            STRING_CODER_FIELD.setAccessible(true);
            SB_CODER_FIELD = Class.forName("java.lang.AbstractStringBuilder").getDeclaredField("coder");
            SB_CODER_FIELD.setAccessible(true);
            COMPACT_STRINGS = isCompactStrings();
        } catch (NoSuchFieldException ex ) {
            throw new ExceptionInInitializerError(ex);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /* {@return true iff CompactStrings are enabled}
     */
    public static boolean isCompactStrings() {
        try {
            Field compactStringField = String.class.getDeclaredField("COMPACT_STRINGS");
            compactStringField.setAccessible(true);
            return compactStringField.getBoolean(null);
        } catch (NoSuchFieldException ex) {
            throw new ExceptionInInitializerError(ex);
        } catch (IllegalAccessException iae) {
            throw new AssertionError(iae);
        }
    }

    // Return the coder for the String
    private static int coder(String s) {
        try {
            return STRING_CODER_FIELD.getByte(s);
        } catch (IllegalAccessException iae) {
            throw new AssertionError(iae);
        }
    }

    // Return the coder for the StringBuilder
    private static int sbCoder(StringBuilder sb) {
        try {
            return SB_CODER_FIELD.getByte(sb);
        } catch (IllegalAccessException iae) {
            throw new AssertionError(iae);
        }
    }

    // Return a summary of the internals of the String
    // The coder and indicate if the coder matches the string contents
    private static String inspectString(String s) {
        try {
            char[] chars = s.toCharArray();
            String r = new String(chars);

            boolean invalidCoder = coder(s) != coder(r);
            String coder = STRING_CODER_FIELD.getByte(s) == 0 ? "isLatin1" : "utf16";
            return (invalidCoder ? "INVALID CODER" : "" ) + " \"" + s + "\", coder: " + coder;
        } catch (IllegalAccessException ex ) {
            return "EXCEPTION: " + ex.getMessage();
        }
    }

    /**
     * {@return true if the coder matches the presence/lack of UTF16 characters}
     * If it returns false, the coder and the contents have failed the precondition for string.
     * @param orig a string
     */
    private static boolean validCoder(String orig) {
        if (!COMPACT_STRINGS) {
            assertEquals(UTF16, coder(orig), "Non-COMPACT STRINGS coder must be UTF16");
        }
        int accum = 0;
        for (int i = 0; i < orig.length(); i++)
            accum |= orig.charAt(i);
        byte expectedCoder = (accum < 256) ? LATIN1 : UTF16;
        return expectedCoder == coder(orig);
    }

    // Check a StringBuilder for consistency of coder and latin1 vs UTF16
    private static boolean validCoder(StringBuilder orig) {
        int accum = 0;
        for (int i = 0; i < orig.length(); i++)
            accum |= orig.charAt(i);
        byte expectedCoder = (accum < 256) ? LATIN1 : UTF16;
        return expectedCoder == sbCoder(orig);
    }

    @Test
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void checkStringRange() {
        char[] chars = {'a', 'b', 'c', 0xff21, 0xff22, 0xff23};
        String orig = new String(chars);
        char[] xx = orig.toCharArray();
        String stringFromChars = new String(xx);
        assertEquals(orig, stringFromChars, "mixed chars");
        assertTrue(validCoder(stringFromChars), "invalid coder"
                + ", invalid coder: " + inspectString(stringFromChars));
    }

    private static List<String> strings() {
        return List.of("01234", " ");
    }

    @ParameterizedTest
    @MethodSource("strings")
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void racyString(String orig) {
        String racyString = racyStringConstruction(orig);
        // The contents are indeterminate due to the race
        assertTrue(validCoder(racyString), orig + " string invalid"
                + ", racyString: " + inspectString(racyString));
    }

    @ParameterizedTest
    @MethodSource("strings")
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void racyCodePoint(String orig) {
        String iffyString = racyStringConstructionCodepoints(orig);
        // The contents are indeterminate due to the race
        assertTrue(validCoder(iffyString), "invalid coder in non-deterministic string"
                + ", orig:" + inspectString(orig)
                + ", iffyString: " + inspectString(iffyString));
    }

    @ParameterizedTest
    @MethodSource("strings")
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void racyCodePointSurrogates(String orig) {
        String iffyString = racyStringConstructionCodepointsSurrogates(orig);
        // The contents are indeterminate due to the race
        if (!orig.equals(iffyString))
            System.err.println("orig: " + orig + ", iffy: " + iffyString + Arrays.toString(iffyString.codePoints().toArray()));
        assertTrue(validCoder(iffyString), "invalid coder in non-deterministic string"
                + ", orig:" + inspectString(orig)
                + ", iffyString: " + inspectString(iffyString));
    }

    // Test the private methods of StringUTF16 that compress and copy COMPRESSED_STRING
    // encoded byte arrays.
    @Test
    public void verifyUTF16CopyBytes()
            throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class<?> stringUTF16 = Class.forName("java.lang.StringUTF16");
        Method mCompressChars = stringUTF16.getDeclaredMethod("compress",
                char[].class, int.class, byte[].class, int.class, int.class);
        mCompressChars.setAccessible(true);

        // First warmup the intrinsic and check 1 case
        char[] chars = {'a', 'b', 'c', 0xff21, 0xff22, 0xff23};
        byte[] bytes = new byte[chars.length];
        int printWarningCount = 0;

        for (int i = 0; i < 1_000_000; i++) {   // repeat to get C2 to kick in
            // Copy only latin1 chars from UTF-16 converted prefix (3 chars -> 3 bytes)
            int intResult = (int) mCompressChars.invoke(null, chars, 0, bytes, 0, chars.length);
            if (intResult == 0) {
                if (printWarningCount == 0) {
                    printWarningCount = 1;
                    System.err.println("Intrinsic for StringUTF16.compress returned 0, may not have been updated.");
                }
            } else {
                assertEquals(3, intResult, "return length not-equal, iteration: " + i);
            }
        }

        // Exhaustively check compress returning the correct index of the non-latin1 char.
        final int SIZE = 48;
        final byte FILL_BYTE = 'R';
        chars = new char[SIZE];
        bytes = new byte[chars.length];
        for (int i = 0; i < SIZE; i++) { // Every starting index
            for (int j = i; j < SIZE; j++) {  // Every location of non-latin1
                Arrays.fill(chars, 'A');
                Arrays.fill(bytes, FILL_BYTE);
                chars[j] = 0xFF21;
                int intResult = (int) mCompressChars.invoke(null, chars, i, bytes, 0, chars.length - i);
                assertEquals(j - i, intResult, "compress found wrong index");
                assertEquals(FILL_BYTE, bytes[j], "extra character stored");
            }
        }

    }

    // Check that a concatenated "hello" has a valid coder
    @Test
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void checkConcatAndIntern() {
        var helloWorld = "hello world";
        String helloToo = racyStringConstruction("hell".concat("o"));
        String o = helloToo.intern();
        var hello = "hello";
        assertTrue(validCoder(helloToo), "startsWith: "
                + ", hell: " + inspectString(helloToo)
                + ", o: " + inspectString(o)
                + ", hello: " + inspectString(hello)
                + ", hello world: " + inspectString(helloWorld));
    }

    // Check that an empty string with racy construction has a valid coder
    @Test
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    public void racyEmptyString() {
        var space = racyStringConstruction(" ");
        var trimmed = space.trim();
        assertTrue(validCoder(trimmed), "empty string invalid coder"
                + ", trimmed: " + inspectString(trimmed));
    }

    // Check that an exception in a user implemented CharSequence doesn't result in
    // an invalid coder when appended to a StringBuilder
    @Test
    @EnabledIf("test.java.lang.String.StringRacyConstructor#isCompactStrings")
    void charSequenceException() {
        ThrowingCharSequence bs = new ThrowingCharSequence("A\u2030\uFFFD");
        var sb = new StringBuilder();
        try {
            sb.append(bs);
            fail("An IllegalArgumentException should have been thrown");
        } catch (IllegalArgumentException ex) {
            // ignore expected
        }
        assertTrue(validCoder(sb), "invalid coder in StringBuilder");
    }

    /**
     * Given a latin-1 String, attempt to create a copy that is
     * incorrectly encoded as UTF-16.
     */
    public static String racyStringConstruction(String original) throws ConcurrentModificationException {
        if (original.chars().max().getAsInt() >= 256) {
            throw new IllegalArgumentException(
                    "Only work with latin-1 Strings");
        }

        char[] chars = original.toCharArray();

        // In another thread, flip the first character back
        // and forth between being latin-1 or not
        Thread thread = new Thread(() -> {
            while (!Thread.interrupted()) {
                chars[0] ^= 256;
            }
        });
        thread.start();

        // at the same time call the String constructor,
        // until we hit the race condition
        int i = 0;
        while (true) {
            i++;
            String s = new String(chars);
            if ((s.charAt(0) < 256 && !original.equals(s)) || i > 1_000_000) {
                thread.interrupt();
                try {
                    thread.join();
                } catch (InterruptedException ie) {
                    // ignore interrupt
                }
                return s;
            }
        }
    }

    /**
     * Given a latin-1 String, creates a copy that is
     * incorrectly encoded as UTF-16 using the APIs for Codepoints.
     */
    public static String racyStringConstructionCodepoints(String original) throws ConcurrentModificationException {
        if (original.chars().max().getAsInt() >= 256) {
            throw new IllegalArgumentException(
                    "Can only work with latin-1 Strings");
        }

        int len = original.length();
        int[] codePoints = new int[len];
        for (int i = 0; i < len; i++) {
            codePoints[i] = original.charAt(i);
        }

        // In another thread, flip the first character back
        // and forth between being latin-1 or not
        Thread thread = new Thread(() -> {
            while (!Thread.interrupted()) {
                codePoints[0] ^= 256;
            }
        });
        thread.start();

        // at the same time call the String constructor,
        // until we hit the race condition
        int i = 0;
        while (true) {
            i++;
            String s = new String(codePoints, 0, len);
            if ((s.charAt(0) < 256 && !original.equals(s)) || i > 1_000_000) {
                thread.interrupt();
                try {
                    thread.join();
                } catch (InterruptedException ie) {
                    // ignore interrupt
                }
                return s;
            }
        }
    }

    /**
     * Returns a string created from a codepoint array that has been racily
     * modified to contain high and low surrogates. The string is a different length
     * than the original due to the surrogate encoding.
     */
    public static String racyStringConstructionCodepointsSurrogates(String original) throws ConcurrentModificationException {
        if (original.chars().max().getAsInt() >= 256) {
            throw new IllegalArgumentException(
                    "Can only work with latin-1 Strings");
        }

        int len = original.length();
        int[] codePoints = new int[len];
        for (int i = 0; i < len; i++) {
            codePoints[i] = original.charAt(i);
        }

        // In another thread, flip the first character back
        // and forth between being latin-1 or as a surrogate pair.
        Thread thread = new Thread(() -> {
            while (!Thread.interrupted()) {
                codePoints[0] ^= 0x10000;
            }
        });
        thread.start();

        // at the same time call the String constructor,
        // until we hit the race condition
        int i = 0;
        while (true) {
            i++;
            String s = new String(codePoints, 0, len);
            if ((s.length() != original.length()) || i > 1_000_000) {
                thread.interrupt();
                try {
                    thread.join();
                } catch (InterruptedException ie) {
                    // ignore interrupt
                }
                return s;
            }
        }
    }

    // A CharSequence that returns characters from a string and throws IllegalArgumentException
    // when the character requested is 0xFFFD (the replacement character)
    // The string contents determine when the exception is thrown.
    static class ThrowingCharSequence implements CharSequence {
        private final String aString;

        ThrowingCharSequence(String aString) {
            this.aString = aString;
        }

        @Override
        public int length() {
            return aString.length();
        }

        @Override
        public char charAt(int index) {
            char ch = aString.charAt(index);
            if (ch == 0xFFFD) {
                throw new IllegalArgumentException("Replacement character at index " + index);
            }
            return ch;
        }

        @Override
        // Not used; returns the entire string
        public CharSequence subSequence(int start, int end) {
            return this;
        }
    }
}