Skip to content

fs/vfs: link() fails with ENOSYS instead of aliasing symlink#19210

Closed
FelipeMdeO wants to merge 1 commit into
apache:masterfrom
FelipeMdeO:fix/vfs-link-enosys
Closed

fs/vfs: link() fails with ENOSYS instead of aliasing symlink#19210
FelipeMdeO wants to merge 1 commit into
apache:masterfrom
FelipeMdeO:fix/vfs-link-enosys

Conversation

@FelipeMdeO

Copy link
Copy Markdown
Contributor

Summary

The NuttX VFS has no hard-link support, yet link() was implemented as a thin
alias of symlink():

#ifdef CONFIG_PSEUDOFS_SOFTLINKS
int link(FAR const char *path1, FAR const char *path2)
{
  return symlink(path1, path2);
}
#endif

This is wrong in two independent ways, and fixing it correctly is why the
change is not a one-liner
.

1. Incorrect POSIX semantics. link() is defined to create a hard link.
Silently creating a symbolic link instead is surprising and breaks callers
that rely on the distinction. In particular, the common portability pattern of
probing for hard-link support — call link() and check for failure — was told
the operation succeeded, having quietly created a symlink it never asked for.
The correct answer for a filesystem with no hard links is -1 / ENOSYS.

2. The hard-link API was gated behind an unrelated feature. Returning
ENOSYS from the function body alone fixes nothing, because the whole of
fs_link.c was compiled only under CONFIG_PSEUDOFS_SOFTLINKS. When soft links
are disabled, link() did not exist at all — so editing its body changes
nothing for those configurations; link() simply remains an undefined symbol.
Hard links and soft links are independent concepts, so gating link() (hard
link) on CONFIG_PSEUDOFS_SOFTLINKS (soft links) is a category error. Making
link() report ENOSYS regardless of soft-link configuration therefore
requires decoupling it from that option.

That decoupling is a single logical change, but NuttX expresses the same build
condition in four parallel places, so the same CONFIG_PSEUDOFS_SOFTLINKS
guard had to be removed from each:

File Role
fs/vfs/fs_link.c the #ifdef wrapping the function itself
fs/vfs/Make.defs Make build
fs/vfs/CMakeLists.txt CMake build
syscall/syscall.csv syscall stub generation

These are four copies of one condition, not four independent changes; leaving
the guard in any of them would make the tree inconsistent (e.g. compiled but
absent from the syscall table, or present in CMake but not in Make).

fs_symlink.c and fs_readlink.c remain under CONFIG_PSEUDOFS_SOFTLINKS, as
they should — soft-link support is untouched (see Impact).

This was already a latent inconsistency, not just a cosmetic issue.
libs/libc/unistd/lib_linkat.c is built unconditionally and is implemented on
top of link():

ret = link(oldfullpath, newfullpath);   /* lib_linkat.c */

So the tree already assumed link() is always available — directly
contradicting the CONFIG_PSEUDOFS_SOFTLINKS guard. Without soft links,
linkat() referenced a link() that was never built. After this change
fs_link.c is always compiled, link() always returns -1/ENOSYS, and
linkat() has a consistent, always-present backend.

int link(FAR const char *path1, FAR const char *path2)
{
  (void)path1;
  (void)path2;

  set_errno(ENOSYS);
  return ERROR;
}

Impact

Soft-link support (CONFIG_PSEUDOFS_SOFTLINKS) is fully preserved. This
change does not touch soft links: symlink()/readlink() remain gated by the
option and behave exactly as before. The two features are orthogonal —
CONFIG_PSEUDOFS_SOFTLINKS provides symbolic links via symlink(), while
link() is the hard-link API. Enabling soft links should not, and now does
not, change the behavior of the hard-link API. The only difference for a
CONFIG_PSEUDOFS_SOFTLINKS=y build is that link() no longer silently creates
a symlink:

Call SOFTLINKS=y, before SOFTLINKS=y, after
symlink() creates a symlink creates a symlink (unchanged)
readlink() reads a symlink reads a symlink (unchanged)
link() silently created a symlink -1 / ENOSYS

A caller that wants a symbolic link must use symlink() (the correct API,
already available when the option is enabled); a caller of link() wants a
hard link, which NuttX supports in no configuration, so ENOSYS is the
correct result with or without soft links. No in-tree caller relied on the old
aliasing.

Other effects:

  • Configurations without soft links: link() now exists and returns
    ENOSYS instead of being an undefined reference, and linkat() behaves
    consistently in every configuration.
  • Build: fs_link.c is now always compiled — a tiny ENOSYS stub, no new
    dependencies and no Kconfig changes. No size impact beyond a few bytes.
  • API / docs: link() is still declared in <unistd.h>; only its runtime
    behavior changes, and it is now standards-compliant for a filesystem without
    hard-link support.
  • Rejected alternative: simply enabling CONFIG_PSEUDOFS_SOFTLINKS in a
    board defconfig would pull in the entire symlink/readlink machinery just to
    make a hard-link call report "unsupported", keeps the incorrect coupling, and
    hides a VFS-wide issue behind a per-board workaround instead of fixing it in
    the place the behavior actually lives.

Testing

Host: <Ubuntu 24.04 x86_64>
Board: esp32c3-devkit, configuration without CONFIG_PSEUDOFS_SOFTLINKS

Built and exercised on the esp32c3-devkit: link() returns -1 with
errno == ENOSYS, and linkat() resolves consistently in a build with soft
links disabled.

NuttX VFS has no hard-link support; link() aliased symlink() and only
under CONFIG_PSEUDOFS_SOFTLINKS, being absent otherwise. Always build
fs_link.c and return -1/ENOSYS so callers can detect the missing
support instead of getting a symlink or an undefined reference.

Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
@github-actions github-actions Bot added Area: File System File System issues Size: S The size of the change in this PR is small labels Jun 24, 2026
@github-actions

Copy link
Copy Markdown

MemBrowse Memory Report

No memory changes detected for:

@FelipeMdeO FelipeMdeO closed this Jun 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: File System File System issues Size: S The size of the change in this PR is small

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant