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

Prevent panics statically #202

Open
4 tasks
Tracked by #137
joshlf opened this issue Jul 28, 2023 · 21 comments
Open
4 tasks
Tracked by #137

Prevent panics statically #202

joshlf opened this issue Jul 28, 2023 · 21 comments
Assignees
Labels
compatibility-nonbreaking Changes that are (likely to be) non-breaking experience-easy This issue is easy, and shouldn't require much experience experience-medium This issue is of medium difficulty, and requires some experience help wanted Extra attention is needed

Comments

@joshlf
Copy link
Member

joshlf commented Jul 28, 2023

Issues like #200 demonstrate that the compiler is sometimes not smart enough to optimize out panics that we can prove statically won't happen. It would be good if we could:

  • Refactor to only use operations which don't include panicking in their call graph
  • Validate this in CI

See also #325, #1125

Mentoring instructions

  • Easy steps
    • Create a new step in this CI job that runs Red Pen on zerocopy, only running the step under if: matrix.crate == 'zerocopy' && matrix.toolchain == 'nightly'
    • Annotate as many functions as possible with #[redpen::dont_panic]
  • Medium to hard (depending on the code): Refactor as much code as possible to remove all panics
@jettr
Copy link

jettr commented Aug 3, 2023

Here is a discussion on another project about having a no panic policy with some good information: https://github.com/orgs/tpm-rs/discussions/5

I think it could help the zerocopy library if it were interested in some automated CI checks to ensure no panics are present in "tier 1" tool chains.

@joshlf
Copy link
Member Author

joshlf commented Aug 7, 2023

Awesome, thanks for that breadcrumb! I'd like to go even further than that, and ensure that panics aren't just optimized out, but never emitted in the first place. Do you know if that technique would work in conjunction with opt-level = 0, and would that suffice to guarantee that we're never emitting panics? I want to make sure we don't get into the situation in #200 in which panics are optimized out in a toy example but not in a larger application.

@jettr
Copy link

jettr commented Aug 7, 2023

I like the idea of using opt-level=0 for the "tier 1" tool chains that CI checks against to ensure there are no panics. I don't know of a way to ensure panics are never emitted with 100% certainty though. I think your idea is about as close as you can get with currently tooling.

@joshlf joshlf added the compatibility-nonbreaking Changes that are (likely to be) non-breaking label Aug 12, 2023
@joshlf joshlf mentioned this issue Aug 20, 2023
@joshlf joshlf added help wanted Extra attention is needed experience-medium This issue is of medium difficulty, and requires some experience labels Aug 28, 2023
@joshlf joshlf added experience-easy This issue is easy, and shouldn't require much experience Hacktoberfest experience-medium This issue is of medium difficulty, and requires some experience and removed experience-medium This issue is of medium difficulty, and requires some experience labels Sep 29, 2023
@jrvanwhy
Copy link
Contributor

I'm starting to work on this. I suspect I'll run out of time to send a PR this week but I hope to send one next week.

@joshlf
Copy link
Member Author

joshlf commented Oct 12, 2023

Awesome, thanks! I'll assign it to you. Feel free to comment here if you have any questions.

@jrvanwhy
Copy link
Contributor

It turns out that redpen does not catch every possible panic path. For example, it does not error on this program:

pub trait MaybePanic { fn maybe_panic(&self); }

pub struct WillPanic;
impl MaybePanic for WillPanic {
    fn maybe_panic(&self) { panic!(); }
}

#[redpen::dont_panic]
pub fn will_this_panic<M: MaybePanic>(m: M) {
    m.maybe_panic();
}

#[redpen::dont_panic]
pub fn main() {
    will_this_panic(WillPanic);
}

So we have a few tools at our disposal, all of which are incomplete:

  1. Linking-based checks: Build code that uses zerocopy in a way where we can look at the emitted symbols/links to determine whether a panic could have happened. no_panic is an example of this approach, but I think it could also be done using e.g. a custom #[panic_handler].
  2. Red Pen: use the redpen tool to find panics, marking everything that we don’t want to panic with #[redpen::dont_panic].
  3. Clippy lints: there are more clippy lints we can enable to find sources of accidental panics.

