* [LSF/MM/BPF TOPIC] Should we make inode->i_ino a u64?
@ 2026-02-18 15:36 Jeff Layton
2026-02-19 14:31 ` Christian Brauner
0 siblings, 1 reply; 2+ messages in thread
From: Jeff Layton @ 2026-02-18 15:36 UTC (permalink / raw)
To: lsf-pc
Cc: linux-fsdevel, Christian Brauner, Christoph Hellwig, Jan Kara,
Al Viro
[-- Attachment #1: Type: text/plain, Size: 1250 bytes --]
For historical reasons, the inode->i_ino field is an unsigned long.
Because it's only 32 bits on 32-bit CPUs, this has caused a lot of fs-
specific hacks on filesystems that have native 64-bit inode numbers
when running a 32-bit arch.
It would be a lot simpler if we just converted i_ino to be 64-bits and
dealt with the conversion at the kernel's edges. This would be a non-
event for the most part on 64-bit arches since unsigned long is already
64 bits there.
The kernel itself doesn't deal much with i_ino, so the internal changes
look fairly straightforward. The bulk of the patches will be to format
strings and to tracepoints.
I think that the biggest problem will be that this will grow struct
inode on 32-bit arches by at least 4 bytes. That may have cacheline
alignment and slab sizing implications. We're actively talking about
deprecating 32-bit arches in the future however, so maybe we can
rationalize that away.
FWIW, I had Claude spin up a plan to do this (attached). It's not bad.
I'm tempted to tell it generate patches for this, since this is mostly
a mechanical change, but I'm curious whether anyone else might have
reasons that we shouldn't go ahead and do it.
--
Jeff Layton <jlayton@kernel.org>
[-- Attachment #2: lazy-swimming-alpaca.md --]
[-- Type: text/markdown, Size: 12037 bytes --]
# Plan: Change `i_ino` in `struct inode` from `unsigned long` to `u64`
## Motivation
On 32-bit architectures, `unsigned long` is 32 bits, limiting inode numbers
to 2^32. Several filesystems (NFS, CIFS, XFS, Ceph, FUSE) natively use 64-bit
inode numbers and must hash/fold them to fit `ino_t`/`unsigned long` on 32-bit.
The VFS intermediate structure `kstat.ino` is already `u64`, and `statx.stx_ino`
is already `__u64`. Making `i_ino` itself `u64` eliminates a type mismatch at
the VFS core and removes the need for filesystem-specific folding hacks on
32-bit architectures.
On 64-bit architectures, `unsigned long` is already 64 bits, so this change is
effectively a type alias change with no runtime impact (only format string and
type signature cleanups).
## Scope Assessment
This is a tree-wide change touching hundreds of files. It should be broken into
a series of patches, each handling one logical subsystem or concern. The series
should be structured so that the core type change is a single commit, preceded
by preparatory patches and followed by cleanups.
## Phase 1: Preparatory Patches (before the type change)
### 1.1 Update VFS inode hash/lookup API signatures
**Files:**
- `include/linux/fs.h` — function declarations
- `fs/inode.c` — function definitions
**Functions to update** (change `unsigned long` ino/hashval params to `u64`):
| Function | File:Line | Change |
|----------|-----------|--------|
| `hash()` (static) | `fs/inode.c:675` | `unsigned long hashval` → `u64 hashval` |
| `find_inode_fast()` | `fs/inode.c:1089` | `unsigned long ino` → `u64 ino` |
| `find_inode_by_ino_rcu()` | `fs/inode.c:1814` | `unsigned long ino` → `u64 ino` |
| `test_inode_iunique()` | `fs/inode.c:1530` | `unsigned long ino` → `u64 ino` |
| `__insert_inode_hash()` | `fs/inode.c:693`, `include/linux/fs.h:3018` | `unsigned long hashval` → `u64 hashval` |
| `iget_locked()` | `fs/inode.c:1458`, `include/linux/fs.h` | `unsigned long ino` → `u64 ino` |
| `iget5_locked()` | `fs/inode.c:1381`, `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `iget5_locked_rcu()` | `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `ilookup()` | `fs/inode.c:1680`, `include/linux/fs.h` | `unsigned long ino` → `u64 ino` |
| `ilookup5()` | `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `ilookup5_nowait()` | `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `find_inode_nowait()` | `fs/inode.c:1728`, `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `find_inode_rcu()` | `include/linux/fs.h:2960` | `unsigned long` → `u64` |
| `inode_insert5()` | `fs/inode.c:1304`, `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
| `insert_inode_locked4()` | `fs/inode.c:1887`, `include/linux/fs.h` | `unsigned long hashval` → `u64 hashval` |
**Note:** `insert_inode_locked()` at `fs/inode.c:1833` uses `ino_t ino = inode->i_ino;`
internally. This local variable should change to `u64` (or just use `inode->i_ino`
directly).
The `hash()` function currently takes `unsigned long hashval`. On 32-bit, when
`i_ino` becomes `u64`, we need the hash function to handle 64-bit values. Consider
using `hash_64()` or folding the upper 32 bits on 32-bit architectures:
```c
static unsigned long hash(struct super_block *sb, u64 hashval)
{
unsigned long tmp;
tmp = (hashval * (unsigned long)sb) ^ (GOLDEN_RATIO_PRIME + hashval) / L1_CACHE_BYTES;
...
}
```
This patch can be done first because `unsigned long` implicitly converts to `u64`
on all architectures (widening), so changing function signatures to `u64` is
backward-compatible even before `i_ino` itself changes.
### 1.2 Update `ino_t` typedef (optional, separate discussion)
**File:** `include/linux/types.h:21`
Currently: `typedef __kernel_ulong_t ino_t;`
Options:
- **Option A**: Change `ino_t` to `u64` — this is the cleanest approach but
affects every use of `ino_t` across the tree. Many local variables use `ino_t`.
- **Option B**: Leave `ino_t` as-is and only change `i_ino` — creates a type
mismatch between `i_ino` (u64) and `ino_t` (unsigned long) on 32-bit.
- **Recommendation**: Option A is preferred for consistency, but this is a
separate patch and a separate discussion with the community. The `i_ino`
change can proceed without it by using `u64` directly.
**Note:** `__kernel_ino_t` in UAPI headers MUST NOT change — it's userspace ABI.
## Phase 2: The Core Type Change
### 2.1 Change `i_ino` field type
**File:** `include/linux/fs.h:786`
```c
// Before:
unsigned long i_ino;
// After:
u64 i_ino;
```
This single change will cause compiler warnings across the tree for format
string mismatches (`%lu` vs `u64`) on 64-bit architectures (where `u64` is
`unsigned long long` but `unsigned long` is also 64-bit). On 32-bit, it will
cause actual width changes.
### 2.2 Struct size impact on 32-bit
`struct inode` will grow by 4 bytes on 32-bit architectures due to `i_ino`
expanding from 4 to 8 bytes. The field may also require 8-byte alignment,
potentially adding padding. Review the field ordering in `struct inode` to
minimize padding impact.
## Phase 3: Tree-Wide Format String Fixes
### 3.1 Strategy for format strings
**Recommended approach**: Use `%llu` with a `(unsigned long long)` cast, which
is the standard kernel pattern for printing `u64` values portably. Do NOT
introduce a new format helper macro — the kernel community generally prefers
explicit types over macros for printk formats.
Pattern to search and replace:
```
%lu.*i_ino → %llu with (unsigned long long) cast
%lx.*i_ino → %llx with (unsigned long long) cast
```
### 3.2 Trace events (highest volume of changes)
Each trace event file should be a separate patch:
| File | Est. Changes | Description |
|------|-------------|-------------|
| `include/trace/events/ext4.h` | ~100+ sites | Change `__field(ino_t, ino)` → `__field(u64, ino)`, update all `%lu` → `%llu`, remove `(unsigned long)` casts |
| `include/trace/events/f2fs.h` | ~50+ sites | Update `show_dev_ino()` macro and all trace events |
| `include/trace/events/writeback.h` | ~15+ sites | Update all writeback trace events |
| `include/trace/events/filelock.h` | ~10 sites | Change `__field(unsigned long, i_ino)` → `__field(u64, i_ino)`, `%lx` → `%llx` |
**Important:** Changing trace event field types changes the binary trace format.
This will affect `trace-cmd` and `perf` trace parsing. This is acceptable but
should be noted in commit messages.
### 3.3 Filesystem-specific format strings
Each filesystem should be a separate patch:
| Filesystem | Key Files | Notes |
|-----------|-----------|-------|
| ext4 | `fs/ext4/super.c`, `fs/ext4/ext4.h` | `__ext4_error_inode()`, `__ext4_warning_inode()`, `__ext4_grp_locked_error()` — change `%lu` → `%llu`, update `unsigned long ino` parameters |
| btrfs | Various | May already use `u64` internally |
| f2fs | Various | Fix format strings |
| Other fs | Various | Grep for `i_ino` with `%lu` in each |
### 3.4 /proc interfaces (ABI-sensitive)
| File | Interface | Current | Notes |
|------|-----------|---------|-------|
| `fs/locks.c:2899` | `/proc/locks` | `%lu` for `inode->i_ino` | Change to `%llu`, values stay the same on 64-bit; on 32-bit, wider numbers possible but no parsers should break since field is positional/decimal |
| `fs/proc/task_mmu.c:468` | `/proc/PID/maps` | `unsigned long ino = 0;` → assigned from `inode->i_ino` | Change local var to `u64`, `seq_put_decimal_ull()` already accepts `unsigned long long` |
## Phase 4: Filesystem-Specific Inode Number Handling
### 4.1 NFS
**Files:**
- `include/linux/nfs_fs.h:670-677` — `nfs_fileid_to_ino_t()`
- `fs/nfs/inode.c:69-73` — `nfs_fattr_to_ino_t()`
- `fs/nfs/inode.c:93-107` — `nfs_compat_user_ino64()`
If `i_ino` is `u64`, NFS can store the full 64-bit fileid in `i_ino` without
folding. The `nfs_fileid_to_ino_t()` function could be simplified or its callers
updated to assign directly. However, the hash value passed to `iget5_locked()`
may still need folding for hash distribution on 32-bit — this is separate from
`i_ino` storage.
**Approach**: Update `nfs_fattr_to_ino_t()` to return `u64` and store the full
fileid. Keep hash folding only for the hash parameter to `iget5_locked()`.
### 4.2 CIFS/SMB
**File:** `fs/smb/client/cifsfs.h:16-29` — `cifs_uniqueid_to_ino_t()`
Same pattern as NFS. With `u64 i_ino`, CIFS can store the full 64-bit uniqueid
without hashing. Update to return `u64`.
**File:** `fs/smb/client/inode.c:1614-1623` — update hash variable type.
### 4.3 Other filesystems
Filesystems that already use 64-bit inode numbers internally (XFS, Ceph, FUSE)
may benefit from simplified code paths. Each is a separate cleanup patch.
## Phase 5: Related Type Updates
### 5.1 `get_next_ino()` (`fs/inode.c:1145`)
Returns `unsigned int` (32-bit). Used by pseudo-filesystems (tmpfs, sysfs,
debugfs, etc.). This is deliberately limited to 32 bits to avoid EOVERFLOW from
`stat()` on 32-bit userspace. **No change needed** — widening to `u64` storage
is fine; the values still fit.
### 5.2 `iunique()` (`fs/inode.c:1556`)
Uses `static unsigned int counter`. Returns `ino_t`. Same consideration as
`get_next_ino()` — keeps values in 32-bit range for compat. **No change needed**
for the counter, but return type should follow `ino_t` (which may or may not
change per Phase 1.2).
### 5.3 `is_zero_ino()` (`include/linux/fs.h:2986`)
Already casts to `(u32)` explicitly. **No change needed.**
## Patch Series Structure (Recommended Ordering)
```
1/N vfs: widen inode hash/lookup functions to take u64
2/N vfs: change i_ino from unsigned long to u64
3/N tracefs: update writeback trace events for u64 i_ino
4/N tracefs: update filelock trace events for u64 i_ino
5/N ext4: update trace events for u64 i_ino
6/N ext4: update error/warning functions for u64 i_ino
7/N f2fs: update trace events for u64 i_ino
8/N proc: update /proc/locks format for u64 i_ino
9/N proc: update /proc/PID/maps for u64 i_ino
10/N nfs: store full 64-bit fileid in i_ino
11/N cifs: store full 64-bit uniqueid in i_ino
12/N <per-filesystem format string fixes as needed>
...
N/N vfs: update ino_t typedef to u64 (if community agrees)
```
Each patch should:
- Build cleanly (no new warnings with W=1)
- Pass `scripts/checkpatch.pl --strict`
- Be bisectable (every commit in the series must compile and run)
## Risks and Considerations
1. **32-bit struct inode size increase**: 4 bytes larger on 32-bit archs. May
affect slab cache efficiency for inode allocation. Embedded systems with
constrained memory could be affected.
2. **Trace event binary format change**: Tools parsing binary trace data
(trace-cmd, perf) will see different field widths. Self-describing format
metadata handles this, but older tools may need updating.
3. **BPF programs**: CO-RE programs handle field relocation automatically.
Legacy BPF programs with hardcoded offsets into `struct inode` on 32-bit
will break. This is expected and acceptable.
4. **Userspace ABI**: No change — `struct stat`, `struct stat64`, `struct statx`
are frozen. The existing overflow checks in `cp_new_stat()`, `cp_old_stat()`,
`cp_compat_stat()` already handle 64-bit ino → 32-bit `st_ino` narrowing
with `-EOVERFLOW`.
5. **`/proc/locks` and `/proc/PID/maps` format**: Wider numbers in output on
32-bit. Parsers using positional fields should be unaffected; parsers using
fixed-width expectations could break.
## Verification
- Build with `make allmodconfig` on x86_64 (catches format string warnings)
- Build with `make allmodconfig ARCH=arm` (32-bit, validates u64 expansion)
- Build with `make allmodconfig ARCH=i386` (32-bit x86)
- Run `scripts/checkpatch.pl --strict` on each patch
- Run `make C=2` (sparse) to catch type mismatches
- Run filesystem-specific selftests (`make kselftest TARGETS=filesystems`)
- Test NFS and CIFS with 64-bit inode numbers on 32-bit kernel
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [LSF/MM/BPF TOPIC] Should we make inode->i_ino a u64?
2026-02-18 15:36 [LSF/MM/BPF TOPIC] Should we make inode->i_ino a u64? Jeff Layton
@ 2026-02-19 14:31 ` Christian Brauner
0 siblings, 0 replies; 2+ messages in thread
From: Christian Brauner @ 2026-02-19 14:31 UTC (permalink / raw)
To: Jeff Layton; +Cc: lsf-pc, linux-fsdevel, Christoph Hellwig, Jan Kara, Al Viro
On Wed, Feb 18, 2026 at 10:36:01AM -0500, Jeff Layton wrote:
> For historical reasons, the inode->i_ino field is an unsigned long.
> Because it's only 32 bits on 32-bit CPUs, this has caused a lot of fs-
> specific hacks on filesystems that have native 64-bit inode numbers
> when running a 32-bit arch.
>
> It would be a lot simpler if we just converted i_ino to be 64-bits and
> dealt with the conversion at the kernel's edges. This would be a non-
> event for the most part on 64-bit arches since unsigned long is already
> 64 bits there.
>
> The kernel itself doesn't deal much with i_ino, so the internal changes
> look fairly straightforward. The bulk of the patches will be to format
> strings and to tracepoints.
>
> I think that the biggest problem will be that this will grow struct
> inode on 32-bit arches by at least 4 bytes. That may have cacheline
> alignment and slab sizing implications. We're actively talking about
> deprecating 32-bit arches in the future however, so maybe we can
> rationalize that away.
If you already have a Claude instance open you may want ask it to please
find the last ten mails about 32-bit that Linus sent and what his
opinions are about worrying about it when doing such changes... :)
> FWIW, I had Claude spin up a plan to do this (attached). It's not bad.
> I'm tempted to tell it generate patches for this, since this is mostly
> a mechanical change, but I'm curious whether anyone else might have
> reasons that we shouldn't go ahead and do it.
Please just do it. I didn't have time to do it myself yet.
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-02-19 14:31 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-02-18 15:36 [LSF/MM/BPF TOPIC] Should we make inode->i_ino a u64? Jeff Layton
2026-02-19 14:31 ` Christian Brauner
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox