|
| 1 | +# Best Practices: Handling glibc 2.42 Compatibility Issues |
| 2 | + |
| 3 | +Based on research comparing GNU coreutils and uutils approaches to the stty panic issue. |
| 4 | + |
| 5 | +## Executive Summary |
| 6 | + |
| 7 | +**Current uutils approach is pragmatic but not ideal.** GNU's defensive programming model is more robust. The best practice involves **three-tier strategy**: pragmatic workaround (now), upstream contribution (medium-term), proper implementation (long-term). |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Tier 1: Current Implementation (Pragmatic Workaround) |
| 12 | + |
| 13 | +### ✅ What We're Doing Right |
| 14 | + |
| 15 | +1. **Panic catching** - Prevents crashes in production |
| 16 | +2. **Silent failure** - Doesn't confuse users with panic messages |
| 17 | +3. **Minimal code change** - Low risk of introducing new bugs |
| 18 | +4. **Preserves functionality** - Everything works except speed display |
| 19 | + |
| 20 | +### ⚠️ What Could Be Better |
| 21 | + |
| 22 | +The current approach masks the real problem rather than solving it: |
| 23 | + |
| 24 | +```rust |
| 25 | +// Current approach - reactive |
| 26 | +let speed_result = panic::catch_unwind(panic::AssertUnwindSafe(|| { |
| 27 | + cfgetospeed(termios) // ← This panic shouldn't happen |
| 28 | +})); |
| 29 | +``` |
| 30 | + |
| 31 | +### 🎯 Recommendations for Tier 1 |
| 32 | + |
| 33 | +**Add comprehensive documentation:** |
| 34 | + |
| 35 | +```rust |
| 36 | +/// Attempts to print terminal speed. |
| 37 | +/// |
| 38 | +/// # Panic Handling |
| 39 | +/// |
| 40 | +/// This function catches panics that occur on systems with glibc 2.42 where |
| 41 | +/// cfgetospeed() may return EINVAL. This is a temporary workaround for: |
| 42 | +/// - Issue: https://github.com/uutils/coreutils/issues/8474 |
| 43 | +/// - Root cause: nix crate unwraps cfgetospeed() result |
| 44 | +/// - See: https://github.com/nix-rust/nix/issues/... |
| 45 | +/// |
| 46 | +/// # Behavior |
| 47 | +/// |
| 48 | +/// - On systems where cfgetospeed works: prints speed (e.g., "speed 38400 baud") |
| 49 | +/// - On affected systems: silently omits speed (functional equivalent to GNU stty) |
| 50 | +/// - Never panics, never prints panic messages |
| 51 | +fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { |
| 52 | + // ...existing implementation |
| 53 | +} |
| 54 | +``` |
| 55 | + |
| 56 | +--- |
| 57 | + |
| 58 | +## Tier 2: Upstream Contribution (Medium-term, 3-6 months) |
| 59 | + |
| 60 | +### The Real Problem |
| 61 | + |
| 62 | +The nix crate (v0.30.1+) has an unsafe unwrap: |
| 63 | + |
| 64 | +```rust |
| 65 | +// nix-0.30.1/src/sys/termios.rs:767 |
| 66 | +pub fn cfgetospeed(termios: &Termios) -> BaudRate { |
| 67 | + unsafe { libc::cfgetospeed(&*inner_termios) } |
| 68 | + .try_into() |
| 69 | + .unwrap() // ← WRONG: Should return Result |
| 70 | +} |
| 71 | +``` |
| 72 | + |
| 73 | +### 🎯 Best Practice: Follow GNU's Model |
| 74 | + |
| 75 | +**Proposal to nix crate maintainers:** |
| 76 | + |
| 77 | +```rust |
| 78 | +// Proposed fix - returns Result like GNU does |
| 79 | +pub fn cfgetospeed(termios: &Termios) -> Result<BaudRate, Errno> { |
| 80 | + let speed = unsafe { libc::cfgetospeed(&*inner_termios) }; |
| 81 | + |
| 82 | + // -1 indicates error; errno is set |
| 83 | + if speed == -1 { |
| 84 | + return Err(Errno::last()); |
| 85 | + } |
| 86 | + |
| 87 | + speed.try_into() |
| 88 | + .map_err(|_| Errno::EINVAL) |
| 89 | +} |
| 90 | +``` |
| 91 | + |
| 92 | +### Implementation Steps |
| 93 | + |
| 94 | +1. **Open issue in nix repository:** |
| 95 | + ``` |
| 96 | + Title: cfgetospeed should return Result instead of panicking |
| 97 | + Description: Explain glibc 2.42 incompatibility |
| 98 | + Link to: GNU coreutils source (defensive approach) |
| 99 | + ``` |
| 100 | + |
| 101 | +2. **Create pull request to nix with:** |
| 102 | + - Return type change from `BaudRate` to `Result<BaudRate, Errno>` |
| 103 | + - Proper error handling |
| 104 | + - Tests for error conditions |
| 105 | + |
| 106 | +3. **Update uutils to use the new Result-based API:** |
| 107 | + ```rust |
| 108 | + match cfgetospeed(termios) { |
| 109 | + Ok(speed) => { |
| 110 | + // Print speed |
| 111 | + } |
| 112 | + Err(_) => { |
| 113 | + // Skip speed (matches GNU behavior) |
| 114 | + } |
| 115 | + } |
| 116 | + ``` |
| 117 | + |
| 118 | +--- |
| 119 | + |
| 120 | +## Tier 3: Proper Implementation (Long-term, Post-nix fix) |
| 121 | + |
| 122 | +### When nix is fixed, migrate uutils to: |
| 123 | + |
| 124 | +```rust |
| 125 | +/// Print terminal speed, matching GNU coreutils defensive approach |
| 126 | +fn print_terminal_size(termios: &Termios, opts: &Options) -> nix::Result<()> { |
| 127 | + // No panic catching needed - nix returns Result |
| 128 | + match cfgetospeed(termios) { |
| 129 | + Ok(speed) => { |
| 130 | + #[cfg(unix)] |
| 131 | + print!("{} ", translate!("stty-output-speed", "speed" => speed)); |
| 132 | + } |
| 133 | + Err(e) if e == Errno::EINVAL => { |
| 134 | + // Expected on some systems - silently skip |
| 135 | + } |
| 136 | + Err(e) => { |
| 137 | + // Unexpected error - could log/warn here |
| 138 | + eprintln!("warning: unable to get terminal speed: {}", e); |
| 139 | + } |
| 140 | + } |
| 141 | + Ok(()) |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +**Benefits:** |
| 146 | +- ✅ No panic catching needed |
| 147 | +- ✅ Proper error semantics |
| 148 | +- ✅ Matches GNU's approach |
| 149 | +- ✅ Self-documenting code |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## Applying These Lessons to Other Tools |
| 154 | + |
| 155 | +### Pattern for Finding Similar Issues |
| 156 | + |
| 157 | +Search for these patterns in uutils: |
| 158 | + |
| 159 | +```bash |
| 160 | +# Find all unsafe unwraps in termios/ioctl operations |
| 161 | +grep -r "\.unwrap()" src/uu/*/src/*.rs | grep -E "termios|ioctl|cfget" |
| 162 | + |
| 163 | +# Find other defensive gaps |
| 164 | +grep -r "panic::" src/uu/*/src/*.rs |
| 165 | +``` |
| 166 | + |
| 167 | +### Defensive Checklist Before Submitting PR |
| 168 | + |
| 169 | +When dealing with system calls, ask: |
| 170 | + |
| 171 | +- [ ] Are all system calls returning `Result`? |
| 172 | +- [ ] Is every error condition checked? |
| 173 | +- [ ] Are there any `.unwrap()` calls? |
| 174 | +- [ ] Do we validate before using returned values? |
| 175 | +- [ ] Does behavior match GNU's implementation? |
| 176 | +- [ ] Have we tested on multiple systems/glibc versions? |
| 177 | + |
| 178 | +--- |
| 179 | + |
| 180 | +## Comparison: Defense in Depth |
| 181 | + |
| 182 | +| Level | GNU Approach | Current uutils | Tier 3 Target | |
| 183 | +|-------|--------------|-----------------|---------------| |
| 184 | +| **System call** | Returns error via errno | May panic in nix | Returns Result | |
| 185 | +| **Error check** | Explicit: `if (result == -1)` | Implicit: nix.unwrap() | Explicit: `match result` | |
| 186 | +| **Failure path** | Graceful degradation | Panic + catch | Graceful degradation | |
| 187 | +| **Code clarity** | Very clear what errors are handled | Obscured by panic catching | Crystal clear intent | |
| 188 | +| **Performance** | No overhead | Exception handling overhead | No overhead | |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +## Action Items |
| 193 | + |
| 194 | +### Immediate (Current PR) |
| 195 | +- [x] Fix panic with catch_unwind |
| 196 | +- [x] Remove unwrap in combo_to_flags |
| 197 | +- [x] Add tests |
| 198 | +- [ ] **Add comprehensive comments explaining workaround** |
| 199 | +- [ ] **Document why we're not doing what GNU does** |
| 200 | + |
| 201 | +### Soon (Next PR) |
| 202 | +- [ ] Check for other unsafe unwraps in stty |
| 203 | +- [ ] Create template for issue in nix repository |
| 204 | +- [ ] Start discussion with nix maintainers |
| 205 | + |
| 206 | +### Later (Upstream) |
| 207 | +- [ ] Contribute Result-based API to nix |
| 208 | +- [ ] Update uutils once nix supports it |
| 209 | +- [ ] Remove catch_unwind workaround |
| 210 | + |
| 211 | +--- |
| 212 | + |
| 213 | +## Reference Documents |
| 214 | + |
| 215 | +### GNU stty.c Defensive Pattern |
| 216 | + |
| 217 | +```c |
| 218 | +// GNU's approach: validate before using |
| 219 | +if (tcgetattr (fd, &mode) < 0) { |
| 220 | + perror_without_progname ("standard input"); |
| 221 | + return EXIT_FAILURE; |
| 222 | +} |
| 223 | + |
| 224 | +speed_t ispeed = cfgetispeed (&mode); |
| 225 | +if (ispeed == (speed_t) -1) { |
| 226 | + // Handle error |
| 227 | +} |
| 228 | +``` |
| 229 | + |
| 230 | +### Rust Result Pattern (What We Should Do) |
| 231 | + |
| 232 | +```rust |
| 233 | +// Idiomatic Rust: Result-based error handling |
| 234 | +match cfgetospeed(termios) { |
| 235 | + Ok(speed) => println!("speed: {}", speed), |
| 236 | + Err(e) => eprintln!("error: {}", e), |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +### Rust Anti-Pattern (What We Currently Do) |
| 241 | + |
| 242 | +```rust |
| 243 | +// Panic catching: last resort, not first choice |
| 244 | +let result = std::panic::catch_unwind(|| { |
| 245 | + dangerous_function() // Might panic |
| 246 | +}); |
| 247 | +``` |
| 248 | + |
| 249 | +--- |
| 250 | + |
| 251 | +## Key Insight |
| 252 | + |
| 253 | +> **Panics are for programmer errors, not runtime conditions.** |
| 254 | +> |
| 255 | +> — Rust Book |
| 256 | +> |
| 257 | +> System calls failing due to glibc changes are a runtime condition, not a programmer error. Using `Result` is correct; catching panics is a workaround. |
| 258 | +
|
| 259 | +The difference: |
| 260 | +- ✅ **Programmer error**: `array[out_of_bounds_index]` → panic is fine |
| 261 | +- ❌ **Runtime condition**: `cfgetospeed()` fails → Result is correct, panic is wrong |
| 262 | + |
| 263 | +--- |
| 264 | + |
| 265 | +## Conclusion |
| 266 | + |
| 267 | +**Current fix:** Good pragmatic workaround, necessary for immediate stability. |
| 268 | + |
| 269 | +**Best practice trajectory:** |
| 270 | +1. Keep current fix + document it thoroughly |
| 271 | +2. Contribute upstream fix to nix crate |
| 272 | +3. Migrate to proper Result-based error handling once nix is updated |
| 273 | + |
| 274 | +This three-tier approach balances pragmatism (fixes users' immediate problem) with principle (gets the root cause fixed properly). |
0 commit comments