Skip to content

fix(security): patch shell blocklist bypass and Android rm flag permutation#248

Merged
qhkm merged 3 commits intomainfrom
fix/security-advisories
Mar 4, 2026
Merged

fix(security): patch shell blocklist bypass and Android rm flag permutation#248
qhkm merged 3 commits intomainfrom
fix/security-advisories

Conversation

@qhkm
Copy link
Copy Markdown
Owner

@qhkm qhkm commented Mar 4, 2026

Summary

Fixes three GitHub Security Advisories reported by @zpbrent:

  • GHSA-hhjv-jq77-cmvx (HIGH): Android device_shell() blocklist bypass via argument permutation — rm -r -f, rm -fr, rm --recursive --force all now caught by flag-set parsing instead of naive literal substring matching
  • GHSA-5wp8-q9mx-8jx8 (CRITICAL): Shell allowlist/blocklist bypass via four vectors:
    1. Command injection metacharacters (;, |, `, $()) now detected before allowlist check
    2. Regex patterns for python/perl/ruby/node -c/-e now match combined flags (python3 -Bc, perl -we)
    3. Glob wildcards (?, *, [x]) in commands no longer bypass literal path blocklist
    4. Empty allowlist in Strict mode now blocks all commands (was silently allowing everything)
  • GHSA-j8q9-r9pq-2hh9 (HIGH): Already fixed — IPv6-to-IPv4 transition address checks were present

Test plan

  • All 2924 lib tests pass
  • New tests for rm flag permutations (6 variants + safe cases)
  • New tests for command injection via semicolon, pipe, subshell
  • New tests for python/perl argument injection (-P -c, -Bc, -w -e)
  • New tests for glob wildcard bypass (/etc/pass[w]d, /etc/shado?, /etc/sh[a]dow)
  • New test for empty strict allowlist blocking everything
  • Existing safe-command tests still pass (no regressions)
  • cargo clippy -- -D warnings clean
  • cargo fmt -- --check clean

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security Improvements

    • Improved shell command validation to block injection/chaining metacharacters, inline-code invocations, and glob/bracket-based bypasses.
    • Allowlist behavior tightened: empty strict allowlist now blocks all; Warn mode logs suspicious patterns; first-token checks handle path prefixes and executable names.
    • Added explicit blocking for dangerous commands and recursive+force file operations.
  • Tests

    • Expanded coverage for injection vectors, inline-code flags, glob/bracket bypasses, and rm/flag permutations.

…tation

Address three GitHub Security Advisories:

- GHSA-hhjv-jq77-cmvx (HIGH): Replace naive literal substring blocklist
  in Android device_shell() with flag-set parsing that catches all rm
  flag permutations (rm -r -f, rm -fr, rm --recursive --force, etc.)

- GHSA-5wp8-q9mx-8jx8 (CRITICAL): Fix four shell security bypasses:
  1. Allowlist bypass via command injection — detect metacharacters
     (;, |, backtick, $()) before allowlist first-token check
  2. Regex bypass via argument injection — patterns now match
     combined flags (python3 -Bc, perl -we, etc.)
  3. Literal bypass via glob wildcards — strip bracket contents and
     convert ?, * to regex wildcards for matching
  4. Empty allowlist in Strict mode now blocks all commands instead
     of silently allowing everything

- GHSA-j8q9-r9pq-2hh9 (HIGH): Already fixed — IPv6-to-IPv4 transition
  address checks were present in web.rs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 4, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Replaces brittle literal checks with glob-aware regex matching and stricter allowlist/metacharacter checks in src/security/shell.rs; adds rm -rf flag detection, early metacharacter guard, token-aware normalization, and keyword-based blocking in src/tools/android/actions.rs.

Changes

Cohort / File(s) Summary
Shell Security Hardening
src/security/shell.rs
Introduces private build_glob_regex and deglobbing to match glob/bracket literals, expands regex-based detection for inline-code invocations, augments allowlist logic (Off/Warn/Strict), detects chaining metacharacters and $(, and updates tests for many bypass vectors. Public API: ShellSecurityConfig::with_allowlist(..., mode: ShellAllowlistMode) -> Self; ShellAllowlistMode includes Off, Warn, Strict.
Android Device Shell Hardening
src/tools/android/actions.rs
Adds token_basename, rm_invocation_args, is_rm_recursive_force, blocks dangerous metacharacters early, normalizes/tokenizes commands, implements flag-aware blocking for rm -r+-f (including busybox/toybox/path-prefixed), and applies a keyword-based blocklist (reboot, factory_reset, wipe, format, dd if=, mkfs, flash, fastboot). Tests updated accordingly.

Sequence Diagram(s)

(None — changes are internal validation logic and do not introduce a multi-component sequential flow that benefits from a diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~40 minutes

Possibly related PRs

Poem

🐰 Hopping through globbed paths at night,
I sniff for semicolons, pipes, and fright,
Brackets, subshells, flags — no sly escape,
I nibble bad tokens, seal each loose gape,
A joyful thump — safer shells in sight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main security fixes in the changeset: shell blocklist bypass patching and Android rm flag permutation handling.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/security-advisories

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/tools/android/actions.rs (1)

477-495: ⚠️ Potential issue | 🟡 Minor

Potential false positive with "format" substring.

The substring match for "format" could trigger false positives on commands containing the word "format" in other contexts (e.g., date +"%Y-%m-%d" with format-related options, or filenames like format_output.txt).

Consider using word boundaries or a more targeted pattern if this becomes an issue in practice.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/android/actions.rs` around lines 477 - 495, The current substring
check in blocked_keywords can yield false positives for tokens like "format";
update the check to match whole-word or anchored patterns instead of simple
contains: replace the simple array+contains logic (referencing blocked_keywords
and the lower variable) with a small list of regex patterns (e.g., use
word-boundary patterns like \bformat\b for plain words, and keep exact substring
patterns such as "dd if=" or "mkfs" as-is) and test each against lower using the
regex crate (or precompile Regexes) so only true dangerous tokens are blocked.
🧹 Nitpick comments (1)
src/tools/android/actions.rs (1)

465-475: Consider extracting recursive flag detection to reduce duplication.

The recursive flag detection logic (--recursive or short flag containing r) is duplicated between is_rm_recursive_force and this inline check. While acceptable, extracting a helper like has_recursive_flag(tokens) would reduce redundancy.

♻️ Optional refactor to extract helper
+fn has_recursive_flag(tokens: &[&str]) -> bool {
+    tokens.iter().skip(1).any(|t| {
+        *t == "--recursive" || (t.starts_with('-') && !t.starts_with("--") && t.contains('r'))
+    })
+}
+
 fn is_rm_recursive_force(tokens: &[&str]) -> bool {
     if tokens.is_empty() || tokens[0] != "rm" {
         return false;
     }
-    let mut has_r = false;
+    let has_r = has_recursive_flag(tokens);
     let mut has_f = false;
-    for &tok in &tokens[1..] {
-        if tok.starts_with("--") {
-            match tok {
-                "--recursive" => has_r = true,
-                "--force" => has_f = true,
-                _ => {}
-            }
-        } else if tok.starts_with('-') && !tok.starts_with("--") {
-            let flags = &tok[1..];
-            if flags.contains('r') {
-                has_r = true;
-            }
-            if flags.contains('f') {
-                has_f = true;
-            }
+    for &tok in &tokens[1..] {
+        if tok == "--force" || (tok.starts_with('-') && !tok.starts_with("--") && tok.contains('f')) {
+            has_f = true;
         }
     }
     has_r && has_f
 }

Then use has_recursive_flag(&lower_tokens) at line 467.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/tools/android/actions.rs` around lines 465 - 475, Extract the duplicated
recursive-flag detection into a helper function (e.g., has_recursive_flag) and
replace the inline check in the rm block with a call to that helper;
specifically, move the logic that checks for "--recursive" or a short flag
containing 'r' (currently duplicated in is_rm_recursive_force and the inline rm
check using lower_tokens) into has_recursive_flag(tokens: &[&str]) and then call
has_recursive_flag(&lower_tokens) inside the if lower_tokens.first() ==
Some(&"rm") { ... } block and update is_rm_recursive_force to use the new helper
as well.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/security/shell.rs`:
- Around line 375-378: The chaining detection boolean has_chaining_metachar
currently checks command_lower.chars().any(|c| matches!(c, ';' | '|' | '`' |
'\n')) || command_lower.contains("$(") but omits the ampersand, allowing
background/chained commands; update that expression to also treat '&' as a
metacharacter (e.g., include '&' in the matches! pattern or add a check for '&'
on command_lower) so has_chaining_metachar correctly flags commands like "cmd1 &
cmd2" that should be rejected.

---

Outside diff comments:
In `@src/tools/android/actions.rs`:
- Around line 477-495: The current substring check in blocked_keywords can yield
false positives for tokens like "format"; update the check to match whole-word
or anchored patterns instead of simple contains: replace the simple
array+contains logic (referencing blocked_keywords and the lower variable) with
a small list of regex patterns (e.g., use word-boundary patterns like \bformat\b
for plain words, and keep exact substring patterns such as "dd if=" or "mkfs"
as-is) and test each against lower using the regex crate (or precompile Regexes)
so only true dangerous tokens are blocked.

---

Nitpick comments:
In `@src/tools/android/actions.rs`:
- Around line 465-475: Extract the duplicated recursive-flag detection into a
helper function (e.g., has_recursive_flag) and replace the inline check in the
rm block with a call to that helper; specifically, move the logic that checks
for "--recursive" or a short flag containing 'r' (currently duplicated in
is_rm_recursive_force and the inline rm check using lower_tokens) into
has_recursive_flag(tokens: &[&str]) and then call
has_recursive_flag(&lower_tokens) inside the if lower_tokens.first() ==
Some(&"rm") { ... } block and update is_rm_recursive_force to use the new helper
as well.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d939371a-70ed-4d14-b6af-c69a637e3d13

📥 Commits

Reviewing files that changed from the base of the PR and between fda2f10 and e399874.

📒 Files selected for processing (2)
  • src/security/shell.rs
  • src/tools/android/actions.rs

Comment thread src/security/shell.rs
Addresses CodeRabbit review: `&` enables background execution and
command chaining (e.g. `git status & python -c 'evil'`) which bypasses
the first-token allowlist check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/security/shell.rs`:
- Around line 140-143: The inline-code regexes for python/perl/ruby/node
currently end with [\s=] which misses attached quoted forms like -e'...'; update
each pattern (the strings r"python[23]?\s+.*-[A-Za-z]*c[\s=]",
r"perl\s+.*-[A-Za-z]*e[\s=]", r"ruby\s+.*-[A-Za-z]*e[\s=]",
r"node\s+.*-[A-Za-z]*e[\s=]") to accept whitespace, equals, an immediate quote,
or end-of-string — e.g., replace [\s=] with (?:[\s=]|['"]|$) so patterns also
catch -e'code' and -e"code" forms.
- Around line 177-197: build_glob_regex currently strips '[' and ']' which
treats character classes like "[w0]" as literal text; change it to convert any
bracket class in command into a single-character wildcard by pushing '.' into
pat and skipping all characters until the matching ']' (handle an unterminated
'[' by treating it as literal '[' or as '.' plus the rest as appropriate), keep
existing escaping logic for other chars, and then anchor the resulting pattern
with '^' at the start and '$' at the end before calling Regex::new so matches
are for the whole path; update the function build_glob_regex to use pat,
command, and Regex::new accordingly and still return Option<Regex>.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 15ff5453-8d67-430f-8bc6-21cafe1fe5fd

📥 Commits

Reviewing files that changed from the base of the PR and between e399874 and 823e7d1.

📒 Files selected for processing (1)
  • src/security/shell.rs

Comment thread src/security/shell.rs
Comment on lines +140 to +143
r"python[23]?\s+.*-[A-Za-z]*c[\s=]",
r"perl\s+.*-[A-Za-z]*e[\s=]",
r"ruby\s+.*-[A-Za-z]*e[\s=]",
r"node\s+.*-[A-Za-z]*e[\s=]",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/security/shell.rs | head -200

Repository: qhkm/zeptoclaw

Length of output: 8046


🏁 Script executed:

sed -n '130,160p' src/security/shell.rs

Repository: qhkm/zeptoclaw

Length of output: 1388


🏁 Script executed:

python3 << 'EOF'
import re

# Current patterns from the code
current_patterns = [
    r"python[23]?\s+.*-[A-Za-z]*c[\s=]",
    r"perl\s+.*-[A-Za-z]*e[\s=]",
    r"ruby\s+.*-[A-Za-z]*e[\s=]",
    r"node\s+.*-[A-Za-z]*e[\s=]",
]

# Suggested patterns from the review
suggested_patterns = [
    r"python[23]?\s+.*-[A-Za-z]*c(?:\s|=|['\"]|$)",
    r"perl\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
    r"ruby\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
    r"node\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
]

# Test cases
test_cases = [
    "perl -e 'print 1'",           # Space before quote
    "perl -e='print 1'",           # Equals before quote
    "perl -e'print 1'",            # Attached quote (NO space/equals)
    "python3 -c 'print(1)'",       # Space before quote
    "python3 -c='print(1)'",       # Equals before quote
    "python3 -c'print(1)'",        # Attached quote
    "python3 -P -c'print(1)'",     # Extra flags + attached quote
    "ruby -e 'puts 1'",            # Space before quote
    "ruby -e'puts 1'",             # Attached quote
    "node -e 'console.log(1)'",    # Space before quote
    "node -e'console.log(1)'",     # Attached quote
]

print("=== CURRENT PATTERNS ===")
for i, pattern in enumerate(current_patterns):
    regex = re.compile(pattern)
    print(f"\nPattern {i}: {pattern}")
    for test in test_cases:
        match = regex.search(test)
        print(f"  {'✓' if match else '✗'} {test}")

print("\n\n=== SUGGESTED PATTERNS ===")
for i, pattern in enumerate(suggested_patterns):
    regex = re.compile(pattern)
    print(f"\nPattern {i}: {pattern}")
    for test in test_cases:
        match = regex.search(test)
        print(f"  {'✓' if match else '✗'} {test}")
EOF

Repository: qhkm/zeptoclaw

Length of output: 2617


Inline-code regexes miss attached quoted forms (e.g., perl -e'...').

The current patterns require [\s=] after -c/-e, allowing attached quote forms like perl -e'code' to bypass the filter. Update the patterns to include quote boundaries:

🔧 Suggested fix
-    r"python[23]?\s+.*-[A-Za-z]*c[\s=]",
-    r"perl\s+.*-[A-Za-z]*e[\s=]",
-    r"ruby\s+.*-[A-Za-z]*e[\s=]",
-    r"node\s+.*-[A-Za-z]*e[\s=]",
+    r"python[23]?\s+.*-[A-Za-z]*c(?:\s|=|['\"]|$)",
+    r"perl\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
+    r"ruby\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
+    r"node\s+.*-[A-Za-z]*e(?:\s|=|['\"]|$)",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/security/shell.rs` around lines 140 - 143, The inline-code regexes for
python/perl/ruby/node currently end with [\s=] which misses attached quoted
forms like -e'...'; update each pattern (the strings
r"python[23]?\s+.*-[A-Za-z]*c[\s=]", r"perl\s+.*-[A-Za-z]*e[\s=]",
r"ruby\s+.*-[A-Za-z]*e[\s=]", r"node\s+.*-[A-Za-z]*e[\s=]") to accept
whitespace, equals, an immediate quote, or end-of-string — e.g., replace [\s=]
with (?:[\s=]|['"]|$) so patterns also catch -e'code' and -e"code" forms.

Comment thread src/security/shell.rs
Comment on lines +177 to +197
/// Convert a command string that may contain shell glob characters into a regex
/// that can match the *literal* path the glob would expand to.
///
/// `?` → `.` (any single char), `*` → `.*`, `[` / `]` stripped,
/// all other regex-special characters escaped.
fn build_glob_regex(command: &str) -> Option<Regex> {
let mut pat = String::with_capacity(command.len() + 16);
for ch in command.chars() {
match ch {
'?' => pat.push('.'),
'*' => pat.push_str(".*"),
'[' | ']' => {} // strip brackets (contents become literal)
c if ".+^${}()|\\".contains(c) => {
pat.push('\\');
pat.push(c);
}
c => pat.push(c),
}
}
Regex::new(&pat).ok()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, get the file size to understand scope
wc -l src/security/shell.rs

Repository: qhkm/zeptoclaw

Length of output: 83


🏁 Script executed:

# Read the build_glob_regex function and surrounding context
sed -n '175,200p' src/security/shell.rs

Repository: qhkm/zeptoclaw

Length of output: 922


🏁 Script executed:

# Read the related code at lines 338-352
sed -n '336,354p' src/security/shell.rs

Repository: qhkm/zeptoclaw

Length of output: 1004


🏁 Script executed:

# Search for usage of build_glob_regex to understand the context
rg -n "build_glob_regex" src/security/shell.rs -A 2 -B 2

Repository: qhkm/zeptoclaw

Length of output: 583


🏁 Script executed:

# Check if .zeptoclaw/config.json is in the blocklist
rg -n "zeptoclaw.*config|config.*json" src/security/shell.rs

Repository: qhkm/zeptoclaw

Length of output: 450


Fix build_glob_regex to treat bracket classes as single-character wildcards.

The current implementation strips brackets entirely, allowing commands like /etc/pass[w0]d to evade the literal blocklist for /etc/passwd because the pattern w0 is treated as literal text rather than a char-class wildcard. Bracket classes [...] in glob patterns must be converted to . (match any single char) and anchored with ^$ for correct matching.

🔧 Suggested fix
 fn build_glob_regex(command: &str) -> Option<Regex> {
     let mut pat = String::with_capacity(command.len() + 16);
-    for ch in command.chars() {
+    let mut chars = command.chars().peekable();
+    while let Some(ch) = chars.next() {
         match ch {
             '?' => pat.push('.'),
             '*' => pat.push_str(".*"),
-            '[' | ']' => {} // strip brackets (contents become literal)
+            '[' => {
+                // Treat any bracket class ([abc], [a-z], [!x]) as one-char wildcard.
+                let mut closed = false;
+                while let Some(next) = chars.next() {
+                    if next == ']' {
+                        closed = true;
+                        break;
+                    }
+                }
+                if closed {
+                    pat.push('.');
+                } else {
+                    pat.push_str(r"\[");
+                }
+            }
             c if ".+^${}()|\\".contains(c) => {
                 pat.push('\\');
                 pat.push(c);
             }
             c => pat.push(c),
         }
     }
-    Regex::new(&pat).ok()
+    Regex::new(&format!("^{}$", pat)).ok()
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Convert a command string that may contain shell glob characters into a regex
/// that can match the *literal* path the glob would expand to.
///
/// `?` → `.` (any single char), `*` → `.*`, `[` / `]` stripped,
/// all other regex-special characters escaped.
fn build_glob_regex(command: &str) -> Option<Regex> {
let mut pat = String::with_capacity(command.len() + 16);
for ch in command.chars() {
match ch {
'?' => pat.push('.'),
'*' => pat.push_str(".*"),
'[' | ']' => {} // strip brackets (contents become literal)
c if ".+^${}()|\\".contains(c) => {
pat.push('\\');
pat.push(c);
}
c => pat.push(c),
}
}
Regex::new(&pat).ok()
}
/// Convert a command string that may contain shell glob characters into a regex
/// that can match the *literal* path the glob would expand to.
///
/// `?` → `.` (any single char), `*` → `.*`, `[` / `]` stripped,
/// all other regex-special characters escaped.
fn build_glob_regex(command: &str) -> Option<Regex> {
let mut pat = String::with_capacity(command.len() + 16);
let mut chars = command.chars().peekable();
while let Some(ch) = chars.next() {
match ch {
'?' => pat.push('.'),
'*' => pat.push_str(".*"),
'[' => {
// Treat any bracket class ([abc], [a-z], [!x]) as one-char wildcard.
let mut closed = false;
while let Some(next) = chars.next() {
if next == ']' {
closed = true;
break;
}
}
if closed {
pat.push('.');
} else {
pat.push_str(r"\[");
}
}
c if ".+^${}()|\\".contains(c) => {
pat.push('\\');
pat.push(c);
}
c => pat.push(c),
}
}
Regex::new(&format!("^{}$", pat)).ok()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/security/shell.rs` around lines 177 - 197, build_glob_regex currently
strips '[' and ']' which treats character classes like "[w0]" as literal text;
change it to convert any bracket class in command into a single-character
wildcard by pushing '.' into pat and skipping all characters until the matching
']' (handle an unterminated '[' by treating it as literal '[' or as '.' plus the
rest as appropriate), keep existing escaping logic for other chars, and then
anchor the resulting pattern with '^' at the start and '$' at the end before
calling Regex::new so matches are for the whole path; update the function
build_glob_regex to use pat, command, and Regex::new accordingly and still
return Option<Regex>.

@qhkm qhkm merged commit 68916c3 into main Mar 4, 2026
7 of 8 checks passed
@qhkm qhkm deleted the fix/security-advisories branch March 4, 2026 17:14
@zpbrent
Copy link
Copy Markdown
Contributor

zpbrent commented Mar 4, 2026 via email

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

Successfully merging this pull request may close these issues.

2 participants