Skip to content

Commit 0281bfe

Browse files
authored
Merge pull request github#10689 from d10c/swift/cleartext-storage-nsuserdefaults
Swift: Query for CWE-312: Exposure of sensitive information using NSUserDefaults
2 parents 13f9834 + b7ad287 commit 0281bfe

File tree

9 files changed

+357
-7
lines changed

9 files changed

+357
-7
lines changed

swift/README.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
# Swift on CodeQL
2+
13
## Warning
24

3-
The Swift codeql package is an experimental and unsupported work in progress.
5+
The Swift CodeQL package is an experimental and unsupported work in progress.
46

5-
## Usage
7+
## Building the Swift extractor
68

79
First ensure you have Bazel installed, for example with
810

@@ -13,16 +15,17 @@ brew install bazelisk
1315
then from the `ql` directory run
1416

1517
```bash
16-
bazel run //swift:create-extractor-pack
18+
bazel run //swift:create-extractor-pack # --cpu=darwin_x86_64 # Uncomment on Arm-based Macs
1719
```
1820

1921
which will install `swift/extractor-pack`.
2022

21-
Using `--search-path=swift/extractor-pack` will then pick up the Swift extractor. You can also use
22-
`--search-path=.`, as the extractor pack is mentioned in the root `codeql-workspace.yml`.
23-
2423
Notice you can run `bazel run :create-extractor-pack` if you already are in the `swift` directory.
2524

