Skip to content

Add non-mmap fallback for unsupported file systems (jffs2) - #1

Open
abcfy2 wants to merge 1 commit into
SagerNet:masterfrom
abcfy2:feat/mmap-fallback
Open

Add non-mmap fallback for unsupported file systems (jffs2)#1
abcfy2 wants to merge 1 commit into
SagerNet:masterfrom
abcfy2:feat/mmap-fallback

Conversation

@abcfy2

Copy link
Copy Markdown

Background

ResolvesSagerNet/sing-box#4251.

On embedded devices where the cache file lives on a file system that does not support mmap (e.g. jffs2 on Asus Merlin routers, or certain OpenWrt MTD-backed partitions), bbolt.Open() fails with:

FATAL start service: initialize cache-file: invalid argument

The root cause: jffs2 rejects mmap(PROT_READ, MAP_SHARED) for files opened O_RDWR with EINVAL, because jffs2 only supports read-only shared mappings. bbolt's entire design assumes mmap, so this is fatal.

Solution

Port the mmap fallback implementation from MetaCubeX/bbolt (commit d4ec34a, authored by @wwqgtxx).

When mmap() fails with an error indicating the file system does not support memory mapping (ENOSYS, ENODEV, EOPNOTSUPP, ENOTSUP, EINVAL), bbolt transparently falls back to a heap-backed buffer that mirrors the data file via ReadAt/WriteAt. This preserves all existing read/write semantics with zero API changes — callers like sing-box need no modifications.

Changes

FileChange
mmap_fallback.goNew — heap mirror layer: mmapFallback(), munmapFallback(), copyToMmapFallback()
mmap_error_unix.goNewisMmapUnsupported() for Linux/macOS/BSD (EINVAL, ENOSYS, ENODEV, EOPNOTSUPP, ENOTSUP)
mmap_error_windows.goNewisMmapUnsupported() for Windows
db.goAdd mmapFallback field; mmap fail → fallback branch; munmap() dispatch; new writeAt() method that mirrors writes to heap buffer in fallback mode
tx.goRoute write path through db.writeAt() (2 call sites) so heap mirror stays consistent
bolt_openbsd.goSkip msync() in fallback mode (heap buffer doesn't need it)

All new symbols are unexported (internal to the package). No public API changes.

Adaptations from upstream Mihomo patch

  • Use root-package maxMapSize instead of common.MaxMapSize (sagernet fork keeps arch constants in bolt_<arch>.go)
  • Remove Logger calls (sagernet fork has no Logger() method)
  • Remove MaxSize field checks (not present in this fork)
  • Add db.writeAt() method + route tx.go through it (sagernet fork lacked this abstraction; Mihomo already had it)

Verification

Unit behavior

  • go build ./... passes
  • go vet ./... passes

End-to-end on simulated jffs2

Tested with mtdram + jffs2 mount (the same method documented in MetaCubeX/mihomo#2922):

Control (original bbolt, no patch):

FATAL[0001] start service: initialize cache-file: invalid argument

Patched bbolt (with fallback):

INFO[0000] sing-box started (0.01s)
cache.db created (32768 bytes)

Data persistence (selector switch → restart → verify):

  • Switch selector direct → block via Clash API
  • Kill sing-box, keep cache.db on jffs2
  • Restart → selector reads back as block

The jffs2 mmap rejection (errno=22 EINVAL) was confirmed before testing:

mmap.mmap(fd, 32768, flags=mmap.MAP_SHARED, prot=mmap.PROT_READ)
# OSError: [Errno 22] Invalid argument

Port the mmap fallback implementation from MetaCubeX/bbolt (commit
d4ec34a) to support file systems that do not provide mmap, such as
jffs2 on Asus Merlin routers.
When mmap fails with ENOSYS/ENODEV/EOPNOTSUPP/ENOTSUP/EINVAL, bbolt
now transparently falls back to a heap-backed buffer that mirrors the
data file, instead of returning a fatal error. All read/write
semantics remain identical; the fallback is automatic and requires no
API changes for callers.
FixesSagerNet/sing-box#4251
@abcfy2

abcfy2 commented Jul 30, 2026

Copy link
Copy Markdown
Author

@nekohasekai PLS review this PR. Thanks.

After merge this PR, then upgrade dependencies of sing-box will resolve mmap issue.

Sign up for freeto 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.

华硕路由器使用 cache_file 报错: FATAL[0001] start service: initialize cache-file: invalid argument

1 participant

@abcfy2