Open Source ยท Apache 2.0

Mount an S3 bucket.
Get a filesystem that tells you the truth.

ObjectFS presents a POSIX interface over AWS S3 for research computing. Every object it writes carries a SHA-256. A failed upload fails close(2). The operations it cannot implement return an error instead of pretending.

SHA-256 on every write Errors reach close(2) Read-modify-write flush Multi-tier cache Linux & macOS

Runs with your AWS credentials ยท Not a POSIX-compliant filesystem, deliberately

What is on the mount path

Everything below is reachable from a running mount. Capabilities that have code but no path from a mount are listed separately in the docs rather than mixed in here.

Integrity

Verified reads

Every object ObjectFS writes records a SHA-256 of its uncompressed content. A read that returns a complete object verifies that hash and refuses a mismatch.

  • Catches truncation, bit-rot, a lost Content-Encoding
  • Refused with an integrity error, not returned with exit 0
  • Partial reads of large objects are not verified โ€” see below
Write path

Real read-modify-write

Writing at an offset buffers dirty byte ranges, fetches the ranges of the stored object it needs, splices, and PUTs the result โ€” it does not replace the object with the fragment you wrote.

  • Write at any offset; truncate and extend both work
  • close/fsync are synchronous and return errors
  • Growing a file leaves a hole that reads as zeros
Performance

Caching and read-ahead

An LRU tier, an optional persistent tier on local disk, and a prefetcher that runs ahead of a sequential reader. A read served from cache makes no S3 request.

  • Large reads fanned out as concurrent ranged GETs
  • Read-ahead tuned by performance.read_ahead
  • Multipart uploads for large files
AWS first

S3 capabilities, probed

AWS S3 is the target, not a lowest common denominator. Capabilities are established by probing the endpoint โ€” never a config flag or a URL heuristic.

  • Transfer Acceleration falls back silently when absent
  • Conditional writes fail closed rather than drop a precondition
  • MinIO, Ceph RGW and RustFS are best-effort, and say so
Operations

Observable while it runs

A Prometheus /metrics endpoint and a /health endpoint, both served by the mount process, both defaulting to loopback because neither is authenticated.

  • Billable S3 request counts and dollar figures, counted in the SDK response path
  • One templated systemd unit, objectfs@.service
  • Strict config decoding โ€” a typo'd key fails at startup, by name
Storage

Encryption and compression

Server-side encryption on every object, SSE-S3 or SSE-KMS. Transparent compression is available and off by default.

  • ZSTD, LZ4 and gzip, opt-in per mount
  • Off by default: a compressed object is no longer readable by aws s3 cp
  • Storage class per mount via storage_tier

What it does not do

S3 is not a disk, and the gap cannot be closed by wishing. These fail, and the failure is the correct answer rather than a missing feature.

โœ•Atomic rename
A rename is a server-side copy then a delete, per object โ€” S3 has no atomic rename to use. A concurrent reader can briefly see both names, and an interruption leaves the data at the old name, the new one, or both, never at neither. RENAME_NOREPLACE and RENAME_EXCHANGE are refused with EINVAL rather than approximated.
โœ•Cross-host locking
Locks are never forwarded to the filesystem, so they are tracked on the mounting host and mean nothing to any other mount of the same bucket. This is why SQLite and Git-inside-the-mount do not work across hosts: two nodes will both believe they hold the same exclusive lock, and nothing will say otherwise.
โœ•Concurrent-writer safety
Two nodes writing the same object produce one winner and no error. Nothing detects the conflict today.
โœ•Hard links, symlinks, devices
link has no meaning when a key names exactly one object โ€” it will never be supported. symlink, mknod and fallocate return ENOTSUP.
โœ•setuid, setgid, sticky bits
Access here is decided by the AWS credentials the process holds, not by a mode bit. A stored setuid bit would promise an escalation that cannot happen. For the same reason security.* and system.* xattrs are refused: object metadata is writable by anyone with bucket write access, so an attribute the kernel acts on must not be stored there.
โœ•Checksums on partial reads
The recorded hash covers the whole object, so verifying a 4 KiB read of a 10 GiB object would mean transferring 10 GiB. Objects ObjectFS did not write carry no checksum at all and read unverified โ€” refusing them would make it unable to mount the buckets it exists for.
โœ•Windows
Every file in internal/fuse is //go:build linux || darwin. macOS needs macFUSE.

The supported-operations table is the contract โ€” derived from the methods that exist, not from intent. Read it before pointing a workload at a mount.

Install

The install script resolves your platform, verifies the SHA-256 published beside the binary, and installs under ~/.local/bin.

curl -fsSL https://raw.githubusercontent.com/scttfrdmn/objectfs/main/scripts/install.sh | bash

# elsewhere, or pinned to a release
curl -fsSL https://raw.githubusercontent.com/scttfrdmn/objectfs/main/scripts/install.sh \
  | bash -s -- --prefix /usr/local --version v0.13.0
# Packages are attached to each release from v0.14.0 onward.
# There is no apt or yum repository to add โ€” install the downloaded file.

apt install ./objectfs_*.deb      # Debian, Ubuntu
dnf install ./objectfs-*.rpm      # RHEL, Fedora, openSUSE
git clone https://github.com/scttfrdmn/objectfs.git
cd objectfs
make build          # produces ./bin/objectfs

# or, without a checkout
go install github.com/scttfrdmn/objectfs/cmd/objectfs@latest

# macOS needs macFUSE first
brew install --cask macfuse

The default prefix is ~/.local rather than /usr/local deliberately: many of this project's users are on a shared login node with no root, and an installer whose default needs sudo teaches people to run the whole thing under sudo โ€” which writes a root-owned binary and root-owned cache directories into a home their own jobs use.

The checksum is always verified and there is no flag to skip it. Note what that does and does not establish: the .sha256 travels the same channel as the tarball, so a mismatch means a corrupted or tampered download โ€” not that the release itself is authentic. That is a signature's job, and this project does not sign releases yet.

How this project decides

Integrity first, performance a close second. When the two conflict, the order is not negotiable.

An error must reach the caller

A failed PUT fails close(2). rm on a missing file is ENOENT, not a silent success. go-fuse defaults an unimplemented Unlink to success, which once let rm exit 0 while the object survived โ€” the kind of defect that only surfaces as lost data months later.

Capabilities are probed, never asked

A store that accepts a header and ignores it looks identical to one that honours it, from every angle except the outcome. So there is no config flag or endpoint heuristic for a backend capability. If a probe cannot get an answer, the capability is absent.

Performance degrades, correctness refuses

Transfer Acceleration falling back to a plain endpoint is a correct outcome โ€” slower. A dropped write precondition is not: it tells every contender it won. Correctness capabilities fail closed with an operator-facing reason.

No unmeasured numbers

This site quotes no throughput figures because no benchmark in the repository produced any. A table of five once sat in the docs โ€” 800โ€“1200 MB/s, a 4.6ร— BBR improvement โ€” and not one was measured. benchmarks/ exists; until it is run against a named bucket and object size, the honest answer is silence.

Tests go through the real seam

A mock on the far side of a seam agrees with its caller by construction, which is how 32,680 lines of tests missed roughly 45 defects. The suite now runs the real S3 backend against an in-process endpoint over real HTTP, plus a differential oracle that replays an operation sequence against the local filesystem and demands they agree.

Documentation is checked, not written

Tests assert that documented config keys exist in the schema, that documented Go symbols exist, and that the docs navigation matches the files on disk. Prose has no way to be told it has gone stale, so the parts that matter are gates instead.