25+
Using `codeql ... --search-path=swift/extractor-pack` will then pick up the Swift extractor. You can also use
26+
`--search-path=.`, as the extractor pack is mentioned in the root `codeql-workspace.yml`. Alternatively, you can
27+
set up the search path in [the per-user CodeQL configuration file](https://codeql.github.com/docs/codeql-cli/specifying-command-options-in-a-codeql-configuration-file/#using-a-codeql-configuration-file).
28+
2629
## Code generation
2730

2831
Run
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE qhelp PUBLIC "-//Semmle//qhelp//EN" "qhelp.dtd">
2+
<qhelp>
3+
<overview>
4+
5+
<p>Sensitive information that is stored unencrypted in an application preference store, such as the user defaults database or the iCloud-backed ubiquitous key-value store, is accessible to an attacker who gains access to that data store. For example, the information could be accessed by any process or user in a rooted device, by compromised app extensions, or could be exposed through another vulnerability.</p>
6+
7+
</overview>
8+
<recommendation>
9+
10+
<p>Either store the data in an encrypted database, or ensure that each piece of sensitive information is encrypted before being stored. In general, decrypt sensitive information only at the point where it is necessary for it to be used in cleartext. Avoid storing sensitive information at all if you do not need to keep it.</p>
11+
12+
</recommendation>
13+
<example>
14+
15+
<p>The following example shows three cases of storing information using UserDefaults. In the 'BAD' case, the data that is stored is sensitive (a credit card number) and is not encrypted. In the 'GOOD' cases, the data is either not sensitive, or is protected with encryption.</p>
16+
17+
<sample src="CleartextStoragePreferences.swift" />
18+
19+
</example>
20+
<references>
21+
22+
<li>
23+
OWASP Top 10:2021:
24+
<a href="https://owasp.org/Top10/A02_2021-Cryptographic_Failures/">A02:2021 &mdash; Cryptographic Failures</a>.
25+
</li>
26+
<li>
27+
Apple Developer Documentation: <a href="https://developer.apple.com/documentation/foundation/userdefaults">UserDefaults</a>, <a href="https://developer.apple.com/documentation/foundation/nsubiquitouskeyvaluestore">NSUbiquitousKeyValueStore</a>
28+
</li>
29+
30+
</references>
31+
</qhelp>
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
/**
2+
* @name Cleartext storage of sensitive information in an application preference store
3+
* @description Storing sensitive information in a non-encrypted store can expose it to an attacker.
4+
* @kind path-problem
5+
* @problem.severity warning
6+
* @security-severity 7.5
7+
* @precision medium
8+
* @id swift/cleartext-storage-preferences
9+
* @tags security
10+
* external/cwe/cwe-312
11+
*/
12+
13+
import swift
14+
import codeql.swift.security.SensitiveExprs
15+
import codeql.swift.dataflow.DataFlow
16+
import codeql.swift.dataflow.TaintTracking
17+
import DataFlow::PathGraph
18+
19+
/**
20+
* A `DataFlow::Node` of something that gets stored in an application preference store.
21+
*/
22+
abstract class Stored extends DataFlow::Node {
23+
abstract string getStoreName();
24+
}
25+
26+
/** The `DataFlow::Node` of an expression that gets written to the user defaults database */
27+
class UserDefaultsStore extends Stored {
28+
UserDefaultsStore() {
29+
exists(ClassDecl c, AbstractFunctionDecl f, CallExpr call |
30+
c.getName() = "UserDefaults" and
31+
c.getAMember() = f and
32+
f.getName() = "set(_:forKey:)" and
33+
call.getStaticTarget() = f and
34+
call.getArgument(0).getExpr() = this.asExpr()
35+
)
36+
}
37+
38+
override string getStoreName() { result = "the user defaults database" }
39+
}
40+
41+
/** The `DataFlow::Node` of an expression that gets written to the iCloud-backed NSUbiquitousKeyValueStore */
42+
class NSUbiquitousKeyValueStore extends Stored {
43+
NSUbiquitousKeyValueStore() {
44+
exists(ClassDecl c, AbstractFunctionDecl f, CallExpr call |
45+
c.getName() = "NSUbiquitousKeyValueStore" and
46+
c.getAMember() = f and
47+
f.getName() = "set(_:forKey:)" and
48+
call.getStaticTarget() = f and
49+
call.getArgument(0).getExpr() = this.asExpr()
50+
)
51+
}
52+
53+
override string getStoreName() { result = "iCloud" }
54+
}
55+
56+
/**
57+
* A more complicated case, this is a macOS-only way of writing to
58+
* NSUserDefaults by modifying the `NSUserDefaultsController.values: Any`
59+
* object via reflection (`perform(Selector)`) or the `NSKeyValueCoding`,
60+
* `NSKeyValueBindingCreation` APIs. (TODO)
61+
*/
62+
class NSUserDefaultsControllerStore extends Stored {
63+
NSUserDefaultsControllerStore() { none() }
64+
65+
override string getStoreName() { result = "the user defaults database" }
66+
}
67+
68+
/**
69+
* A taint configuration from sensitive information to expressions that are
70+
* stored as preferences.
71+
*/
72+
class CleartextStorageConfig extends TaintTracking::Configuration {
73+
CleartextStorageConfig() { this = "CleartextStorageConfig" }
74+
75+
override predicate isSource(DataFlow::Node node) { node.asExpr() instanceof SensitiveExpr }
76+
77+
override predicate isSink(DataFlow::Node node) { node instanceof Stored }
78+
79+
override predicate isSanitizerIn(DataFlow::Node node) {
80+
// make sources barriers so that we only report the closest instance
81+
this.isSource(node)
82+
}
83+
84+
override predicate isSanitizer(DataFlow::Node node) {
85+
// encryption barrier
86+
node.asExpr() instanceof EncryptedExpr
87+
}
88+
}
89+
90+
/**
91+
* Gets a prettier node to use in the results.
92+
*/
93+
DataFlow::Node cleanupNode(DataFlow::Node n) {
94+
result = n.(DataFlow::PostUpdateNode).getPreUpdateNode()
95+
or
96+
not n instanceof DataFlow::PostUpdateNode and
97+
result = n
98+
}
99+
100+
from CleartextStorageConfig config, DataFlow::PathNode sourceNode, DataFlow::PathNode sinkNode
101+
where config.hasFlowPath(sourceNode, sinkNode)
102+
select cleanupNode(sinkNode.getNode()), sourceNode, sinkNode,
103+
"This operation stores '" + sinkNode.getNode().toString() + "' in " +
104+
sinkNode.getNode().(Stored).getStoreName() +
105+
". It may contain unencrypted sensitive data from $@.", sourceNode,
106+
sourceNode.getNode().toString()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
func storeMyData(faveSong : String, creditCardNo : String) {
3+
// ...
4+
5+
// GOOD: not sensitive information
6+
UserDefaults.standard.set(faveSong, forKey: "myFaveSong")
7+
8+
// BAD: sensitive information saved in cleartext
9+
UserDefaults.standard.set(creditCardNo, forKey: "myCreditCardNo")
10+
11+
// GOOD: encrypted sensitive information saved
12+
UserDefaults.standard.set(encrypt(creditCardNo), forKey: "myCreditCardNo")
13+
14+
// ...
15+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
edges
2+
| testNSUbiquitousKeyValueStore.swift:41:24:41:24 | x : | testNSUbiquitousKeyValueStore.swift:42:40:42:40 | x |
3+
| testNSUbiquitousKeyValueStore.swift:44:10:44:22 | call to getPassword() : | testNSUbiquitousKeyValueStore.swift:45:40:45:40 | y |
4+
| testNSUbiquitousKeyValueStore.swift:55:10:55:10 | passwd : | testNSUbiquitousKeyValueStore.swift:59:40:59:40 | x |
5+
| testNSUbiquitousKeyValueStore.swift:56:10:56:10 | passwd : | testNSUbiquitousKeyValueStore.swift:60:40:60:40 | y |
6+
| testNSUbiquitousKeyValueStore.swift:57:10:57:10 | passwd : | testNSUbiquitousKeyValueStore.swift:61:40:61:40 | z |
7+
| testUserDefaults.swift:41:24:41:24 | x : | testUserDefaults.swift:42:28:42:28 | x |
8+
| testUserDefaults.swift:44:10:44:22 | call to getPassword() : | testUserDefaults.swift:45:28:45:28 | y |
9+
| testUserDefaults.swift:55:10:55:10 | passwd : | testUserDefaults.swift:59:28:59:28 | x |
10+
| testUserDefaults.swift:56:10:56:10 | passwd : | testUserDefaults.swift:60:28:60:28 | y |
11+
| testUserDefaults.swift:57:10:57:10 | passwd : | testUserDefaults.swift:61:28:61:28 | z |
12+
nodes
13+
| testNSUbiquitousKeyValueStore.swift:28:12:28:12 | password | semmle.label | password |
14+
| testNSUbiquitousKeyValueStore.swift:41:24:41:24 | x : | semmle.label | x : |
15+
| testNSUbiquitousKeyValueStore.swift:42:40:42:40 | x | semmle.label | x |
16+
| testNSUbiquitousKeyValueStore.swift:44:10:44:22 | call to getPassword() : | semmle.label | call to getPassword() : |
17+
| testNSUbiquitousKeyValueStore.swift:45:40:45:40 | y | semmle.label | y |
18+
| testNSUbiquitousKeyValueStore.swift:49:40:49:42 | .password | semmle.label | .password |
19+
| testNSUbiquitousKeyValueStore.swift:55:10:55:10 | passwd : | semmle.label | passwd : |
20+
| testNSUbiquitousKeyValueStore.swift:56:10:56:10 | passwd : | semmle.label | passwd : |
21+
| testNSUbiquitousKeyValueStore.swift:57:10:57:10 | passwd : | semmle.label | passwd : |
22+
| testNSUbiquitousKeyValueStore.swift:59:40:59:40 | x | semmle.label | x |
23+
| testNSUbiquitousKeyValueStore.swift:60:40:60:40 | y | semmle.label | y |
24+
| testNSUbiquitousKeyValueStore.swift:61:40:61:40 | z | semmle.label | z |
25+
| testUserDefaults.swift:28:15:28:15 | password | semmle.label | password |
26+
| testUserDefaults.swift:41:24:41:24 | x : | semmle.label | x : |
27+
| testUserDefaults.swift:42:28:42:28 | x | semmle.label | x |
28+
| testUserDefaults.swift:44:10:44:22 | call to getPassword() : | semmle.label | call to getPassword() : |
29+
| testUserDefaults.swift:45:28:45:28 | y | semmle.label | y |
30+
| testUserDefaults.swift:49:28:49:30 | .password | semmle.label | .password |
31+
| testUserDefaults.swift:55:10:55:10 | passwd : | semmle.label | passwd : |
32+
| testUserDefaults.swift:56:10:56:10 | passwd : | semmle.label | passwd : |
33+
| testUserDefaults.swift:57:10:57:10 | passwd : | semmle.label | passwd : |
34+
| testUserDefaults.swift:59:28:59:28 | x | semmle.label | x |
35+
| testUserDefaults.swift:60:28:60:28 | y | semmle.label | y |
36+
| testUserDefaults.swift:61:28:61:28 | z | semmle.label | z |
37+
subpaths
38+
#select
39+
| testNSUbiquitousKeyValueStore.swift:28:12:28:12 | password | testNSUbiquitousKeyValueStore.swift:28:12:28:12 | password | testNSUbiquitousKeyValueStore.swift:28:12:28:12 | password | This operation stores 'password' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:28:12:28:12 | password | password |
40+
| testNSUbiquitousKeyValueStore.swift:42:40:42:40 | x | testNSUbiquitousKeyValueStore.swift:41:24:41:24 | x : | testNSUbiquitousKeyValueStore.swift:42:40:42:40 | x | This operation stores 'x' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:41:24:41:24 | x : | x |
41+
| testNSUbiquitousKeyValueStore.swift:45:40:45:40 | y | testNSUbiquitousKeyValueStore.swift:44:10:44:22 | call to getPassword() : | testNSUbiquitousKeyValueStore.swift:45:40:45:40 | y | This operation stores 'y' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:44:10:44:22 | call to getPassword() : | call to getPassword() |
42+
| testNSUbiquitousKeyValueStore.swift:49:40:49:42 | .password | testNSUbiquitousKeyValueStore.swift:49:40:49:42 | .password | testNSUbiquitousKeyValueStore.swift:49:40:49:42 | .password | This operation stores '.password' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:49:40:49:42 | .password | .password |
43+
| testNSUbiquitousKeyValueStore.swift:59:40:59:40 | x | testNSUbiquitousKeyValueStore.swift:55:10:55:10 | passwd : | testNSUbiquitousKeyValueStore.swift:59:40:59:40 | x | This operation stores 'x' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:55:10:55:10 | passwd : | passwd |
44+
| testNSUbiquitousKeyValueStore.swift:60:40:60:40 | y | testNSUbiquitousKeyValueStore.swift:56:10:56:10 | passwd : | testNSUbiquitousKeyValueStore.swift:60:40:60:40 | y | This operation stores 'y' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:56:10:56:10 | passwd : | passwd |
45+
| testNSUbiquitousKeyValueStore.swift:61:40:61:40 | z | testNSUbiquitousKeyValueStore.swift:57:10:57:10 | passwd : | testNSUbiquitousKeyValueStore.swift:61:40:61:40 | z | This operation stores 'z' in iCloud. It may contain unencrypted sensitive data from $@. | testNSUbiquitousKeyValueStore.swift:57:10:57:10 | passwd : | passwd |
46+
| testUserDefaults.swift:28:15:28:15 | password | testUserDefaults.swift:28:15:28:15 | password | testUserDefaults.swift:28:15:28:15 | password | This operation stores 'password' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:28:15:28:15 | password | password |
47+
| testUserDefaults.swift:42:28:42:28 | x | testUserDefaults.swift:41:24:41:24 | x : | testUserDefaults.swift:42:28:42:28 | x | This operation stores 'x' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:41:24:41:24 | x : | x |
48+
| testUserDefaults.swift:45:28:45:28 | y | testUserDefaults.swift:44:10:44:22 | call to getPassword() : | testUserDefaults.swift:45:28:45:28 | y | This operation stores 'y' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:44:10:44:22 | call to getPassword() : | call to getPassword() |
49+
| testUserDefaults.swift:49:28:49:30 | .password | testUserDefaults.swift:49:28:49:30 | .password | testUserDefaults.swift:49:28:49:30 | .password | This operation stores '.password' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:49:28:49:30 | .password | .password |
50+
| testUserDefaults.swift:59:28:59:28 | x | testUserDefaults.swift:55:10:55:10 | passwd : | testUserDefaults.swift:59:28:59:28 | x | This operation stores 'x' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:55:10:55:10 | passwd : | passwd |
51+
| testUserDefaults.swift:60:28:60:28 | y | testUserDefaults.swift:56:10:56:10 | passwd : | testUserDefaults.swift:60:28:60:28 | y | This operation stores 'y' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:56:10:56:10 | passwd : | passwd |
52+
| testUserDefaults.swift:61:28:61:28 | z | testUserDefaults.swift:57:10:57:10 | passwd : | testUserDefaults.swift:61:28:61:28 | z | This operation stores 'z' in the user defaults database. It may contain unencrypted sensitive data from $@. | testUserDefaults.swift:57:10:57:10 | passwd : | passwd |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
queries/Security/CWE-312/CleartextStoragePreferences.ql
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
// --- stubs ---
3+
4+
class NSObject
5+
{
6+
}
7+
8+
class NSUbiquitousKeyValueStore : NSObject
9+
{
10+
class var `default`: NSUbiquitousKeyValueStore {
11+
return NSUbiquitousKeyValueStore()
12+
}
13+
14+
func set(_ anObject: Any?, forKey aKey: String) {}
15+
}
16+
17+
func encrypt(_ data: String) -> String { return data }
18+
func hash(data: inout String) { }
19+
20+
func getPassword() -> String { return "" }
21+
func doSomething(password: String) { }
22+
23+
// --- tests ---
24+
25+
func test1(password: String, passwordHash : String) {
26+
let store = NSUbiquitousKeyValueStore.default
27+
28+
store.set(password, forKey: "myKey") // BAD
29+
store.set(passwordHash, forKey: "myKey") // GOOD (not sensitive)
30+
}
31+
32+
class MyClass {
33+
var harmless = "abc"
34+
var password = "123"
35+
}
36+
37+
func test3(x: String) {
38+
// alternative evidence of sensitivity...
39+
40+
NSUbiquitousKeyValueStore.default.set(x, forKey: "myKey") // BAD [NOT REPORTED]
41+
doSomething(password: x);
42+
NSUbiquitousKeyValueStore.default.set(x, forKey: "myKey") // BAD
43+
44+
let y = getPassword();
45+
NSUbiquitousKeyValueStore.default.set(y, forKey: "myKey") // BAD
46+
47+
let z = MyClass()
48+
NSUbiquitousKeyValueStore.default.set(z.harmless, forKey: "myKey") // GOOD (not sensitive)
49+
NSUbiquitousKeyValueStore.default.set(z.password, forKey: "myKey") // BAD
50+
}
51+
52+
func test4(passwd: String) {
53+
// sanitizers...
54+
55+
var x = passwd;
56+
var y = passwd;
57+
var z = passwd;
58+
59+
NSUbiquitousKeyValueStore.default.set(x, forKey: "myKey") // BAD
60+
NSUbiquitousKeyValueStore.default.set(y, forKey: "myKey") // BAD
61+
NSUbiquitousKeyValueStore.default.set(z, forKey: "myKey") // BAD
62+
63+
x = encrypt(x);
64+
hash(data: &y);
65+
z = "";
66+
67+
NSUbiquitousKeyValueStore.default.set(x, forKey: "myKey") // GOOD (not sensitive)
68+
NSUbiquitousKeyValueStore.default.set(y, forKey: "myKey") // GOOD (not sensitive)
69+
NSUbiquitousKeyValueStore.default.set(z, forKey: "myKey") // GOOD (not sensitive)
70+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
2+
// --- stubs ---
3+
4+
class NSObject
5+
{
6+
}
7+
8+
class UserDefaults : NSObject
9+
{
10+
class var standard: UserDefaults {
11+
return UserDefaults()
12+
}
13+
14+
func set(_ value: Any?, forKey defaultName: String) {}
15+
}
16+
17+
func encrypt(_ data: String) -> String { return data }
18+
func hash(data: inout String) { }
19+
20+
func getPassword() -> String { return "" }
21+
func doSomething(password: String) { }
22+
23+
// --- tests ---
24+
25+
func test1(password: String, passwordHash : String) {
26+
let defaults = UserDefaults.standard
27+
28+
defaults.set(password, forKey: "myKey") // BAD
29+
defaults.set(passwordHash, forKey: "myKey") // GOOD (not sensitive)
30+
}
31+
32+
class MyClass {
33+
var harmless = "abc"
34+
var password = "123"
35+
}
36+
37+
func test3(x: String) {
38+
// alternative evidence of sensitivity...
39+
40+
UserDefaults.standard.set(x, forKey: "myKey") // BAD [NOT REPORTED]
41+
doSomething(password: x);
42+
UserDefaults.standard.set(x, forKey: "myKey") // BAD
43+
44+
let y = getPassword();
45+
UserDefaults.standard.set(y, forKey: "myKey") // BAD
46+
47+
let z = MyClass()
48+
UserDefaults.standard.set(z.harmless, forKey: "myKey") // GOOD (not sensitive)
49+
UserDefaults.standard.set(z.password, forKey: "myKey") // BAD
50+
}
51+
52+
func test4(passwd: String) {
53+
// sanitizers...
54+
55+
var x = passwd;
56+
var y = passwd;
57+
var z = passwd;
58+
59+
UserDefaults.standard.set(x, forKey: "myKey") // BAD
60+
UserDefaults.standard.set(y, forKey: "myKey") // BAD
61+
UserDefaults.standard.set(z, forKey: "myKey") // BAD
62+
63+
x = encrypt(x);
64+
hash(data: &y);
65+
z = "";
66+
67+
UserDefaults.standard.set(x, forKey: "myKey") // GOOD (not sensitive)
68+
UserDefaults.standard.set(y, forKey: "myKey") // GOOD (not sensitive)
69+
UserDefaults.standard.set(z, forKey: "myKey") // GOOD (not sensitive)
70+
}

swift/tools/qltest.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ for src in *.swift; do
1010
opts=(-sdk "$CODEQL_EXTRACTOR_SWIFT_ROOT/qltest/$CODEQL_PLATFORM/sdk" -c -primary-file $src)
1111
opts+=($(sed -n '1 s=//codeql-extractor-options:==p' $src))
1212
echo -e "calling extractor with flags: ${opts[@]}\n" >> $QLTEST_LOG
13-
"$CODEQL_EXTRACTOR_SWIFT_ROOT/tools/$CODEQL_PLATFORM/extractor" "${opts[@]}" >> $QLTEST_LOG 2>&1
13+
"$CODEQL_EXTRACTOR_SWIFT_ROOT/tools/$CODEQL_PLATFORM/extractor" "${opts[@]}" >> $QLTEST_LOG 2>&1 || FAILED=1
1414
done
15+
16+
[ -z "$FAILED" ] || cat "$QLTEST_LOG" # Show compiler errors on extraction failure

0 commit comments

Comments
 (0)