These all have their advantages and disadvantages:

Linking-based checks

Pro: Errors exactly when rustc emits a panic in generated code, which is the cost we care about.
Con: Pretty much all of zerocopy is #[inline] or generic, so we can’t just check the zerocopy rlib. We’ll have to manually maintain an additional binary/library that invokes much of zerocopy’s functionality, which adds a lot of maintenance effort. I suspect this crate will be forgotten about frequently.
Con: Sensitive to exact compiler versions, optimization levels. As previously mentioned, this could pass in zerocopy's CI when the same functionality will emit a panic in user code. Likely to break during nightly toolchain updates.
Con: Ugly error messages, which may make it difficult to identify where the panic is.

Red Pen

Note: At the moment, redpen does not change its status code to indicate whether it found any issues. If we'd like to use redpen, I'll send a PR upstream to make it report its failures, and then we'll probably want to wait for a new release of redpen before we start using it.

Pro: Catches most panics.
Pro: Clearly documents what can panic in the code (if it has #[redpen::dont_panic] then it can't panic... theoretically).
Con: Relatively new and unproven. It requires a shim crate to be added to zerocopy's [dependencies]. That shim crate is simple, but may be concerning for some of zerocopy's users. Also, if it disappears we'll have to tear it out of zerocopy's codebase, and would need another approach to avoid panics anyway.
Con: Awkward integration into the CI's toolchain test matrix, as it is pinned to its own toolchain.
Con: It's one more tool for zerocopy contributors to install and run.

Clippy lints

With Rust 1.69 (what I happen to have on my system), the following Clippy lints point to code in zerocopy that can emit panic branches:

I also think there is merit in enabling missing_panics_doc, although at the moment it does not point out anything in zerocopy.

Pro: Well-supported tool that zerocopy already uses which emits well-formatted error messages.
Con: Won't catch all panic causes. E.g. no clippy lint catches uses of Vec::swap_remove, which can panic.

Looking for feedback

We can choose to use any combination of linking checks, Red Pen, and Clippy to minimize panics. I would like input from zerocopy's maintainers on which approaches you would prefer. I can pursue any of them.

Here's my take:

  1. Linking-based checks are more toil than they are worth.
  2. I'm ambivalent on Red Pen. It is design to solve exactly the problem we have but has significant drawbacks.
  3. I think we should investigate the additional Clippy lints. I can send PRs that enable and address each lint individually, and we can see if each one is beneficial in practice.

Maybe I should enable and address the Clippy lints first, then give Red Pen a try and see how many panics it catches that Clippy doesn't? Then we can evaluate whether Red Pen is worth the effort.

@joshlf
Copy link
Member Author

joshlf commented Oct 19, 2023

Holy cow, thank you for such a detailed writeup!!

Maybe I should enable and address the Clippy lints first, then give Red Pen a try and see how many panics it catches that Clippy doesn't? Then we can evaluate whether Red Pen is worth the effort.

This sounds like a good plan to me!

Linking-based checks

...
Con: Pretty much all of zerocopy is #[inline] or generic, so we can’t just check the zerocopy rlib. We’ll have to manually maintain an additional binary/library that invokes much of zerocopy’s functionality, which adds a lot of maintenance effort. I suspect this crate will be forgotten about frequently.

Do you think it'd be possible to do this in zerocopy itself? A few thoughts:

  • If we mark a generic function as #[no_panic] and then instantiate it with a concrete type (e.g. by doing let _ = foo::<T>;), will that exercise the check?
  • Maybe we could just emit code in zerocopy itself (e.g. when cfg(test)) which takes the place of an external crate, and so requires less overhead
  • Is there any way we could write an automatic check that determines whether each function has been called in this checking code?

Con: Sensitive to exact compiler versions, optimization levels. As previously mentioned, this could pass in zerocopy's CI when the same functionality will emit a panic in user code. Likely to break during nightly toolchain updates.

My hope is that we can get rid of panics without relying on the optimizer, and so we can run this check with opt-level = 0. Hopefully that will also make exact compiler versions a non-issue.

Red Pen

Note: At the moment, redpen does not change its status code to indicate whether it found any issues. If we'd like to use redpen, I'll send a PR upstream to make it report its failures, and then we'll probably want to wait for a new release of redpen before we start using it.

+1

Con: Relatively new and unproven. It requires a shim crate to be added to zerocopy's [dependencies]. That shim crate is simple, but may be concerning for some of zerocopy's users. Also, if it disappears we'll have to tear it out of zerocopy's codebase, and would need another approach to avoid panics anyway.

I was thinking we'd just have this be a dev-dependency and only check it during cargo test in CI. Would that be a viable approach?

@jrvanwhy
Copy link
Contributor

Maybe I should enable and address the Clippy lints first, then give Red Pen a try and see how many panics it catches that Clippy doesn't? Then we can evaluate whether Red Pen is worth the effort.

This sounds like a good plan to me!

Alright, I'll start working on Clippy lint PRs. We can continue discussing the other options in parallel.

Linking-based checks

...
Con: Pretty much all of zerocopy is #[inline] or generic, so we can’t just check the zerocopy rlib. We’ll have to manually maintain an additional binary/library that invokes much of zerocopy’s functionality, which adds a lot of maintenance effort. I suspect this crate will be forgotten about frequently.

Do you think it'd be possible to do this in zerocopy itself? A few thoughts:

  • If we mark a generic function as #[no_panic] and then instantiate it with a concrete type (e.g. by doing let _ = foo::<T>;), will that exercise the check?

With the no_panic crate, the check only happens when the object files are linked into a binary. We can use the test binary, so the panic check happens during cargo test. In that case, you can invoke a particular monomorphization of the function, which will check that monomorphization.

  • Maybe we could just emit code in zerocopy itself (e.g. when cfg(test)) which takes the place of an external crate, and so requires less overhead

Yes, using the test binary works (I don't know why I didn't think of that initially).

  • Is there any way we could write an automatic check that determines whether each function has been called in this checking code?

We could use a code coverage tool. If we tag a function as #[no_panic], and the coverage tool indicates that function was executed by cargo test, then I think we can be confident the no-panic check ran on that function.

Con: Sensitive to exact compiler versions, optimization levels. As previously mentioned, this could pass in zerocopy's CI when the same functionality will emit a panic in user code. Likely to break during nightly toolchain updates.

My hope is that we can get rid of panics without relying on the optimizer, and so we can run this check with opt-level = 0. Hopefully that will also make exact compiler versions a non-issue.

Sadly, with the latest stable compiler (1.73.0), even an empty function requires opt-level = 1 to pass:

// Only compiles with optimization
#[no_panic::no_panic]
#[test]
fn foo() {
}

Red Pen

Note: At the moment, redpen does not change its status code to indicate whether it found any issues. If we'd like to use redpen, I'll send a PR upstream to make it report its failures, and then we'll probably want to wait for a new release of redpen before we start using it.

+1

Con: Relatively new and unproven. It requires a shim crate to be added to zerocopy's [dependencies]. That shim crate is simple, but may be concerning for some of zerocopy's users. Also, if it disappears we'll have to tear it out of zerocopy's codebase, and would need another approach to avoid panics anyway.

I was thinking we'd just have this be a dev-dependency and only check it during cargo test in CI. Would that be a viable approach?

I suspect yes, but it's not obvious to me. redpen is its own binary, so it's unclear how to enable Rust's test harness when using it. It may or may not pay attention to command line flags and environment variables, so that may require some more work and an upstream contribution to enable.

jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Oct 24, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Oct 24, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Oct 26, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Nov 13, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Nov 27, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
@kupiakos
Copy link
Collaborator

IIUC estebank/redpen@887ea4f should fix the specific example provided in #202 (comment)

jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Dec 4, 2023
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
@kupiakos
Copy link
Collaborator

kupiakos commented Feb 27, 2024

I think a library like zerocopy should try to be semantically panic-free and validate through something independent of the optimizer like redpen. This is primarily because a library cannot reliably test every way it's used to detect if panics could possibly be generated, especially across compiler versions/configurations. If we can get redpen working I believe it's worth the work to guard against panics in something of limited scope like zerocopy.

However, part of my work is to progressively remove panics in a large embedded binary that was written without explicit concern for panics. There are some code constructs that would be extremely non-trivial to rewrite in a way that is both semantically panic-free and doesn't invoke unsafe. So, I really need linking-based checks as a last resort and as an initial guard before redpen is working. My plan is to mark everything that can be semantically panic-free with redpen, and then progressively used linking-based checks for the rest.

@joshlf
Copy link
Member Author

joshlf commented Mar 5, 2024

Sorry, I definitely phrased my review comment badly.

A few points:

  • I agree with @kupiakos that semantic panic-freedom is ideal
  • I also agree that it often requires unsafe code in practice
  • In cases where unsafe is required, I haven't thought through how I want to handle that. We try to avoid unsafe at all costs, so introducing new unsafe just to take our code from "this panic gets optimized out in practice" to "this semantically doesn't panic" is a tough pill to swallow. I could be convinced, though, and depending on the use cases, I could also see some more general-purpose unsafe utilities that let us do this in a way that's reusable; it'd just take engineering work that we haven't had the cycles for so far
  • At an absolute minimum, a tool like redpen is useful to annotate all functions that don't currently panic so we can prevent regressions
  • It would also be good to use a "hack" like link-time assertions for the cases where we can't yet prevent panics semantically, although of course I consider this a last resort
  • The thing that worried me in my review comment was introducing a branch. On further reflection, a panic also introduces a branch, so I guess ? is strictly better? That said, maybe a panic coaxes the optimizer to try harder to prove that it's unreachable? I'd need to do more thinking/godbolt-ing.

I hope that provides some clarity. Let me know if you have any other questions or thoughts on how best to proceed.

@briansmith
Copy link

That said, maybe a panic coaxes the optimizer to try harder to prove that it's unreachable?

I spent some time looking into this and AFAICT the compiler doesn't try to optimize more to avoid panics. The cases I looked at were division by a non-constant (contains check-for-zero and branch to panic) and slice indexing.

For slice indexing, I basically had to remove all uses of it in favor of higher-level operations (iterators, split_at, etc.), or else get and get_mut. The optimizer does seem to consider the panic code paths "cold" pretty reliably.

@joshlf
Copy link
Member Author

joshlf commented Mar 5, 2024

Ah okay cool that's good to know.

@kupiakos
Copy link
Collaborator

kupiakos commented Mar 21, 2024

We got bit by another panic path today, again from split_at. This diff, an unambiguous ergonomic improvement, introduced a new panic path detected via the linking method:

-    let Some(mut counts) = buf
-        .get_mut(..size_of::<ResetCounts>())
-        .and_then(Ref::<_, ResetCounts>::new)
-    else {
+    let Some(mut counts) = ResetCounts::mut_from_prefix(buf) else {

I don't have stats, but I've noticed split_at, split_at_mut, and copy_from_slice are usually the panic-causing culprits.

jswrenn added a commit that referenced this issue Mar 25, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
jswrenn added a commit that referenced this issue Mar 25, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Mar 26, 2024
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Mar 26, 2024
This enables Clippy's
[`expect_used`](https://rust-lang.github.io/rust-clippy/master/index.html#/expect_used)
lint, which errors on calls to `Option::expect` and `Result::{expect,
expect_err}`. The intent of enabling this lint is to reduce the number
of panics emitted in `zerocopy`'s code (issue google#202).
jswrenn added a commit that referenced this issue Mar 27, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
@joshlf
Copy link
Member Author

joshlf commented Mar 27, 2024

We got bit by another panic path today, again from split_at. This diff, an unambiguous ergonomic improvement, introduced a new panic path detected via the linking method:

-    let Some(mut counts) = buf
-        .get_mut(..size_of::<ResetCounts>())
-        .and_then(Ref::<_, ResetCounts>::new)
-    else {
+    let Some(mut counts) = ResetCounts::mut_from_prefix(buf) else {

I don't have stats, but I've noticed split_at, split_at_mut, and copy_from_slice are usually the panic-causing culprits.

How are you detecting these panics? Do you have static tooling that you're using? We're considering landing #1071 to help reduce panic paths, and it'd be good to have a way to test to confirm that it's actually having the desired effect.

jswrenn added a commit that referenced this issue Mar 28, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
@kupiakos
Copy link
Collaborator

kupiakos commented Mar 28, 2024

How are you detecting these panics

@joshlf This was detected via the linking method, as mentioned. This is how we do it specifically:

  • Ensure that the #[panic_handler] function calls a #[no_mangle] function. Give that function a well-defined name like panic_is_possible. Hint extremely strongly to LLVM to never optimize or inline it with #[cold], #[inline(never)], and a body containing core::hint::black_box(()). I believe you can also make the #[panic_handler] itself directly the symbol being detected.
  • Build without stripping symbols - defer this to a later build step that invokes objcopy.
  • After build, check if the well-defined name is present in the non-stripped output binary. We currently use objdump on the object file and grep for the symbol.

This forbids panics in a single output binary. It does not function as designed for anything that isn't fully linked.

We also could've had the panic handler produce a linking failure by referencing an undefined symbol, but we determined that the developer experience was much worse than a tool explicitly saying "a panic was introduced in this binary, but it forbids them". This tooling is not public and is highly specific to our bespoke build system.

Do you have static tooling that you're using

I'm not entirely sure what you mean by "static tooling" here. It doesn't run the code to do the detection, so it's static in that way.

@jswrenn
Copy link
Collaborator

jswrenn commented Mar 28, 2024

Do you have a trick for working backwards from the #[no_mangle] function to the panic location?

jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Mar 29, 2024
This panic is unreachable. Panicing is heavyweight, so we're trying to reduce panics (issue google#202). This PR replaces the panic with a branch that should be lighter-weight (if it is not optimized away entirely).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Mar 29, 2024
This panic is unreachable. Panicing is heavyweight, so we're trying to
reduce panics (issue google#202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
@kupiakos
Copy link
Collaborator

kupiakos commented Mar 30, 2024

Do you have a trick for working backwards from the #[no_mangle] function to the panic location?

@jswrenn objdump -CSd <binary> | less, then search for the panic handler symbol - you'll find a jump somewhere. There are other binary analysis tools, but this is the quick and dirty way. If you've managed to eliminate the other panics from the binary and this is a new trigger, it's usually easy to find the specific function that does it. We don't currently have automatic tooling that points to a source location or anything like that.

It's highly sensitive to compiler and build changes; it's definitely non-ideal but better than re-introducing panic paths after we managed to remove all of them in a binary. It's not uncommon that the panic is in code you didn't directly touch due to the additional optimizer pressure introduced by the change.

jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Apr 8, 2024
This panic is unreachable. Panicing is heavyweight, so we're trying to
reduce panics (issue google#202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
jswrenn added a commit that referenced this issue Apr 10, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
jswrenn added a commit that referenced this issue Apr 10, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
jswrenn added a commit that referenced this issue Apr 15, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Apr 16, 2024
This panic is unreachable. Panicing is heavyweight, so we're trying to
reduce panics (issue google#202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
github-merge-queue bot pushed a commit that referenced this issue Apr 18, 2024
Given the importance of avoiding panic paths to some of our
customers, we should, as a rule, avoid only offering (or relying
upon) APIs with panic paths.

Related to #202

Co-authored-by: Joshua Liebow-Feeser <joshlf@users.noreply.github.com>
@joshlf
Copy link
Member Author

joshlf commented Apr 19, 2024

Related: #1125

jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Apr 23, 2024
This panic is unreachable. Panicing is heavyweight, so we're trying to
reduce panics (issue google#202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
jrvanwhy added a commit to jrvanwhy/zerocopy that referenced this issue Apr 24, 2024
This panic is unreachable. Panicxing is heavyweight, so we're trying to
reduce panics (issue google#202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
github-merge-queue bot pushed a commit that referenced this issue Apr 24, 2024
This panic is unreachable. Panicxing is heavyweight, so we're trying to
reduce panics (issue #202). This PR replaces the panic with a branch
that should be lighter-weight (if it is not optimized away entirely).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compatibility-nonbreaking Changes that are (likely to be) non-breaking experience-easy This issue is easy, and shouldn't require much experience experience-medium This issue is of medium difficulty, and requires some experience help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

6 participants