diff --git a/src/libexpr/eval.cc b/src/libexpr/eval.cc
index 36f2cd7d7..9d60676f5 100644
--- a/src/libexpr/eval.cc
+++ b/src/libexpr/eval.cc
@@ -246,6 +246,30 @@ EvalState::EvalState(
     }
     , repair(NoRepair)
     , emptyBindings(0)
+    , storeFS(
+        makeMountedSourceAccessor(
+            {
+                {CanonPath::root, makeEmptySourceAccessor()},
+                /* In the pure eval case, we can simply require
+                   valid paths. However, in the *impure* eval
+                   case this gets in the way of the union
+                   mechanism, because an invalid access in the
+                   upper layer will *not* be caught by the union
+                   source accessor, but instead abort the entire
+                   lookup.
+
+                   This happens when the store dir in the
+                   ambient file system has a path (e.g. because
+                   another Nix store there), but the relocated
+                   store does not.
+
+                   TODO make the various source accessors doing
+                   access control all throw the same type of
+                   exception, and make union source accessor
+                   catch it, so we don't need to do this hack.
+                 */
+                {CanonPath(store->storeDir), store->getFSAccessor(settings.pureEval)},
+            }))
     , rootFS(
         ({
             /* In pure eval mode, we provide a filesystem that only
@@ -261,11 +285,6 @@ EvalState::EvalState(
 
             auto realStoreDir = dirOf(store->toRealPath(StorePath::dummy));
             if (settings.pureEval || store->storeDir != realStoreDir) {
-                auto storeFS = makeMountedSourceAccessor(
-                    {
-                        {CanonPath::root, makeEmptySourceAccessor()},
-                        {CanonPath(store->storeDir), makeFSSourceAccessor(realStoreDir)}
-                    });
                 accessor = settings.pureEval
                     ? storeFS
                     : makeUnionSourceAccessor({accessor, storeFS});
diff --git a/src/libexpr/include/nix/expr/eval.hh b/src/libexpr/include/nix/expr/eval.hh
index 0933c6e89..61da225fc 100644
--- a/src/libexpr/include/nix/expr/eval.hh
+++ b/src/libexpr/include/nix/expr/eval.hh
@@ -265,6 +265,11 @@ public:
     /** `"unknown"` */
     Value vStringUnknown;
 
+    /**
+     * The accessor corresponding to `store`.
+     */
+    const ref<SourceAccessor> storeFS;
+
     /**
      * The accessor for the root filesystem.
      */
diff --git a/src/libfetchers/store-path-accessor.cc b/src/libfetchers/store-path-accessor.cc
index bed51541e..f389d0327 100644
--- a/src/libfetchers/store-path-accessor.cc
+++ b/src/libfetchers/store-path-accessor.cc
@@ -5,11 +5,7 @@ namespace nix {
 
 ref<SourceAccessor> makeStorePathAccessor(ref<Store> store, const StorePath & storePath)
 {
-    // FIXME: should use `store->getFSAccessor()`
-    auto root = std::filesystem::path{store->toRealPath(storePath)};
-    auto accessor = makeFSSourceAccessor(root);
-    accessor->setPathDisplay(root.string());
-    return accessor;
+    return projectSubdirSourceAccessor(store->getFSAccessor(), storePath.to_string());
 }
 
 }
diff --git a/src/libstore/build/worker.cc b/src/libstore/build/worker.cc
index ae50dc3b5..66c31d39e 100644
--- a/src/libstore/build/worker.cc
+++ b/src/libstore/build/worker.cc
@@ -524,7 +524,7 @@ bool Worker::pathContentsGood(const StorePath & path)
         res = false;
     else {
         auto current = hashPath(
-            {store.getFSAccessor(), CanonPath(store.printStorePath(path))},
+            {store.getFSAccessor(), CanonPath(path.to_string())},
             FileIngestionMethod::NixArchive, info->narHash.algo).first;
         Hash nullHash(HashAlgorithm::SHA256);
         res = info->narHash == nullHash || info->narHash == current;
diff --git a/src/libstore/dummy-store.cc b/src/libstore/dummy-store.cc
index 7252e1d33..80367d597 100644
--- a/src/libstore/dummy-store.cc
+++ b/src/libstore/dummy-store.cc
@@ -83,7 +83,9 @@ struct DummyStore : public virtual DummyStoreConfig, public virtual Store
     { callback(nullptr); }
 
     virtual ref<SourceAccessor> getFSAccessor(bool requireValidPath) override
-    { unsupported("getFSAccessor"); }
+    {
+        return makeEmptySourceAccessor();
+    }
 };
 
 static RegisterStoreImplementation<DummyStore, DummyStoreConfig> regDummyStore;
diff --git a/src/libstore/local-fs-store.cc b/src/libstore/local-fs-store.cc
index 599765ced..c6c5d53c9 100644
--- a/src/libstore/local-fs-store.cc
+++ b/src/libstore/local-fs-store.cc
@@ -33,30 +33,35 @@ struct LocalStoreAccessor : PosixSourceAccessor
     bool requireValidPath;
 
     LocalStoreAccessor(ref<LocalFSStore> store, bool requireValidPath)
-        : store(store)
+        : PosixSourceAccessor(std::filesystem::path{store->realStoreDir.get()})
+        , store(store)
         , requireValidPath(requireValidPath)
-    { }
-
-    CanonPath toRealPath(const CanonPath & path)
     {
-        auto [storePath, rest] = store->toStorePath(path.abs());
+    }
+
+
+    void requireStoreObject(const CanonPath & path)
+    {
+        auto [storePath, rest] = store->toStorePath(store->storeDir + path.abs());
         if (requireValidPath && !store->isValidPath(storePath))
             throw InvalidPath("path '%1%' is not a valid store path", store->printStorePath(storePath));
-        return CanonPath(store->getRealStoreDir()) / storePath.to_string() / CanonPath(rest);
     }
 
     std::optional<Stat> maybeLstat(const CanonPath & path) override
     {
-        /* Handle the case where `path` is (a parent of) the store. */
-        if (isDirOrInDir(store->storeDir, path.abs()))
+        /* Also allow `path` to point to the entire store, which is
+           needed for resolving symlinks. */
+        if (path.isRoot())
             return Stat{ .type = tDirectory };
 
-        return PosixSourceAccessor::maybeLstat(toRealPath(path));
+        requireStoreObject(path);
+        return PosixSourceAccessor::maybeLstat(path);
     }
 
     DirEntries readDirectory(const CanonPath & path) override
     {
-        return PosixSourceAccessor::readDirectory(toRealPath(path));
+        requireStoreObject(path);
+        return PosixSourceAccessor::readDirectory(path);
     }
 
     void readFile(
@@ -64,12 +69,14 @@ struct LocalStoreAccessor : PosixSourceAccessor
         Sink & sink,
         std::function<void(uint64_t)> sizeCallback) override
     {
-        return PosixSourceAccessor::readFile(toRealPath(path), sink, sizeCallback);
+        requireStoreObject(path);
+        return PosixSourceAccessor::readFile(path, sink, sizeCallback);
     }
 
     std::string readLink(const CanonPath & path) override
     {
-        return PosixSourceAccessor::readLink(toRealPath(path));
+        requireStoreObject(path);
+        return PosixSourceAccessor::readLink(path);
     }
 };
 
diff --git a/src/libstore/local-store.cc b/src/libstore/local-store.cc
index fff0b35bf..1c6d6bced 100644
--- a/src/libstore/local-store.cc
+++ b/src/libstore/local-store.cc
@@ -1102,7 +1102,7 @@ void LocalStore::addToStore(const ValidPathInfo & info, Source & source,
                     auto & specified = *info.ca;
                     auto actualHash = ({
                         auto accessor = getFSAccessor(false);
-                        CanonPath path { printStorePath(info.path) };
+                        CanonPath path { info.path.to_string() };
                         Hash h { HashAlgorithm::SHA256 }; // throwaway def to appease C++
                         auto fim = specified.method.getFileIngestionMethod();
                         switch (fim) {
diff --git a/src/libstore/remote-fs-accessor.cc b/src/libstore/remote-fs-accessor.cc
index 340e7ee2e..fdbe12fa9 100644
--- a/src/libstore/remote-fs-accessor.cc
+++ b/src/libstore/remote-fs-accessor.cc
@@ -51,7 +51,7 @@ ref<SourceAccessor> RemoteFSAccessor::addToCache(std::string_view hashPart, std:
 
 std::pair<ref<SourceAccessor>, CanonPath> RemoteFSAccessor::fetch(const CanonPath & path)
 {
-    auto [storePath, restPath_] = store->toStorePath(path.abs());
+    auto [storePath, restPath_] = store->toStorePath(store->storeDir + path.abs());
     auto restPath = CanonPath(restPath_);
 
     if (requireValidPath && !store->isValidPath(storePath))
diff --git a/src/libstore/store-api.cc b/src/libstore/store-api.cc
index 9e606d0ab..e9e982e61 100644
--- a/src/libstore/store-api.cc
+++ b/src/libstore/store-api.cc
@@ -1233,7 +1233,7 @@ static Derivation readDerivationCommon(Store & store, const StorePath & drvPath,
     auto accessor = store.getFSAccessor(requireValidPath);
     try {
         return parseDerivation(store,
-            accessor->readFile(CanonPath(store.printStorePath(drvPath))),
+            accessor->readFile(CanonPath(drvPath.to_string())),
             Derivation::nameFromPath(drvPath));
     } catch (FormatError & e) {
         throw Error("error parsing derivation '%s': %s", store.printStorePath(drvPath), e.msg());
diff --git a/src/libutil/include/nix/util/source-accessor.hh b/src/libutil/include/nix/util/source-accessor.hh
index 3a28b2c2b..5ef660150 100644
--- a/src/libutil/include/nix/util/source-accessor.hh
+++ b/src/libutil/include/nix/util/source-accessor.hh
@@ -222,4 +222,10 @@ ref<SourceAccessor> makeMountedSourceAccessor(std::map<CanonPath, ref<SourceAcce
  */
 ref<SourceAccessor> makeUnionSourceAccessor(std::vector<ref<SourceAccessor>> && accessors);
 
+/**
+ * Creates a new source accessor which is confined to the subdirectory
+ * of the given source accessor.
+ */
+ref<SourceAccessor> projectSubdirSourceAccessor(ref<SourceAccessor>, CanonPath subdirectory);
+
 }
diff --git a/src/libutil/meson.build b/src/libutil/meson.build
index e9fb73d39..782c361e0 100644
--- a/src/libutil/meson.build
+++ b/src/libutil/meson.build
@@ -142,6 +142,7 @@ sources = [config_priv_h] + files(
   'signature/signer.cc',
   'source-accessor.cc',
   'source-path.cc',
+  'subdir-source-accessor.cc',
   'strings.cc',
   'suggestions.cc',
   'tarfile.cc',
diff --git a/src/libutil/source-accessor.cc b/src/libutil/source-accessor.cc
index fc0d6cff1..b9ebc82b6 100644
--- a/src/libutil/source-accessor.cc
+++ b/src/libutil/source-accessor.cc
@@ -114,9 +114,11 @@ CanonPath SourceAccessor::resolveSymlinks(
                     if (!linksAllowed--)
                         throw Error("infinite symlink recursion in path '%s'", showPath(path));
                     auto target = readLink(res);
-                    res.pop();
-                    if (isAbsolute(target))
+                    if (isAbsolute(target)) {
                         res = CanonPath::root;
+                    } else {
+                        res.pop();
+                    }
                     todo.splice(todo.begin(), tokenizeString<std::list<std::string>>(target, "/"));
                 }
             }
diff --git a/src/libutil/subdir-source-accessor.cc b/src/libutil/subdir-source-accessor.cc
new file mode 100644
index 000000000..265836118
--- /dev/null
+++ b/src/libutil/subdir-source-accessor.cc
@@ -0,0 +1,59 @@
+#include "nix/util/source-accessor.hh"
+
+namespace nix {
+
+struct SubdirSourceAccessor : SourceAccessor
+{
+    ref<SourceAccessor> parent;
+
+    CanonPath subdirectory;
+
+    SubdirSourceAccessor(ref<SourceAccessor> && parent, CanonPath && subdirectory)
+        : parent(std::move(parent))
+        , subdirectory(std::move(subdirectory))
+    {
+        displayPrefix.clear();
+    }
+
+    std::string readFile(const CanonPath & path) override
+    {
+        return parent->readFile(subdirectory / path);
+    }
+
+    void readFile(const CanonPath & path, Sink & sink, std::function<void(uint64_t)> sizeCallback) override
+    {
+        return parent->readFile(subdirectory / path, sink, sizeCallback);
+    }
+
+    bool pathExists(const CanonPath & path) override
+    {
+        return parent->pathExists(subdirectory / path);
+    }
+
+    std::optional<Stat> maybeLstat(const CanonPath & path) override
+    {
+        return parent->maybeLstat(subdirectory / path);
+    }
+
+    DirEntries readDirectory(const CanonPath & path) override
+    {
+        return parent->readDirectory(subdirectory / path);
+    }
+
+    std::string readLink(const CanonPath & path) override
+    {
+        return parent->readLink(subdirectory / path);
+    }
+
+    std::string showPath(const CanonPath & path) override
+    {
+        return displayPrefix + parent->showPath(subdirectory / path) + displaySuffix;
+    }
+};
+
+ref<SourceAccessor> projectSubdirSourceAccessor(ref<SourceAccessor> parent, CanonPath subdirectory)
+{
+    return make_ref<SubdirSourceAccessor>(std::move(parent), std::move(subdirectory));
+}
+
+}
diff --git a/src/nix-store/nix-store.cc b/src/nix-store/nix-store.cc
index fbbb57f43..23d4071e9 100644
--- a/src/nix-store/nix-store.cc
+++ b/src/nix-store/nix-store.cc
@@ -563,7 +563,7 @@ static void registerValidity(bool reregister, bool hashGiven, bool canonicalise)
 #endif
             if (!hashGiven) {
                 HashResult hash = hashPath(
-                    {store->getFSAccessor(false), CanonPath { store->printStorePath(info->path) }},
+                    {store->getFSAccessor(false), CanonPath { info->path.to_string() }},
                     FileSerialisationMethod::NixArchive, HashAlgorithm::SHA256);
                 info->narHash = hash.first;
                 info->narSize = hash.second;
diff --git a/src/nix/cat.cc b/src/nix/cat.cc
index a790c0301..aa27446d2 100644
--- a/src/nix/cat.cc
+++ b/src/nix/cat.cc
@@ -6,21 +6,21 @@ using namespace nix;
 
 struct MixCat : virtual Args
 {
-    std::string path;
-
-    void cat(ref<SourceAccessor> accessor)
+    void cat(ref<SourceAccessor> accessor, CanonPath path)
     {
-        auto st = accessor->lstat(CanonPath(path));
+        auto st = accessor->lstat(path);
         if (st.type != SourceAccessor::Type::tRegular)
-            throw Error("path '%1%' is not a regular file", path);
+            throw Error("path '%1%' is not a regular file", path.abs());
         logger->stop();
 
-        writeFull(getStandardOutput(), accessor->readFile(CanonPath(path)));
+        writeFull(getStandardOutput(), accessor->readFile(path));
     }
 };
 
 struct CmdCatStore : StoreCommand, MixCat
 {
+    std::string path;
+
     CmdCatStore()
     {
         expectArgs({
@@ -44,7 +44,8 @@ struct CmdCatStore : StoreCommand, MixCat
 
     void run(ref<Store> store) override
     {
-        cat(store->getFSAccessor());
+        auto [storePath, rest] = store->toStorePath(path);
+        cat(store->getFSAccessor(), CanonPath{storePath.to_string()} / CanonPath{rest});
     }
 };
 
@@ -52,6 +53,8 @@ struct CmdCatNar : StoreCommand, MixCat
 {
     Path narPath;
 
+    std::string path;
+
     CmdCatNar()
     {
         expectArgs({
@@ -76,7 +79,7 @@ struct CmdCatNar : StoreCommand, MixCat
 
     void run(ref<Store> store) override
     {
-        cat(makeNarAccessor(readFile(narPath)));
+        cat(makeNarAccessor(readFile(narPath)), CanonPath{path});
     }
 };
 
diff --git a/src/nix/env.cc b/src/nix/env.cc
index f6b12f21c..277bd0fdd 100644
--- a/src/nix/env.cc
+++ b/src/nix/env.cc
@@ -65,11 +65,11 @@ struct CmdShell : InstallablesCommand, MixEnvironment
 
     void run(ref<Store> store, Installables && installables) override
     {
+        auto state = getEvalState();
+
         auto outPaths =
             Installable::toStorePaths(getEvalStore(), store, Realise::Outputs, OperateOn::Output, installables);
 
-        auto accessor = store->getFSAccessor();
-
         std::unordered_set<StorePath> done;
         std::queue<StorePath> todo;
         for (auto & path : outPaths)
@@ -85,13 +85,16 @@ struct CmdShell : InstallablesCommand, MixEnvironment
             if (!done.insert(path).second)
                 continue;
 
-            if (true)
-                pathAdditions.push_back(store->printStorePath(path) + "/bin");
+            auto binDir = state->storeFS->resolveSymlinks(CanonPath(store->printStorePath(path)) / "bin");
+            if (!store->isInStore(binDir.abs()))
+                throw Error("path '%s' is not in the Nix store", binDir);
 
-            auto propPath = accessor->resolveSymlinks(
+            pathAdditions.push_back(binDir.abs());
+
+            auto propPath = state->storeFS->resolveSymlinks(
                 CanonPath(store->printStorePath(path)) / "nix-support" / "propagated-user-env-packages");
-            if (auto st = accessor->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) {
-                for (auto & p : tokenizeString<Paths>(accessor->readFile(propPath)))
+            if (auto st = state->storeFS->maybeLstat(propPath); st && st->type == SourceAccessor::tRegular) {
+                for (auto & p : tokenizeString<Paths>(state->storeFS->readFile(propPath)))
                     todo.push(store->parseStorePath(p));
             }
         }
@@ -108,7 +111,7 @@ struct CmdShell : InstallablesCommand, MixEnvironment
 
         // Release our references to eval caches to ensure they are persisted to disk, because
         // we are about to exec out of this process without running C++ destructors.
-        getEvalState()->evalCaches.clear();
+        state->evalCaches.clear();
 
         execProgramInStore(store, UseLookupPath::Use, *command.begin(), args);
     }
diff --git a/src/nix/ls.cc b/src/nix/ls.cc
index 1a90ed074..4b282bc43 100644
--- a/src/nix/ls.cc
+++ b/src/nix/ls.cc
@@ -8,8 +8,6 @@ using namespace nix;
 
 struct MixLs : virtual Args, MixJSON
 {
-    std::string path;
-
     bool recursive = false;
     bool verbose = false;
     bool showDirectory = false;
@@ -38,7 +36,7 @@ struct MixLs : virtual Args, MixJSON
         });
     }
 
-    void listText(ref<SourceAccessor> accessor)
+    void listText(ref<SourceAccessor> accessor, CanonPath path)
     {
         std::function<void(const SourceAccessor::Stat &, const CanonPath &, std::string_view, bool)> doPath;
 
@@ -77,26 +75,27 @@ struct MixLs : virtual Args, MixJSON
                 showFile(curPath, relPath);
         };
 
-        auto path2 = CanonPath(path);
-        auto st = accessor->lstat(path2);
-        doPath(st, path2,
-            st.type == SourceAccessor::Type::tDirectory ? "." : path2.baseName().value_or(""),
+        auto st = accessor->lstat(path);
+        doPath(st, path,
+            st.type == SourceAccessor::Type::tDirectory ? "." : path.baseName().value_or(""),
             showDirectory);
     }
 
-    void list(ref<SourceAccessor> accessor)
+    void list(ref<SourceAccessor> accessor, CanonPath path)
     {
         if (json) {
             if (showDirectory)
                 throw UsageError("'--directory' is useless with '--json'");
-            logger->cout("%s", listNar(accessor, CanonPath(path), recursive));
+            logger->cout("%s", listNar(accessor, path, recursive));
         } else
-            listText(accessor);
+            listText(accessor, std::move(path));
     }
 };
 
 struct CmdLsStore : StoreCommand, MixLs
 {
+    std::string path;
+
     CmdLsStore()
     {
         expectArgs({
@@ -120,7 +119,8 @@ struct CmdLsStore : StoreCommand, MixLs
 
     void run(ref<Store> store) override
     {
-        list(store->getFSAccessor());
+        auto [storePath, rest] = store->toStorePath(path);
+        list(store->getFSAccessor(), CanonPath{storePath.to_string()} / CanonPath{rest});
     }
 };
 
@@ -128,6 +128,8 @@ struct CmdLsNar : Command, MixLs
 {
     Path narPath;
 
+    std::string path;
+
     CmdLsNar()
     {
         expectArgs({
@@ -152,7 +154,7 @@ struct CmdLsNar : Command, MixLs
 
     void run() override
     {
-        list(makeNarAccessor(readFile(narPath)));
+        list(makeNarAccessor(readFile(narPath)), CanonPath{path});
     }
 };
 
diff --git a/src/nix/why-depends.cc b/src/nix/why-depends.cc
index 8dfd8343f..5de32caae 100644
--- a/src/nix/why-depends.cc
+++ b/src/nix/why-depends.cc
@@ -172,7 +172,7 @@ struct CmdWhyDepends : SourceExprCommand, MixOperateOnOptions
         struct BailOut { };
 
         printNode = [&](Node & node, const std::string & firstPad, const std::string & tailPad) {
-            CanonPath pathS(store->printStorePath(node.path));
+            CanonPath pathS(node.path.to_string());
 
             assert(node.dist != inf);
             if (precise) {