df --total accumulates the total row's block count as Σ ceil(bytesᵢ / block_size) — summing each filesystem's rounded-up block count instead of rounding the summed bytes once. With a large --block-size every non-empty filesystem rounds up to one block, so the total degenerates into roughly a count of filesystems. The -h path then multiplies that inflated count back by the block size, which overflows. Normal rows take neither path, which is why every non-total row matches GNU exactly and only the total row is wrong.
Steps to reproduce
- default release build (shipped):
the total row reports wrong numbers — with -B 10000000000000000000 GNU reports a total of 1 block where uutils reports 29 (or 12E with -h).
$ df --total -h -B 10000000000000000000 -a | tail -1
total 12E 8.9E 15E 44% - # uutils
$ /usr/bin/df --total -h -B 10000000000000000000 -a | tail -1
total 1 1 1 43% - # GNU
$ df --total -B 10000000000000000000 -a | tail -1 # without -h
total 29 25 9 44% - # uutils
$ /usr/bin/df --total -B 10000000000000000000 -a | tail -1
total 1 1 1 43% - # GNU
- overflow-checks build:
adding -h multiplies that inflated count back by the block size and the product overflows u64 — attempt to multiply with overflow at
df/src/table.rs:312:17, exit 134.
$ df --total -h -B 10000000000000000000 -a
thread 'main' panicked at src/uu/df/src/table.rs:312:17:
attempt to multiply with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Aborted (core dumped)
$ echo $?
134
Root cause
1. scaled sums per-row ceilings, not the ceiling of the sum.
|
impl BytesCell { |
|
fn new(bytes: u64, block_size: &BlockSize) -> Self { |
|
Self { |
|
bytes, |
|
scaled: { |
|
let BlockSize::Bytes(d) = block_size; |
|
(bytes as f64 / *d as f64).ceil() as u64 |
|
}, |
|
} |
|
} |
|
} |
|
|
|
impl Add for BytesCell { |
|
type Output = Self; |
|
|
|
fn add(self, rhs: Self) -> Self { |
|
Self { |
|
bytes: self.bytes + rhs.bytes, |
|
scaled: self.scaled + rhs.scaled, |
|
} |
|
} |
|
} |
2. The -h branch converts blocks back to bytes with an unguarded multiply.
|
fn scaled_bytes(&self, bytes_column: &BytesCell) -> Cell { |
|
let size = bytes_column.scaled; |
|
let s = if let Some(h) = self.options.human_readable { |
|
let size = if self.is_total_row { |
|
let BlockSize::Bytes(d) = self.options.block_size; |
|
d * size |
|
} else { |
|
bytes_column.bytes |
|
}; |
|
to_magnitude_and_suffix(size.into(), SuffixType::HumanReadable(h), true) |
df --totalaccumulates the total row's block count asΣ ceil(bytesᵢ / block_size)— summing each filesystem's rounded-up block count instead of rounding the summed bytes once. With a large--block-sizeevery non-empty filesystem rounds up to one block, so the total degenerates into roughly a count of filesystems. The-hpath then multiplies that inflated count back by the block size, which overflows. Normal rows take neither path, which is why every non-total row matches GNU exactly and only the total row is wrong.Steps to reproduce
the total row reports wrong numbers — with
-B 10000000000000000000GNU reports a total of1block where uutils reports29(or12Ewith-h).adding
-hmultiplies that inflated count back by the block size and the product overflowsu64—attempt to multiply with overflowatdf/src/table.rs:312:17, exit 134.Root cause
1.
scaledsums per-row ceilings, not the ceiling of the sum.coreutils/src/uu/df/src/table.rs
Lines 214 to 235 in 8118c24
2. The
-hbranch converts blocks back to bytes with an unguarded multiply.coreutils/src/uu/df/src/table.rs
Lines 307 to 316 in 8118c24