Gerrit Review Dashboard

Hiroshi Nishida · review.whamcloud.com
updated 2026-09-09 17:00:04 UTC
16
needs action
5
longstanding
9
in CI
0
ready to land

Needs your action

16 new · 5 longstanding
68288 master J ✗1B ✓T …R 0/2−
LU-20637 llapi: name a device scan's objects
mine janitor: 1 test failure unique to this patch — conf-sanity4@zfs:test_302
1 unique failure XL +1872 −62
4h
janitor: 1 test failure unique to this patch — conf-sanity4@zfs:test_302
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
conf-sanity4@zfs:test_302NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
build #131391 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+7 earlier PS1 08-24 PS2 08-25 PS3 08-26 PS4 08-28 PS5 08-29 PS6 08-29 PS7 08-30
PS8 08-31 PS9 09-01 PS10 09-03 PS11 09-04 PS12 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS12 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20637 llapi: name a device scan's objects

A scan of a target holds names and parent FIDs but no paths, so lfind
prints FIDs and a user has no way to turn them into pathnames.  The
HLD names an Output Format for exactly this, beside the FID printing
that is already there.

Add llapi_scan_rec_path(), which uses the path a namespace walk
already put in the record and resolves the FID through a client mount
when there is none.  lfind gets --fid2path MOUNT for it.

The cost is one ioctl per object, so it is the consumer's to ask for
rather than the scanner's to pay: it falls on the objects a search
kept, not on every object it saw.  That is also why the HLD prices
pathname generation separately from the scan rate.

An object with no pathname is counted and reported at the end rather
than printed: the objects a target keeps for itself have none, their
FIDs being below FID_SEQ_NORMAL, so a search under --internal is
expected to report some.  An OST data object does have a pathname --
its FID resolves through the OSC to the file that owns it, which
sanity test_154C asserts -- so a scan of an OST names that file once
per stripe it has on the target.  A hardlinked object is printed once,
under its first name.  A FID that cannot be looked up at all -- no
CAP_DAC_READ_SEARCH, which ll_fid2path() checks first -- is an error
and not a missing name, so a broken search cannot read as a complete
one.

Neither half of the resolution asks anything of the target being
scanned, which is what lets it name objects on a target that is out of
service -- and a ZFS target can only be scanned out of service, its
pool being exported.

An OST data object is named by the file that owns it, read from the
object's own trusted.fid.  Its own FID is an OST-sequence one, which
__ll_fid2path() answers only by asking the OST holding it, through the
OSC: exactly the target the scan is reading.  ff_parent is the owner's
MDT FID, and the stripe index filter_fid keeps in its f_ver is
dropped,
that field being an index and not a version.  An object no file owns
yet -- precreated and never written, of which a target holds many --
carries no trusted.fid and is reported as having no name rather than
being chased through the OSC.

trusted.fid is not the only place that FID lives.  An OST whose inodes
are 256 bytes or smaller has none at all: osd_xattr_set_pfid() packs
the parent into the LMA as a struct lustre_ost_attrs and removes the
separate xattr, saying so with LMAC_STRIPE_INFO.  Reading only
trusted.fid there leaves every object on the target without an owner,
so --fid2path prints nothing and reports them all unnamed -- measured
on an OST formatted -I 256: 33 objects, no owner on any of them,
against four files whose paths it should have printed.  mkfs.lustre
defaults to -I 512 now, but -I 256 is still accepted and older OSTs
carry it.  The LMA buffer is already in hand for the classification,
so the fallback costs no second xattr, and ll_decode_filter_fid(8)
reads the same two places.

An MDT object is named from its trusted.link and a map of the target's
directories, FID to parent and name, built by a pass over the target
before the search.  A pass of its own because objects arrive in inode
order, so an object is usually delivered before its ancestors are;
resolving inline would mean holding every match until the scan ended.
The pass asks for no layout, no SOM and no HSM, and a filesystem holds
far fewer directories than files.  On an OST it stops at the first
object and leaves the map empty.

There is no fall back to the lookup once the map is in use.  The map
is
used precisely when the target cannot answer, so one object it could
not place would stop the whole search there.  Two things are nameless
in consequence and counted with the rest: an ownerless OST object, and
on DNE an object whose ancestors live on another MDT, the map covering
only the target scanned.

--paths is the same composition with no mount at all.  --fid2path
still needs one, not to resolve anything but as the filesystem the
paths hang from and for the comparison below, and a filesystem whose
only MDT is the target being scanned has none to give -- which for a
single-MDT ZFS filesystem is every scan of its MDT, the pool having
to be exported.  --paths prints filesystem-root-relative names
instead, asks nothing of any running service, and is refused on an
OST, whose objects are named by files that live on an MDT.

A mount whose own filesystem cannot be read is refused rather than
run unguarded: --fid2path was asked for explicitly, and the fallback
was the very outcome the comparison exists to prevent, another
filesystem's pathnames printed with a zero exit.

llapi_scan_rec_path.3 says the same thing to a consumer pairing that
call with llapi_scan_device() directly, because nothing in the
library can make the check for them: the call is handed a descriptor
and a record and never sees the target the record came from.  lfind
compares the mount's fsname with the target's label; anyone else has
to do that too, or know by construction that the two agree.

A MOUNT of another filesystem is refused when the target is opened,
its fsname compared with the one the target's label names.  The
comparison sits inside the scan's own open rather than in a probe
ahead of it: a second open is not free on every backend, and opening
a target twice buys nothing the one open cannot answer.  It cannot be
caught later: FID sequences are not unique across filesystems.
FID_SEQ_ROOT and FID_SEQ_DOT_LUSTRE are constants, so the root of
every filesystem has the same FID, and normal sequences overlap
because each filesystem's controller starts from FID_SEQ_NORMAL
(LUSTRE_SEQ_SPACE_RANGE).  fld_client_lookup() on the wrong mount
therefore finds a range and __ll_fid2path() answers with a pathname
from it, so the run would have printed another filesystem's paths as
though they were the target's and exited 0.

The mount point is resolved before use: a bare fsname, a
subdirectory and a trailing slash all open the same root, and none of
them names a file when prefixed to what fid2path answers.

conf-sanity test_302 stops an OST, exports its pool where the backend
is
ZFS, and compares the names a scan of it gives against what the client
saw before it went down -- a scan that resolved nothing and one that
resolved everything both exit 0, so the set is what is asserted.  Its
files are written rather than merely created, a precreated object
being
rightly nameless.

conf-sanity test_301 diffs the resolved paths against what the client
sees, and checks that a hardlinked object is named once.  It mounts a
client on the MDS for the duration, because --fid2path resolves
through a mount on the node running the scan and a server is not a
client; a configuration that cannot mount one skips.  It is ldiskfs
only: reading a target in service is what ZFS refuses.

scan_device_run()'s comment says what a second open costs rather than
calling it a deadlock, because it is not one: llapi_find_device()
builds the directory map in a pass of its own, so every --fid2path and
--paths
run already opens and closes the target twice in one process, and
conf-sanity test_302 does exactly that against a stopped OST with its
pool exported and passes on conf-sanity4@zfs.  The reason to test the
fsname inside the scan's own open is the cost of the second import and
export, not a hang.  The wasted pass on an OST -- where
scan_dirmap_cb() stops at the first object -- is left as it is;
folding the map into the search's own open would serve both from one
open and is worth doing separately.

The resolve-failure message names the FID that was looked up and the
mount only when there is one.  An OST object is resolved by its
owner's FID rather than its own, so the message named the wrong one;
and under --paths there is no mount at all, yet -ERANGE and -ELOOP
still arrive here and reported a failure "through the mount given".

lfind(8) says what --fid2path costs, which differs by target and is
easy to state as one number and be wrong. One lookup per kept object
is the OST's cost; on an MDT there is no lookup and a whole extra pass
instead, to build the map the page describes four blocks further down
-- the more expensive of the two, and paid before the search rather
than per match.  Where a name cannot be composed the page points at
lfs-fid2path(1), which is a tool that exists, rather than at a
lookup-only mode of this one, which is not.

--paths and --fid2path together goes through the same exit as every
other error in that function.  Returning CMD_HELP from main() exits 4
and prints no usage, where "ret = CMD_HELP; goto out" prints it and
exits EXIT_FAILURE.

fp_paths takes fp_unused_bit1.  struct find_param keeps those bits for
exactly this, and a trailing byte would have cost 8 with padding.

The directory map's names stay inline.  At NAME_MAX + 1 an entry is
~288 bytes, so a million directories is ~288 MB and 1.5x that while
scan_dirmap_grow() holds both tables, and interning them in an arena
would cut it several-fold.  Left for now: it is a change to the map's
memory layout for a case no measurement here has reached, and a
calloc() failure already fails the search cleanly rather than
corrupting it.  Worth revisiting when --paths is pointed at a target
big enough to need it.

conf-sanity test_303 covers --paths, where 301 and
302 both exercise only --fid2path, so the composition --paths exists
for was never run.  It scans an MDT in service with no mount given,
which is the case a filesystem whose only MDT is the target has to be
named in, and asserts the answer is the client's set: one path per
object rather than one per name, the hardlinked object once, no FID
where a path was asked for, and no mount point in the answer -- root-
relative being what separates it from --fid2path, and a mounted prefix
meaning the wrong composer ran.  It also asserts the two refusals:
--paths on an OST, whose objects are named by files that live on an
MDT, and --paths together with --fid2path.  ldiskfs only, an imported
ZFS pool answering EBUSY by design.

lfsp_fsname makes the check a public caller can ask for.
llapi_scan_rec_path.3 tells one they must establish that the mount and
the target are the same filesystem, and nothing in
<lustre/lustreapi.h> let them: scan_device_run() is internal,
llapi_scan_device() passed want_fsname NULL, and the target's label
lives in a private header.  The field is the same machinery under a
public name -- the scan refuses a target whose label names another
filesystem with -EXDEV -- and it has to be the scan that does it,
because only the scan reads the label and a lookup on the wrong mount
succeeds and answers with that filesystem's pathname.

The usage agrees with its own option list.  "The output is a
FID because a target has no paths" sat two lines below --paths and
--fid2path, both of which print pathnames; it now says the output is a
FID unless one of them is given.

test_301 and test_303 sync the MDS, not only the client.
lustre_super_operations has no .sync_fs, so a client sync writes back
page-cache data and leaves the MDT's ldiskfs to commit on its own
schedule -- and both tests scan that ldiskfs directly while it is in
service, so an object still only in memory is one the scan cannot see
and the comparison against the client's answer is a race.  test_302
needs none of this: its target is stopped, which flushes it.

test_302 also tells the scan where the OST's pool devices are.  An
exported pool is found by reading vdev labels under a search path,
which defaults to /dev, and the test framework's ZFS vdevs are files
in $TMP -- so without --search the pool is not found at all and the
test failed on every ZFS configuration with ENOENT.  On ldiskfs the
argument is empty and nothing changes.

test_301's one bare lfs becomes $LFS, as every other call in the file
is.  An interop run that points LFS at another binary would have used
whatever PATH found.

The EXAMPLES fragment in llapi_scan_rec_path.3 escapes its newline as
n, not n: troff reads the single form as a register reference,
which rendered the line as printf("%s0, path).  llapi_fid_parse.3
escapes it the same way.

The three places that said a target scan needs no client now say what
--fid2path changes.  lfind.c's file header and llapi_find_device()'s
block comment both stated it flatly, and llapi_find_device.3
documented neither fp_fid2path_mnt nor the -ENAMETOOLONG an over-long
mount path answers with; its SEE ALSO gains llapi_scan_rec_path(3),
which is the call that does the resolving.

llapi_scan_rec_path.3's NOTES point a consumer at lfsp_fsname rather
than telling it to compare the mount and the target itself.
lfsp_fsname is that comparison -- the scan refuses a target of another
filesystem with -EXDEV -- and saying otherwise on the page that
introduces it would send a caller to write what the library already
does.

test_303's one bare lfs becomes $LFS, as test_301's did.  It was added
after that fix and reintroduced it.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6476abfd28c91b2ccc04fb08c4e5860aef8fe009
68415 master J ✗2B ✓T …R 0/2−
LU-20649 llapi: a changelog as an Object Stream
mine janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
2 unique failures XL +2327 −3
4h
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity2@ldiskfs+DNE:test_157dseen in 1 other review
sanity2@zfs:test_157dNEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
build #131390 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20649 llapi: a changelog as an Object Stream

The two scanners we have enumerate what exists.  A changelog
enumerates what happened, and llapi_scan_changelog() delivers it in
the same record, so a consumer written against llapi_scan_namespace()
or llapi_scan_device() can be fed by a delta without being rewritten.

A changelog record carries the FID, the parent FID, the name, the
event and its time, and -- where the server recorded them -- the uid
and gid of whoever caused it.  That pair is lfsr_event_uid and
lfsr_event_gid, not lfsr_uid: mdd_changelog_ns_store() fills the
uidgid extension from lu_ucred(env), the acting client's credentials,
so it says who acted and not who owns the object, which is what
lfsr_uid means in every scanner that fills it and what lfs find -uid
matches. Delivering it as lfsr_uid would have answered "-uid alice"
from the identity of whoever last touched her file.  The record
carries no size, mode, owner, times, layout or HSM state at any
setting, so those arrive absent with their lfsr_valid bit clear.  With
LLAPI_SCAN_CL_F_RESOLVE they are filled by opening the object through
a client mount, which is one open and one stat each, or two of each
for a regular file whose size was asked for; the demand mask decides
whether that happens at all, so a consumer reading only what an event
carries pays nothing.  Resolution fills what the stream lacks and
never restates what it has, which is what lets a caller ask what an
event recorded rather than what is true now.

Two modes.  Event mode delivers one record per event, in order.
Object mode holds events in a FID-keyed cache and delivers one record
per object once it has been quiet, so a file written a hundred times
is one record and one lookup rather than a hundred of each.

Clearing is off by default and never runs ahead of the callback: it is
irreversible and it purges the registered user's whole backlog, not
this reader's alone.  Registering that user stays the caller's, since
registering without consuming is how an MDT fills up.

The record gains lfsr_event_type, _flags, _time, _index and _prev, the
rename source, and a job id, behind three new validity bits.  An event
time is not an object time, so it does not land in lfsr_mtime: a
search asking for one must not be handed the other.

Only an event that implies a type answers for it -- the three
creations, and CL_RMDIR, whose target is a directory by definition --
so the mode and its validity bit are set together, and a later event
leaves both alone.  Setting them as a pair is what keeps "cannot
answer" apart from "the answer is zero": a mode written on every
event, with the bit only ever OR'd in, would leave a coalesced object
claiming a type while carrying zero for it.  An object's type does not
change, so the first answer stands for every event after it.

lfsr_parent_fd and lfsr_fd are set to -1 rather than left as the
memset found them.  Zero is stdin, and a consumer written against
llapi_scan_namespace() is told to test lfsr_parent_fd and then
fstatat() on it.

The Changelog reader is finished on every path out, the error one
included.  llapi_changelog_start_user() opens the device and sets
*priv before it issues OBD_IOC_CHANGELOG_FILTER, so a failure there --
an unregistered user, an unreachable MDT -- hands back an error with
the reader live, and the fd, its buffer and the kernel-side reader
state would stay pinned for the life of the process if nothing closed
them.

sc_user, sc_mnt and sc_stats are read from the bounded copy rather
than the caller's structure, and the advertised minimum sc_size
reaches through sc_mdtname.  A minimum stopping short of it would let
a caller declare a structure without the field the call cannot work
without, and reading that field off the caller's own memory would go
past its end before the size check had a chance to refuse it.

Clearing stops at the oldest record any cached object still needs, not
at the last index delivered.  Coalescing puts those apart: create B at
10, create A at 11, touch B at 1000, and A can age out and be accepted
while B is still held with a record at 10.  Clearing to 11 there and
then stopping the scan loses B entirely -- no callback saw it, and a
restart from sc_startrec cannot read it back, because the record is
gone.

An event with no target object is delivered per event even in object
mode.  mdd_changelog_ns_store() zeroes cr_tfid when its target is
NULL, which mdd_rename() passes whenever the destination name did not
already exist, and cr_markerflags -- a CL_MARK's union with the same
bytes -- is not a FID either.  Coalescing keys on the FID, so all of
them shared one cache entry and only the last survived: an ordinary
`mv a b` was enough to lose the one before it, and with clearing on,
the record it came from is gone as well.

The parent FID is taken only from an event that carries one.  CL_CLOSE
does not -- mdd_changelog_data_store() is called with pfid NULL -- so
assigning it from every event while only ever OR-ing the bit in left
create+write+close reporting LLAPI_SCAN_PARENT over a zero.  A name or
job id whose copy failed leaves its bit clear for the same reason: a
bit that promises a string must not be set over a NULL.

A resolve is skipped only for a demand mask every event can answer,
which LLAPI_SCAN_TYPE is not: scan_cl_mode() answers it for the four
record types that imply one and no others, so counting it there would
leave a consumer asking for FID and type with neither a lookup nor a
type on a CL_CLOSE.

Resolution opens O_PATH and takes a real open only for the regular
file whose size it came for.  llite gives a special file
init_special_inode(), so opening every object for read opened the
driver behind a device node stored in the filesystem; O_NONBLOCK holds
off the wait, not the rest of what an open does.

The uidgid extension is asked for when sc_want is 0, and that case is
tested on its own rather than by a mask.  Zero means "everything", so
it is the caller that most needs the extension -- and `0 &
~LLAPI_SCAN_EVENT` is 0, so any test of that shape would exclude
precisely the caller that wanted it, leaving LLAPI_SCAN_EVENT_UID out
of lfsr_valid.

A size is reported strict only where the OSTs were actually glimpsed.
The second open runs for a regular file whose demand mask names a
size and can still fail, so a consumer asking for an mtime alone
reaches the fill on the O_PATH descriptor; that is the MDT's lazy
answer, and LLAPI_SCAN_LAZY_SIZE and LLAPI_SCAN_LAZY_BLOCKS are what
say so.  liblustreapi_pfind.c maps the two to OBD_MD_FLSIZE and
OBD_MD_FLLAZYSIZE, so they are not interchangeable.

LLAPI_SCAN_CL_F_RESOLVE is one open and one fstat, so the page says
which six of the ten fields it fills.  The project id, the layout,
the directory stripe and the HSM state each cost an ioctl or an xattr
of their own and are not filled at any setting; the page said
otherwise by listing all ten and then naming the flag.

The checkpoint gets its caveat, in the header and the page both.
Under coalescing a record naming no object goes out as it arrives
while an object whose earliest record sits lower is still held, so
the delivered indexes are not a watermark.  A run that reaches the
end of the log flushes what it holds and the highest index is safe;
a run the consumer stopped drops it, so that run must leave the
checkpoint alone.

sc_startrec and sc_endrec are both inclusive and the page now says
so, the example resuming from the saved index plus one rather than
re-delivering the record the job already processed.  lfsr_event_flags
is cr_flags & CLF_FLAGMASK, so CLF_RENAME, CLF_JOBID and
CLF_EXTRA_FLAGS are masked off -- they say which extensions a record
carries, not what happened.

Three smaller things.  The second fstat() is taken only when the
glimpse replaced the descriptor.  scan_cl_bucket() drops a modulo
that cannot change llapi_fid_hash()'s value, that being hash_long()
on the same shift.  The sc_mdtname test after the memcpy goes: the
minimum sc_size reaches through that field and the caller's own
struct was tested at the top, so it could never fire.  Four comments
described an earlier revision of this patch rather than the code and
now describe the code.

The COALESCE flag says which event's fields the one record carries. It
promised one record per object and left open which of the object's
events that record spoke for, so a consumer matching on lfsr_name had
no way to know it sees one name per object rather than every name the
log recorded -- an object created under one name and hardlinked under
another arrives carrying only the last of them.  lfs find --changelog
-name is that consumer, and the gap showed there first.

sc_type_mask without sc_user is refused.  The mask reaches the server
only through llapi_changelog_start_user(), which needs a registered
user to filter for; on the other branch llapi_changelog_start() never
sees it, so a caller asking for CL_UNLINK alone read every event type
and nothing said so.  Refused rather than applied client-side, because
that is a different thing -- the records still cross the wire -- and
sc_filter is already the client-side spelling.  The man page said only
that the mask is intersected with the user's own, which does not tell
a caller what happens when there is no user.

llapi_scan_changelog_test covers the five things the shape of this
stream turns on: that a record describes the event it came from, that
object mode collapses a burst into one record, that the demand mask
keeps a lookup from happening at all, that a consumer's own return
value comes back unchanged, and that bad parameters are refused rather
than crashed on.  It takes -m for the MDT, -u for a registered user
and -d for a client mount, since three of those need a live log.
Nothing in lustre/tests drives it yet, unlike its two siblings, which
land their suite case with the scanner: that case wants a changelog
registered and cleared around it and a skip where the MDT records
nothing, and is left for its own patch rather than grown here.

sanity 157d runs it, so the binary is not merely built.  Its events
have to be on the MDT whose log it reads, which under DNE a mkdir
below a striped root does not guarantee, so the test makes the
directory on MDT0 itself rather than leaving the binary to notice and
refuse.  -u passes the registered Changelog user the clearing cases
need; without one they skip themselves.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12efd3bd2150592a4507533129ab56573ef924d9
68416 master J !B ✗T –R 0/2
LU-20650 llapi: fill a scan record for one FID
mine build failure (#131392)
L +781 −10
4h
janitor run: build failed — no tests ran — janitor results
build #131392 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 llapi: fill a scan record for one FID

llapi_scan_namespace() walks what exists, llapi_scan_device() reads
what exists on a target, and llapi_scan_changelog() reads what
happened.  A consumer holding a FID had no way in, so the changelog
module resolved privately and with fstat() alone: mode, nlink, uid,
gid, three times, size and blocks, and nothing else.  A record filled
that way answers -size and -mtime but not -stripe-count, --hsm-state
or -projid, so "lfs find --since -stripe-count 4" would answer
differently from a plain find on the same filesystem.  That is a
defect rather than a difference of provenance: --since promises what
is true now, verified through the mount.  This patch adds the call;
the search moves onto it in "lfs: find --changelog, the log as
source", which is where that example starts answering.

The default demand mask is the walk's, so --hsm-state and -projid
still want naming in lfsp_want; what the call fixes without being
asked is the layout, which is what the example above turns on.

llapi_scan_fid() is the fourth way into the record and fills it the
way the walk does, because it is the walk's own gather.  It resolves
the FID to a pathname and gathers there rather than gathering from a
descriptor opened by FID: scan_rec_gather() is path-oriented
throughout -- get_lmd_info_fd() wants a parent descriptor and a name,
get_projid() opens by path, and the HSM and MDT-index branches open
the path themselves -- so the alternative is an fd-shaped twin of
every one of those, which is the second copy of the knowledge
LU-20611 existed to remove.  Resolution is not an added cost on the
caller that wants this: --since prints pathnames and prefix-matches
them for its subtree restriction, so it needs the pathname anyway.

What that costs is stated rather than buried: per object one fid2path,
one type-only statx and one open of the object or its parent, and
CAP_DAC_READ_SEARCH unless llite.*.user_fid2path is set.  The statx
asks for STATX_TYPE alone deliberately: anything touching size, blocks
or mtime keeps need_glimpse set in ll_getattr_dentry() and buys an OST
glimpse per regular file that the gather then discards.
An object unlinked since the caller learned of it no longer resolves
and comes back -ENOENT, which for a consumer asking what is true now
is the answer and not an error.

The call fills a fresh record and never reads one in.  A consumer
merging it into a record it already holds decides which fields it
lets this one supply, which is what will keep --changelog --resolve
filling only what the stream lacks.

A filter that returns a positive value skips the object, as it does
for the other three scanners, and the call returns 0 rather than
handing that value back to a caller testing `if (rc)`.  The gather
runs against a pathname, so the FID it found is checked against the
one asked for and a name unlinked and recreated underneath it answers
-ESTALE rather than a record for a different object.

Only lfsp_want and lfsp_filter are read from the parameter block.
lfsp_max_depth, lfsp_stats, lfsp_search and lfsp_fsname describe a
traversal there is none of and are ignored; lfsp_thread_count above 1
is refused rather than ignored, because a caller setting it has
misunderstood the call.

It widens a lfsp_want of 0 through LLAPI_SCAN_WANT_DEFAULT, the
definition llapi_scan_namespace() already uses, so the two mount-side
entry points mean the same thing by 0.

A trailing slash on mnt_path is trimmed before the pathname is
composed.  The leading slashes of fid2path's answer were already
stripped, but "/mnt/lustre/" with the root FID composed
"/mnt/lustre/" and left lfsr_name on the terminating NUL -- which
lustreapi.h says means NULL, not empty -- and every other FID got a
doubled separator in the published lfsr_path.

The kernel-doc no longer lists lfsp_flags among the fields that are
ignored.  Every flag it defines is, but a bit outside them is refused
with -EINVAL, as the code does and llapi_scan_fid.3 already says.

llapi_find_device() clears lfsp_got.  scan_param_copyin() copies the
caller's block whole, so the caller's pointer came through and was
answered with find's own demand mask -- a mask for a question the
caller never asked.  The field's kernel-doc is rewritten with it, to
say that it is per scan rather than per library and why it is reached
through a pointer.

The syscalls go through mnt_fd, not through the composed pathname.
The FID was already resolved through that descriptor; the statx, the
object's open and the parent's now use it too, at the name fid2path
answered, so mnt_fd alone says which filesystem is read.  An fd pins
its mount where a path string is resolved afresh every time, and a
mount replaced between the open and the scan would otherwise have the
resolve read one filesystem and the gather another, with rc 0 and
nothing said.  It also takes the mount prefix off every lookup, three
per object, which over a changelog's worth of FIDs is what this entry
point is for.

mnt_fd has to be the mount root now.  The ioctl behind
llapi_fid2path_at() takes any descriptor in the filesystem, and the
page always named llapi_root_path_open(), which returns the root; a
caller that passed something else answered before and answers -ENOENT
now.  mnt_path stays required and stays what lfsr_path is built from,
so a wrong one still mislabels the answer -- it just no longer decides
what was read.

llapi_scan_test gains a case for it, so sanity 157c runs it: one
record for a known FID, checked against the pathname and the basename
the client sees; a filter that skips, which is a success with nothing
delivered; the argument refusals, including the -EBADF that a
negative mnt_fd has to answer rather than the -EINVAL that also means
"this FID has no name"; and a FID that resolves to nothing, which
must fail and deliver no record whichever way it fails.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I796db84d3fe596b1cfa93658dcfeb240db8e33a5
68156 master J ✓B ✓T …R 0/2
LU-20606 llapi: scan an ldiskfs target directly
mine 1 unresolved thread(s) await your reply — last from Andreas Dilger
10 unresolved XL +3136 −34
4h
1 unresolved thread(s) await your reply — last from Andreas Dilger
build OK — testing in progress (typically 6–14h)
9 thread(s) waiting on others
build #131384 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-23 PS7 08-24 PS8 08-25 PS9 08-26 PS10 08-28 PS11 08-29 PS12 08-29 PS13 08-30
PS14 08-31 PS15 09-01 PS16 09-03 PS17 09-04 PS18 09-09
threads: 1 need your reply · 9 waiting on others · 0 own notes · 0 bot
reply PS9 2026-09-05 08:53 Andreas Dilger COMMIT_MSG:22 — API changes are not critical within the development releases.
Gerrit AI review for Lustre PS9 · 2026-08-27 20:07
(minor) This paragraph lists what the record grew, but two other pieces of new public API aren't accounted for anywhere in the body: `struct llapi_scan_stats` with the `sp_stats` parameter field, and the `LLAPI_SCAN_F_INTERNAL` flag.  Both are things a caller has to know about, and the counters in particular are described in the man page as part of the answer rather than a diagnostic.  Worth a sentence each so the diff holds no surprises.
Hiroshi Nishida PS17 · 2026-09-04 22:04
Done. Held back through several rounds as prose-only, but you are right that new public API is not prose. The message now names `struct llapi_scan_stats` with `sp_stats` and says what LLAPI_SCAN_F_INTERNAL is for: the objects a target holds that its namespace never shows -- the OSD's own, the DNE agent inodes, the ones with no LMA -- which a scan otherwise counts in ss_class and keeps back. Checked against `enum llapi_scan_class` and the gate in liblustreapi_scan_device.c rather than written from the flag's name.
Andreas Dilger PS9 · 2026-09-05 08:53
API changes are not critical within the development releases.
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20606 llapi: scan an ldiskfs target directly

Add llapi_scan_device(), which reads an MDT's or OST's objects
straight off the device with libext2fs and delivers the same records
llapi_scan_namespace() does.  No mount, no MDS, no kernel change: it
runs against a stopped target, a snapshot or a failover partner's LUN,
read-only and with no write path.

libext2fs stays off liblustreapi.  The backend builds as
scan_osd_ldiskfs.so and is dlopen'ed on first use, named and found the
way mount.lustre's own mount_osd_ldiskfs.so is, so a client build has
no plugin and the call answers ENOTSUP.  Three refusals get three
answers, because a caller keys off them: ENOTSUP where there is no
backend to ask, ENOPKG where the backend is there and refused the
target -- stock e2fsprogs will not open a filesystem with dirdata set,
and the package to install is the answer -- and EINVAL for a device
that is not a Lustre target at all.  Giving the first two the same
errno would have conf-sanity test_300 skip itself on a node carrying a
real MDT and a build that can scan it.

The record grows what a device answers for and a walk does not: the
object id, the inode generation, the LMA flags, the raw linkea and a
class.  The label is read the four ways mkfs.lustre writes it -- '-',
':', '=' and the '+' that tunefs.lustre --nolocallogs leaves on disk
while the target is stopped, which is exactly when this is what there
is to read it with.

Two more pieces of public API, so the diff holds no surprises. struct
llapi_scan_stats is what the new lfsp_stats points at: the caller sets
ss_size and owns the storage, the scan fills the rest on return, and
the man page describes it as part of the answer rather than as a
diagnostic.  And LLAPI_SCAN_F_INTERNAL asks for the objects a target
holds that its namespace never shows -- the OSD's own, the DNE agent
inodes, the ones with no LMA -- which a scan otherwise counts in
ss_class and keeps back.  Without it "every object on this device" and
"every object a walk would find" would be the same question, and only
one of them is what a target scan is for.

The counters are one accounting rather than several: ss_seen is
incremented once per object, in the pre-filter, so a skip the backend
raises after that point adds to ss_skipped alone.  ss_seen equals
ss_filtered plus ss_skipped plus the sum of ss_class on any completed
scan, and llapi_scan_device_test asserts it -- an inequality would not
have caught counting the same object on both sides of the pre-filter.

An object with no LMA is answered with its IGIF rather than with the
parts to build one from.  The inode number and generation of such an
object are its FID -- osd_scrub.c builds the same one, and it is not a
reconstruction, which is LFSCK's job -- so lfsr_fid carries it and
LLAPI_SCAN_FID is set.  That leaves the generation with no consumer of
its own and it is not a field: what remains is lfsr_ino, which every
object has and which names the ones no IGIF can, a ZFS object id being
wider than an IGIF holds.  There is no LLAPI_SCAN_GEN bit for it.

llapi_scan_namespace.3 gains the pointer to this page here rather than
in the patch that wrote it, where it would have referred to a page the
tree did not yet have.

A worker that cannot be initialised degrades the scan rather than
failing it, once one worker exists, as the pthread_create() arm beside
it does: a backend opens the device again per worker,
lfsp_thread_count is a __u8, and the cursor feeds whatever workers
there are, so running out of them is not a reason to turn a complete
result into an error.

The page says what stx_attributes carries across scanners.  A device
scan narrows the inode's own flags to the set llite also declares, so
one file answers "lfs find -attrs d" the same way whichever scanner
ran, and so_attrs_mask declares that set in the record's
stx_attributes_mask -- which is what lets a consumer tell "not
immutable" from "this scanner cannot say".  ERRORS also spells out the
two EINVAL cases a caller is most likely to trip, a short lfsp_size
and a short ss_size.

A directory's own stripe takes LMV_MAGIC_V1.  LMV_USER_MAGIC is how
the tree marks a *default* LMV: cb_get_dirstripe() sets it exactly
when fp_get_default_lmv is asked for and LMV_MAGIC_V1 otherwise, and a
default lives in trusted.dmv, which is never read here -- trusted.lmv
is the directory's actual stripe.  llite fills LMV_MAGIC_V1 for
LL_IOC_LMV_GETSTRIPE, so LMV_USER_MAGIC here would have the two
scanners answer one striped directory with two different values in the
same field -- the opposite of what lfsr_lmv promises, which is that
the field means one thing whichever scanner filled it.

No in-tree consumer reaches that difference from a scan record yet, so
this is a contract broken rather than a wrong answer observed.
lmv_dump_user_lmm() is what it would break: the magic decides its
"(Default)" prefix and which fields a bare -v shows, and
llapi_lov_dump_user_lmm() accepts both values, so a caller pointed at
a scan record instead of an ioctl reply would print a device scan's
directories as defaults.  The size is unchanged either way,
lmv_user_md_size() growing only for LMV_USER_MAGIC_SPECIFIC.

A parent FID the scan will not vouch for is left cleared rather than
written: LLAPI_SCAN_PARENT is set only where fid_is_sane() accepts
what trusted.link held, and the field follows the bit, so a caller
reading it without testing lfsr_valid does not get the raw xattr.

llapi_scan_device.3 gives lfsr_parent_fid an entry of its own, since
LLAPI_SCAN_PARENT is both what a caller puts in lfsp_want and what it
has to test in lfsr_valid, and a field named in neither place is a
field a caller cannot use.

run_tests() in lustre/tests/llapi_test_utils.{c,h} is split so that
run_test_tbl() takes the table: the new test is handed a device where
the others take a mountpoint.  llapi_scan_test, llapi_root_test and
llapi_pool_test share that helper, which is why the split is worth
naming here rather than left to be read out of the diff.

so_flags is narrowed to the attributes a namespace scan can also
report.  ll_dir_ioctl() puts only IMMUTABLE, APPEND and, under
HAVE_LUSTRE_CRYPTO, ENCRYPTED into stx_attributes_mask, and
llapi_scan_namespace() takes stx_attributes from that mask -- so
reporting COMPR and NODUMP here would make one file answer "lfs find
--attrs d" differently depending on which scanner ran, where
stx_attributes is meant to mean one thing whichever filled it.  The
bit is on the MDT inode either way: making it answerable is llite's to
do, for both scanners at once.

so_attrs_mask carries that same set into the record's
stx_attributes_mask, rather than the scanner assuming one set for
every backend: a bit clear there is "cannot say", which is a different
answer from the attribute itself being clear.  It is carved from
so_padding, so every offset above it is unchanged, and a backend that
leaves it zero has its records leave LLAPI_SCAN_ATTRS clear rather
than claim attributes nothing vouches for.

scan_lmv_to_user() clears the shard area, not just the header.  One
buffer serves every object a worker meets, and a directory's converted
LMV carries the real lum_stripe_count while the size answered is
header-only -- the shard FIDs being left out on purpose, an MDT index
needing an FLD lookup this has no client to make.  A consumer sizing
lum_objects[] by the count rather than by lfsr_lmvsize, which
lmv_dump_user_lmm() does, then read whatever the last object left
there: a preceding foreign directory's opaque value, in full.  The
clear now reaches as far as such a read can, bounded by the room
before a count off a device measures anything, so what it finds is
zeroes.  lfsr_lmvsize is the measure either way, and the header says
so.

The buffer is a union of the two structures it holds rather than a
bare char[], which carries neither one's alignment while the call site
casts to both.

An unreadable inode is counted by the chunk that owns it and no
other.  The skip arm continued without asking either question the
readable path asks below it, so an inode just past end_ino was
consumed by this chunk and read again by the chunk starting there,
and ss_seen and ss_skipped each took it twice; a reserved inode that
could not be read was counted as an object, where a readable one is
not.  ext2fs_get_next_inode_full() assigns *ino before returning any
of the three errors this arm catches -- unlike the errors above it,
which leave it untouched -- so both questions can be asked here.
Nothing was ever delivered twice: it was the accounting that
overstated, which is what the counters exist to be trusted for.

fid_is_root() moves to the UAPI header and the scan calls it.  It was
in lustre_fid.h by history rather than by need: LU_ROOT_FID and
lu_fid_eq() are both already public, so the helper is a one-line
predicate over two things userspace has, and userspace now has the
same question to ask -- a scan of a target has to tell the root from
the rest of FID_SEQ_ROOT.  unlikely() is dropped, that header being
compiled in userspace too, and the definition is removed from
lustre_fid.h, which includes the UAPI one, so every existing caller in
llite, lmv, lod, mdd and mdt is unaffected.

That also settles a test that had drifted.  lu_fid_eq() is a
whole-struct compare, so a hand-written test of f_seq and f_oid called
a FID in FID_SEQ_ROOT with FID_OID_ROOT and a non-zero f_ver the root,
where fid_is_namespace_visible() on the MDT would not.  The narrower
point the spelling exists for is unchanged: the echo client's root
shares the sequence and is still not visible.

Both scanners fill lfsp_stats.  The structure arrives with this patch,
which is why the walk did not fill it before -- the field came in with
the code that needed it, not with the walk -- and there is no reason
for it to stay that way: a walk counts an object seen once, then
filtered, skipped, or delivered and namespace-visible, which is every
class a walk can meet, and the same seen == filtered + skipped +
sum(class) holds.  It counts atomically rather than per-worker,
llapi_scan_namespace() running its callback on every thread at once
where the device scan merges per-worker counters at the end.
lfsp_stats is checked against LLAPI_SCAN_STATS_MIN_SIZE there too, and
after the copy-in for the same reason: a short lfsp_size may not reach
the field.

The struct comment says what the field is rather than which scanners
use it.  llapi_scan_device.3 also says the stats structure is
extensible -- ss_size negotiates it exactly as lfsp_size does, so
counters appended later leave an older caller reading its own fields
-- which was true and written down nowhere.

The struct comments describe the fields rather than which scanner uses
them.  What each scanner offers lfsp_filter, and what each takes 0 in
lfsp_want to mean, were spelled out in the header and again on both
pages, and the header's copy is the one that goes stale: it claimed a
device scan "offers no path and no name", which LU-20637 makes untrue
two patches later by naming an OST object from the parent FID in
trusted.fid.  What is left is the part that does not change -- the
filter sees only what the scan already had, and 0 means a default the
two scanners do not share -- with each page saying which fields that
is for itself.  llapi_scan_device.3 gains the one sentence it was
missing, what 0 means there.

stack_trap is called without EXIT, which is its default, and the
setupall after stopall carries no justification -- it is the ordinary
shape and needs none.  The base tree passes an explicit EXIT eighteen
times of ninety-two; this series had added thirteen more, so dropping
ours leaves the file at the eighteen that were already there.

The EXAMPLES fragment escapes its newline with a single backslash.
troff reads that as a register reference, so the page rendered
printf("%llu files; %llu seen, %llu skipped0, with the escape and the
closing quote swallowed.  It needs the doubled form that
llapi_fid_parse.3 and llapi_scan_rec_path.3 already use.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I8a28a8173b0b8f03ba8f9dc5a58a821118ac6296
68417 master J !B ✗T –R 0/2
LU-20650 lfs: find --since, from the changelog
mine build failure (#131393)
1 unresolved XL +995 −8
4h
build failure (#131393)
1 thread(s) waiting on others
janitor run: build failed — no tests ran — janitor results
build #131393 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --since, from the changelog

The three pieces were already here.  llapi_scan_changelog() says which
objects had an event, llapi_scan_fid() fills each one's record the way
a walk fills it, and find_decide() judges it with the predicates a
walk uses.  --since composes them, so it answers over fewer objects
and judges each the same way: the objects are a strict subset,
narrowed and never different.  The lines are not a subset of the
lines -- a walk reaches a hardlinked file once per directory entry
and prints it once per link, while --since reaches it once per FID
and prints it once, under the first of its names that is under the
root and matches.  No new predicates and no second copy of the
vocabulary.

The argument is never guessed.  A bare integer is an index, which is
what "lfs changelog MDT <startrec>" already takes; a single-unit
duration (30m, 2h, 7d) is that far back from now; a timestamp or
@<seconds> is that time; anything else is an error naming the four
spellings.  A leading sign is refused rather than wrapped through
strtoull(), and a duration reaching past the epoch is a typo rather
than a window.

cr_index belongs to one MDT, so a bare index is refused on a
filesystem with more than one and the message says which time
spellings do work.  That count has to be right, and the obvious source
is not: LL_IOC_GETOBDCOUNT answers with the size of the LMV's target
array, so a single-MDT filesystem reports 64.  It is used as an upper
bound and each index confirmed with llapi_search_tgt() before its
changelog is read.

A changelog covers the whole filesystem, so the subtree the caller
named is enforced against the pathname llapi_scan_fid() resolved, and
the depth the walk would have counted is recovered from it for
--maxdepth and --mindepth.  Without that, a find rooted at one
directory would answer for its siblings.

Against every name the object has, not just the resolved one.
llapi_scan_fid() resolves linkno 0, so a hardlinked file arrives under
whichever name fid2path answered with first: with /a/f and /b/f the
same inode, "lfs find /a --since 2h" would be handed b/f, reject it,
and lose a file a walk of /a prints.  -name has the same shape, which
is why a target scan walks every linkea name.  The names come from
fid2path here, one ioctl each, and only a file whose link count says
it has another name pays for any of them.

A time anchor is cut on the record rather than at the start index,
because a time cannot be turned into a per-MDT index without reading
the log; coalescing has already reduced an object to its latest event,
so an object whose latest event is older than the window did not
change in it.  An object unlinked between its event and now cannot be
verified, so it is not in the answer -- that is what the flag gives up
-- and the run says how many were dropped that way.

--since is refused for a target scan rather than ignored.  The option
table is compiled into lfind as well, so `lfind --local --since 2h`
parsed and then scanned the whole target: llapi_find_device() has no
changelog to anchor on and never reads fp_since_kind.  It joins the
list find_device_supported() already keeps for --maxdepth, --threads
and -printf.

llapi_find_since() refuses fp_thread_count above 1 for itself, before
it looks at anything else: this source reads one MDT's changelog at a
time and has no work to divide.  That is why lfs_find() now computes
the default thread count on the walk's arm of the dispatch rather than
above it -- computed for every search, the default alone was above 1
and the refusal fired on every --since run, which is not a setting the
caller made.

The mount point is found from the resolved root and not from the
spelling, which are the same string only when the caller already gave
a canonical absolute path.  /home/user/data -> /mnt/lustre/data
answered -ENODEV, llapi_search_mounts() matching mnt_dir literally,
and a relative path went through the fsname to the *first* mount of
that filesystem -- so under $MOUNT2 every object fell outside the
subtree and the search printed nothing and exited 0.

What is printed is then the caller's spelling of that root with the
resolved remainder appended, because a walk prints the path as it was
given -- param_callback() copies the argument and
llapi_semantic_traverse() appends to it.  Named from the mount every
time, "lfs find . --since 2h" would answer /mnt/lustre/sub/f where
"lfs find ." answers ./sub/f, and --since promises the same answer as
the search without it.

ONLY=56 is the regression check that the shared paths still answer as
they did; the subtests for the new options arrive with the tests
patch at the end of this series.

Two things the page is careful about.  An MDT with no registered user
does not record at all, rather than recording and purging:
mdd_changelog_off() clears CLM_ON when the last user deregisters and
mdd_changelog_enabled() requires it.  And the usage string spells the
argument TIME|INDEX rather than TIME, because a bare integer is a
changelog index -- the one distinction the parser goes out of its way
to make, and the one a reader is most likely to get wrong.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I13426ef1ed12cd08d78aa5a5da9f8ba4465690d2
68418 master J !B ✗T –R 0/2
LU-20650 lfs: find --changelog, the log as source
mine build failure (#131395)
XL +952 −79
5h
janitor run: build failed — no tests ran — janitor results
build #131395 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --changelog, the log as source

--since treats the changelog as a candidate set and verifies each hit
against the live object, so it answers as it is now.  --changelog
replaces the source: it reads the log and nothing else, and answers as
it was recorded.  That is why -name here matches the name in the event
rather than the object's current one, and why an object unlinked since
the event is in this answer and cannot be in --since's.

It takes an MDT name or "all", required rather than optional because
getopt_long() accepts an optional argument only as --changelog=VALUE.
One named MDT is also what makes a bare --since index unambiguous, the
index being paired with the log it came from.

Every predicate a record cannot answer is refused naming itself,
rather than answered from a zero.  Without --resolve that is -size,
-blocks, -perm, -links, --attrs, --xattr, the layout options, -projid,
-printf, --mdt-count and --mdt-hash; --xattr was the loud one,
reaching llistxattr(NULL, ...) once per object, and the rest were
quiet and worse, "--perm 644" matching nothing and its negation
everything.  -uid and -gid are refused for a different reason and at
every setting short of --resolve: a changelog records who caused an
event, from the acting client's credentials, which is not who owns the
object.  An object time is refused outright, a record carrying an
event time and the two being different questions; the message says
--since 1d is how to ask.  Only --mdt, --maxdepth and --mindepth are
refused at every setting -- they describe a walk, or need something a
per-MDT log does not have.  --ost and the object times are refused
without --resolve alone, the lookup supplying both.

-type is the one field with a third answer.  A record says what kind
of object was created and only for four record types, so an object
whose event does not carry one is counted undecided rather than
compared against a mode of zero, which reads as "not a file" and "not
a directory" at once.

Output is a pathname where the FID still resolves to one and the FID
where it does not, which is the whole point of asking: an object
unlinked since its event is the case --changelog exists for, and
counting it as "no pathname" the way a target scan counts its own
objects would drop exactly that case in silence.  The count of those
is reported at the end, so a caller knows some lines are FIDs.

The path argument names the filesystem, not a subtree, a changelog
knowing nothing about where an object sits; a path below the mount is
refused unless --resolve makes a pathname to match against.

--resolve looks each object up through the mount and hands
find_decide() what it found -- the path, the parent descriptor and the
object's own -- so -size glimpses the OSTs, -projid opens through a
descriptor of ours, and -printf gathers as a walk gathers.  It fills
what the stream lacks and never restates what it has: the merge starts
from the looked-up record and puts every field the event answered for
back over it, taking only the type bits of the mode, so -perm reads
the live one.  The subtree is tested on that pathname, over every name
the object has rather than the one linkno 0 answered with.

An object that has gone between its event and the lookup keeps the
answer honest in the other direction.  It is still in this answer --
resolution fills and never removes -- but only where the record can
carry the question: where the search asks something only a lookup
supplies, or names a subtree the object can no longer be placed in, it
is counted undecided instead.

find_device_supported() refuses --changelog for the same reason it
refuses --since: the option table is compiled into lfind too, so it
parses there, and llapi_find_device() never reads it -- `lfind
--changelog all` would otherwise have run the target scan, printed
everything the other predicates kept, and exited 0.

-name joins -type as a predicate the record cannot always answer.
cr_namelen is 0 for an event on a FID whose name the log never held,
so CL_CLOSE, CL_SETATTR, CL_TRUNC and CL_LAYOUT all
arrive nameless -- a file created before the window and written inside
it is the ordinary case.  Matching those against "" decided them both
ways and said nothing about it, and is not even reliably a non-match,
fnmatch("*", "") succeeding.  They are counted undecided instead.

The first resolve failure that is not "this object has no name"
decides the exit status, as it already does for a target scan, so a
run that could resolve nothing cannot report itself complete.

fc_d is set only for a directory, as find_since_rec_cb() already had
it.  printf_format_lustre() reads d == -1 as "not a directory", and
sr_fd is not always -1 for a regular file: under -printf the gather
widens, scan_rec_gather() opens the object for the MDT index and
leaves that descriptor in sr_fd.  Passing it through sent every
striped file down the directory branch and onto an lmv buffer
find_rec_to_lmv() had just zeroed, so %Lc and %Li printed 0, %Lo
[0] and %Lp nothing.

-name is answered against the recorded name under --changelog.
find_since_pick() walks the object's other links to place it in the
subtree, and was substituting each link's basename into the record
before testing: right for --since, which asks what the object is now,
and wrong here, where the record is the answer.  The alternate path
still settles the subtree; only the name stays as recorded.

The pathname a match prints goes through the caller's spelling
whichever way it was composed.  Without --resolve it comes from
llapi_scan_rec_path(), which builds an absolute name from the mount,
so "lfs find ." answered /mnt/lustre/sub/f there and ./sub/f
everywhere else.  find_since_respell() splits into a find_respell()
taking the two roots, and the print site uses it.

The demand mask -printf widens is the one handed to llapi_scan_fid():
find_want() builds fss_want with the -printf flag set and the resolve
passes it as sp_want.  llapi_scan_changelog()'s own sc_want is a
separate literal and does not depend on -printf.  What the two sources
share is fss_want, which is why --since and --changelog widen
together.
The comment above it claimed %s, %b and %LP as the reason; those come
from find_decide()'s own stat and find_get_projid(), gated on
fc_gather_all, and find_want() clears LLAPI_SCAN_PROJID on its way
out regardless.  What the widening actually buys is LLAPI_SCAN_LMV,
for %Lc and its neighbours on a directory.

The cookie is not mentioned any more.  fp_since_cookie arrives with
--since-cookie in the next patch, and this one documented it in
llapi_find_since()'s Return: block and in llapi_find_since.3 --
an -EINVAL for giving both an anchor and a cookie, and an -ESTALE,
neither of which exists here.  -ENOTSUP does exist and was missing
from both.

A CL_MARK is dropped rather than delivered.
mdd_changelog_write_header() writes the marker flags into the union
with cr_tfid, so cr_tfid.f_seq would read back as CLM_ON|CLM_START,
0x10001 -- and fid_seq_is_igif() accepts anything in [12, 0xffffffff],
so the FID gate would pass a value that is not a FID and find_decide()
would print it as a FID with no pathname.

No such record can arrive today, so this is a guard and not a fix for
an observed answer: CHANGELOG_MINMASK is BIT(CL_MARK) and
mdd_changelog_mask_seq_write() hands it to cfs_str2mask() as the
floor, so CL_MARK cannot be masked out -- and
mdd_changelog_write_header() returns early whenever CL_MARK is in the
current mask, so no marker record is ever written.  Asking for "creat
mkdir" gives back "MARK CREAT MKDIR", and deregistering every user and
re-registering leaves an empty log.  The guard costs one test per
record and stops the flags being read as a FID if that ever changes.

It sits at the top of find_since_cand_cb(), above the FID gate.  The
anchor is not taken there but in find_since_filter_cb(), which runs
first on every record the log delivered, marks included: it has to see
them, or it lags the end of the log by whatever trailing run of them
the scan finished on.

-printf's widening now reaches --since.  find_want() clears
LLAPI_SCAN_PROJID on its way out and find_decide() gates
find_get_projid() and the OST glimpse on fc_gather_all rather than on
the demand mask, so a flag this patch sets for --changelog and not for
--since meant "--since -printf %LP" answered DEFAULT_PROJID for every
object where a walk answered the real one, and %s/%b answered from the
MDT instead of a glimpse.

The comment on the nameless case said cr_namelen is 0 for a MARK.  It
is not: mdd_changelog_write_header() sets it to strlen(obd->obd_name)
and copies the device name in, so a mark arrives named "lustre-
MDT0000" -- which is also why "-name 'lustre-MDT*'" matched one before
the record was dropped above.  A data event on a FID whose name the
log never held is the real case, and the sentence now says only that.

The undecided warning no longer claims a missing field.
find_since_cand_cb() also counts an object undecided when it has gone
and the search needed a lookup or named a subtree, and there the
record answered -- the object is what is missing.  "could not be
decided from their changelog record" covers both.

LLAPI_SCAN_CL_F_ONCE is set for --since and not for --changelog.  Once
means once per scan, which is what "as it is now" wants; a source
answering as it was recorded has a second burst to report, so an
object touched in two of them prints twice.  The man page says so.

The dropped-objects warning names what the count is of -- "%llu of
%llu changed objects could not be read" -- rather than leaving a bare
number beside the errno it belongs to.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: If9ead118b5fc1dee86cfac6646106e982841d8e2
68419 master J !B ✗T –R 0/2
LU-20650 lfs: find --since-cookie, per-MDT anchor
mine build failure (#131394)
L +708 −36
5h
janitor run: build failed — no tests ran — janitor results
build #131394 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --since-cookie, per-MDT anchor

cr_index belongs to one MDT, so "where I got to" on a DNE filesystem
is one number per MDT and not one number.  --since <time> is the human
spelling and means the same thing everywhere, but only as far as the
clocks agree; a cookie is exact on every MDT, and is what a repeated
job should use.

The file is a line per MDT, "<mdtname> <index>", read as the anchors
and rewritten with the indexes this run reached.  It is written whole
and renamed over, so an interrupted run leaves the previous anchors
rather than half of the new ones -- a half-written cookie is worse
than a stale one, because the stale one is caught.

Which is the reason a cookie is worth having.  If an MDT has purged
past its anchor the records in between are gone, and reading from the
oldest one left would return a short answer with nothing to say so.
So the oldest surviving index is read first, by a one-record scan that
stops at the first callback, and an anchor older than it is refused
naming both numbers.  Every MDT is checked before any of them is
scanned: refusing from inside the scan would leave the earlier MDTs'
matches on stdout with no cookie written, and the next run would
repeat them.  An MDT with no anchor is not probed at all -- 0 has
nothing to be stale against, and that is every MDT of a first run.

Zero is reserved for "the log is empty", which is why a record that
arrives without an index is reported rather than recorded as index 0.
Changelog indexes start at 1, so storing 0 for a record that has one
would make a log holding records look like a log holding none, and 0
compares false against every anchor -- the check would go on being
skipped and say nothing about it.  An empty log does proceed: from a
client, "nothing happened since the anchor" and "everything since it
was purged" look the same, and refusing on that would fail on every
idle filesystem.

A cookie from another filesystem would anchor this one at indexes that
mean nothing here, so a line whose name does not match the filesystem
is ignored, and a file whose header names another filesystem is
refused.  Ignoring every one of its lines instead would leave no
anchors at all, rescan this filesystem's whole log, and then drop the
other one's anchors in the rewrite -- three wrong answers, none of
them said out loud.

The rest of the file is input the caller does not control either.  The
scanf width is not optional: %s into a fixed buffer is how such a file
overruns it.  Nor is the type of the bound test, since %x accepts a
sign: "<fsname>-MDT-1" reads back as 0xffffffff, which passes a test
made against an int and then indexes the array far past its end.  So
the sign is refused at the parse and the bound is compared against the
value that does the indexing.

--since and --since-cookie are two ways to say where to start, so
giving both is refused rather than one of them silently winning.  More
than one path is refused for a related reason: lfs find runs the
search once per path and every run advances the anchors, so the second
path started past the end of the log, printed nothing, and left the
records it should have matched behind the anchor for good.

The cookie is proved writable in the same pre-pass that proves the
anchors are not stale, and for the same reason -- a cookie that cannot
be written leaves the matches on stdout and the next run repeating
them.  A missing directory component reads as ENOENT, which
find_cookie_read() takes for a first run, so the typo was not noticed
until every log had been scanned.

The temporary file is fsync()ed before the rename, and the directory
after it.  The rename makes the replacement atomic against another
process, which is not the same as durable: after a host crash the
rename can be on disk while the contents are not, and an empty cookie
parses as no anchors at all.

The pre-pass probes the temporary name the write will use rather than
the cookie itself, because the write creates "<cookie>.new" and
renames over: what it needs is permission on the directory, which
opening an existing cookie for append does not test.  A cookie owned
by the job user in a root-owned directory passed and then failed
EACCES with every match already printed.

The anchors are left where they were when a run did not finish.  A
resolve failure that is not "this object has no name" drops matches,
and writing the cookie anyway would advance past the very records a
rerun needs, so the exit status is settled before the rewrite rather
than after it.

An index is read with strtoull() and a leading sign refused, the MDT
number having been refused for the same reason: "%llu" negates, so
"-2" would anchor a run at 0xfffffffffffffffe, match nothing, and
write that value back.

find_device_supported() refuses --since-cookie along with --since:
the option table is compiled into lfind too, so it parses there, and
llapi_find_device() never reads it -- a target scan would have
printed every object the other predicates kept and exited 0, without
even reading or writing the cookie it was given.

ONLY=56 is the regression check that the shared paths still answer as
they did; --since-cookie's own subtests are in the tests patch at the
end of this series.

The cookie names the search as well as the filesystem, and a
mismatch is refused the way a wrong fsname already was.  The anchors
say how far each log was read, not how far a subtree was: a record is
counted before the FID and the subtree are tested, so a run over one
subtree carries every anchor to the end of the log and a second
subtree sharing the cookie starts past its own changes and prints
nothing.  That is the collision lfs_find() refuses when two paths are
given to one command, reached by running the command twice.  A cookie
written before this carries no root and is still accepted.

A cookie that exists and is not a regular file is refused up front.
The probe settles the containing directory, not the name: the write
ends in rename(), which answers EISDIR over a directory, while the
read succeeds and returns no anchors -- so naming a directory scanned
every log to the end, printed every match, and only then said it
could not rewrite.  An empty name is refused with it.

The comment on the file format said a comment or a blank line costs
nothing.  It costs the read nothing; the rewrite emits the header and
the anchors and nothing else, so it does not survive the run.  Said
so, the sentence otherwise reading as an invitation to annotate the
file.

The documentation for fp_since_cookie arrives here rather than in the
previous patch, which described the field, its -EINVAL and its
-ESTALE before any of them existed.

The root is the rest of the header line rather than a field.  A
pathname can hold a space and %s cuts it at the first one, so a search
under "/mnt/lustre/my data" was recorded whole and read back as
"/mnt/lustre/my": the comparison then failed for every run after the
first, and the message named a path the caller never gave.  The root
is whatever realpath() returned for the caller's path, so a directory
with a space in it was enough to reach it.

And the root is escaped rather than written raw, because the rest of a
line still cannot hold a newline.  Only '/' and NUL are excluded from
a name, so "/mnt/lustre/anb" was written whole, read back as
"/mnt/lustre/a", and refused for every run after the first naming a
path the caller never gave -- the space case one character further
out, with the remainder becoming its own line and being dropped.  Both
sides escape and the reader compares the escaped forms, so nothing
decodes: the mapping only has to be injective for a comparison, which
is also why backslash is escaped.  The refusal names both sides in
that spelling, or a newline would break the line meant to name it.

find_cookie_check()'s two other refusals say so as well.  An empty
--since-cookie and a name too long to append ".new" to both returned
the errno alone, beside a "is not a regular file" case that reports
itself, so the run ended at the caller's "failed for '<path>': Invalid
argument" with nothing naming the argument at fault.

Two diagnostics that this patch makes reachable are corrected with it.
The refusal of --resolve without --changelog said "--since already
reads the object", which names an option the caller need not have
given once --since-cookie exists; it names both anchored spellings
now.  And a cookie refused for its filesystem or its root was reported
twice, the second line saying "cannot read" over a file that read
perfectly well -- find_cookie_read() has already named which refusal
it was, so the wrapper is left with what fopen() passed up.

The MDT number is exactly the four hex digits find_cookie_write()
emits.  %4x converts the leading digits and ignores what follows, so
"<fsname>-MDT00001" and "<fsname>-MDT0000_UUID" both anchored MDT0000
at an index never written for it -- the short answer with nothing said
that the stale check exists to catch, from a file this function's own
comment says it ignores anything but "<mdtname> <index>" in.  The span
refuses the sign as well, so the separate test for it goes.

ERRORS gains -ESTALE, which the previous patch dropped on the
understanding it would arrive with the cookie and which the kernel-doc
Return: block has listed since.  It is the error the whole option is
built around, and the man page was the one place a caller of the API
could not find it.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ia8ce93368a2c2c8c2ea2ee88c8e7f7e099b1bfa5
68420 master J !B ✗T –R 0/2
LU-20650 tests: sanity cases for find's changelog flags
mine build failure (#131396)
1 unresolved L +478 −42
5h
build failure (#131396)
1 thread(s) waiting on others
janitor run: build failed — no tests ran — janitor results
build #131396 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 tests: sanity cases for find's changelog flags

lfs-find.1 gains --since, --since-cookie, --changelog and --resolve:
what each answers, that --since is a strict subset of the same search
without it, that a bare index belongs to one MDT and is refused where
that is ambiguous, that a number which could be an index or an epoch
second is never guessed at, and that reading a changelog neither
consumes it nor needs a registered user while records exist only while
some user is registered.  TIME's spellings are given in full, since
set_since() takes eight of them and its error text offers one the page
would otherwise not list.

The page says what --resolve does not answer for -- an object gone
between its event and the lookup is counted undecided and dropped, so
--resolve can leave out an object the same search without it reports
by FID -- and which name -name is tested against.  The log is
coalesced to one record per object before the search sees it, so the
name that can match is the one the object's latest event to carry one
carried: a file created as f and hardlinked as g is matched by "-name
g" alone, and the pathname printed is its first name rather than the
one that matched.  A plain walk meets each name in turn and matches
all of them.  The behaviour is deliberate -- substituting a current
name would answer -name against something the record never said -- but
a reader had no way to know it.

Four sanity cases.  160aa asserts the promise --since is worth having
-- that its answer is a subset of the same find without it, and a
smaller one -- rather than asserting a particular file, so it does not
depend on what else the filesystem is doing.  It names the files it
expects rather than only counting them, since a count that merely
shrank is also satisfied by returning nothing, and it widens the gap
around its window so a node that spends seconds in sync does not
change what the test means.  Its locals stay separate from the
assignments that fill them, and it sorts off the pipeline rather than
in it: "local x=$(...)" takes local's own exit status, which is always
0, and a command piped into sort reports sort's, so either shape would
let a failing lfs find read as the empty answer the index case
asserts.

160ab walks the refusals: -size, an object time, --maxdepth, a path
below the mount, an unknown MDT, -perm, -links, --mdt-count,
--mdt-hash and --xattr are each refused, --resolve on its own is
refused rather than dropped, and a named MDT is accepted as the other
half of --changelog's argument.  It asserts the --resolve answer
rather than the exit status alone: -size needs a strict size, the MDT
holds only the lazy one, and a resolution that did not glimpse left
every object "could not be decided" while still exiting 0.  It also
asserts -printf's %s, which reads the same size through a widened
gather, that --projid is answered rather than failing on the first
record, and that a subtree search under --resolve answers for that
subtree and not the whole filesystem.

160ab covers the two things that make --changelog worth having over
--since, which the man page leads with: an object unlinked since its
event is still in the answer, and its FID names it once no pathname
does.  It ends on -name, which is answered from the record: the log is
cleared, one file that predates the clear is written to -- data
stores, which carry no name -- and another is created.  The named one
has to come back and the nameless one must not, so a -name that
matched everything fails the second assertion and one that matched
nothing fails the first.

160ac runs the cookie three times: the first writes it, the second
returns the new object and not the old one, the third returns nothing,
and then a stale anchor and both anchors together are refused.  It
asks for creations only.  A wide mask records the test's own reads,
and then "nothing changed since" is never true -- which is what hid an
off-by-one in the cookie until a narrow mask made every run repeat the
last object of the one before it.

A test that asserts a precondition instead of creating one only holds
where that precondition happens to be true.  160ac's stale anchor is
made rather than assumed -- records the cookie has not consumed, then
a purge, then one more past the hole -- because writing index 1 is
only older than the oldest surviving record on a filesystem that
already has changelog history.  On a fresh one index 1 IS the oldest
record, nothing was purged, and refusing would be wrong.

160ac also feeds the cookie two files it is never written with: one
whose MDT index is negative, and one whose header names another
filesystem.  Neither is a bad anchor to ignore -- the first indexes
the anchor array far past its end, and the second is a whole-log
rescan of this filesystem plus the loss of the other one's anchors --
so one has to be ignored and the other refused, and the file it
refuses has to still be there afterwards.

160ad hardlinks one file into two directories and searches under each.
A changelog names an object by FID, and fid2path answers with one of
its names -- the first linkea entry, the one the file was created
under -- so a search rooted at the other directory is answered about a
name that is not under it, and -name about a basename the object does
not go by there.  Without the links being walked it finds the file
under a and loses it under b, which is a silent miss and the worst
shape a wrong answer can take.

The tag is tests: rather than doc: because that is where the bulk of
the change is; the man page rides along because the options it
describes are the ones these cases exercise.

There is no version gate.  Nothing in this series touches server code
-- it is lustreapi.h, lustre/utils/ and Documentation/ -- so the
options work against any server old enough to have changelogs, and a
gate on the MDS would skip interop runs that pass.  A client gate
would say nothing either: this script ships with the client whose lfs
has the options.  The third Test-Parameters line below is that claim
under test rather than only asserted: an old server, a new client, and
these four cases.

Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad
Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad mdscount=2 mdtcount=4
Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad serverversion=2.15.6
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Id4001c6821e72f15b83a9733bbb28cef4248b822
68726 master J ✗2B ✓T …R 0/2
LU-20603 llapi: pull a scan's records in batches
mine janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
2 unique failures XL +1257 −1
5h
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity2@ldiskfs+DNE:test_157dNEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
sanity2@zfs:test_157dseen in 1 other review
build #131397 SUCCESS · tests all Maloo sessions · test queue
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS1 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20603 llapi: pull a scan's records in batches

Add llapi_scan_namespace_open(), llapi_scan_next() and
llapi_scan_close(): the namespace scan turned around, so a consumer
pulls records in batches on its own thread instead of being called
once per object on the scan's.  A layer over llapi_scan_namespace()
and not a second scanner -- the scan runs on a thread of its own, its
callback copies each record into the batch being filled, and
llapi_scan_next() hands out a full batch while the scan fills the
other.

The copy is what a batch costs.  A callback record points into
buffers the scan reuses for the next object, so a record that
outlives its callback has to own what it points at: the path, the
name, the layout, the directory stripe, the linkea, the rename source
and the jobid are copied into an arena the batch owns and the
pointers rewritten.  The descriptors are not carried and read -1.
Two batches suffice, the scan blocking when the one it fills is full,
so what is in flight is bounded by the batch size and not by the
tree.  The caller sees an array of record pointers rather than of
records, so its stride is a pointer's and a record that grows does
not move its neighbours under a caller built against the older size.

A filter in lfsp_filter runs on the scan's threads with the caller's
data, which the layer keeps beside its own.  An error is reported
after every record produced before it has been handed out, so the
batches before it are whole; a close on a running scan stops it, and
that is not an error.

A handle belongs to one consumer thread, and that is enforced rather
than only stated: llapi_scan_next() and llapi_scan_close() refuse a
second caller with -EBUSY.  Without it a second consumer resets the
batch the first is still reading and installs one whose sb_count is
already sb_cap, and the next append writes past the end of sb_recs --
reproduced under ASan on the unfixed build, five runs out of five, and
gone on the fixed one, which serves every object once.  It is the
opposite of the callback API's rule, where the callback runs on every
scan thread at once, so it is said in the header and the man page too.

The arena keeps every chunk across a reset rather than the largest.
A batch needing two chunks -- about 320 bytes of payload per record at
the 1024 default, which a path plus an 8-stripe layout reaches --
otherwise malloc'd and freed the second on every batch, which is what
the comment said it did not do.

Measured on a 20k-object tree off Lustre, where per-object work is a
cached lstat() and the copy is at its largest share: a batch of 1024
costs 76 ns per object over the callback at 4 threads (+8%), 135 ns
at one (+5%); a batch of 1 costs 2-4 us, the lock round trip per
record.  On a scan whose per-object work is an ioctl the same copy is
a fraction of a percent, which the man page says.

test16 rides along: it asks llapi_scan_namespace() what lfsp_got
reports, which is the callback API and not this one, and belongs with
whichever change lands lfsp_got.

llapi_scan_next.3 says when a batch stops being the consumer's, not
only that it does.  The release happens at the top of the following
llapi_scan_next(), before that call waits for the scan rather than
when it returns, so a consumer that hands a batch to another thread
and calls again to overlap the next fill has released what that thread
is reading -- and the -EBUSY that refuses a second caller does not
refuse a reader racing one.  The arena is rewound rather than freed,
which is what makes a steady-state scan allocate nothing per batch, so
that reader finds the following batch's bytes and not a fault.

The -EBUSY case is made rather than waited for.  A second consumer is
refused only while the first is inside llapi_scan_next(), and a test
that calls from between two of them never reaches the guard it is
about -- the ASan overflow above cannot fail it.  A filter that sleeps
holds the scan back so the third call blocks; two calls come first,
the layer filling one batch ahead and the consumer not blocking until
it has drained that.  The overlap is then measured, not assumed: a
machine slow enough to put the second call outside the window makes
the case unreachable, and an unreachable case is reported rather than
failed.

Test-Parameters: trivial testlist=sanity env=ONLY="56El 157c"
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I29aaa31a4c0bd34ca591316601a653f9b5ae70c2
67840 master J ✓B ✓T ✓R 1/2−
LU-20564 utils: probe md RAID geometry for OST mkfs/mount
mine needs rebase — checkpatch: cannot be cherry-picked
L +342 −0
2d
needs rebase — checkpatch: cannot be cherry-picked
no vote yet from Alex Zhuravlev, Li Dongyang
reviews: Andreas Dilger +1 2026-08-11 19:33
no vote yet: Alex Zhuravlev, Li Dongyang
patchsets: PS1 08-07 PS2 08-07
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS2 uploaded 32d ago · NEW · open in Gerrit ↗
commit message
LU-20564 utils: probe md RAID geometry for OST mkfs/mount

mkfs.lustre now probes the md array underneath an OST target (through
stacked dm devices) and seeds -E stride/stripe_width so mballoc can
align and size allocations to full stripes.  mke2fs can derive these
from the blkid topology, but that is absent on some stacked
configurations.  raid0, raid4/5/6, raid10 and raidkm (level 71) are
decoded; the replication factor of raid10 and the declustered g/m of
raidkm come from the md layout word.  User-supplied
stride/stripe_width values are respected, misaligned partition starts
draw a warning at mkfs time, and mount.lustre now warns when an OST on
a striped array has no (or mismatched) fs geometry.  A stripe_width
that is a multiple of the array row is still stripe-aligned and is not
reported.

mkfs.lustre also warns when the array's data row (k * chunk) is not a
power of two.  Such a row cannot be written whole by any client: bulk
RPC sizes are powers of two in practice, so a row divides some RPC
size if and only if the row is itself a power of two.  A geometry like
k=5 at 64K (320KiB) or k=14 at 64K (896KiB) leaves a partial row at
the tail of every RPC, which the array services as a read-modify-
write, at every chunk size and with no tuning available.  Unlike
stride/stripe_width this cannot be corrected afterwards -- it is a
property of the array -- so the warning is issued at mkfs time, while
the geometry can still be chosen, and is not suppressed by a
user-supplied stride/stripe_width.

Tested on RHEL 9.7 against live loop-backed md arrays: raid0/4/5/6,
raid10 near, far and offset layouts, a non-power-of-two row,
user-supplied values, dm-linear over one array and spanning two with
conflicting geometry, and aligned and misaligned partition starts.
Also tested against live raidkm (md level 71) arrays: plain m=2 and
m=3, parity-last, and declustered g=6/m=2 and g=8/m=3 over wider
pools.  In every case the computed row matches md's own
queue/optimal_io_size.

Test-Parameters: trivial
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: Claude:Fable-5 claude-code
Assisted-by: Claude:Opus-5 claude-code
Change-Id: I442528cea51a55c6b74f7f2282a1db566eab9783
67783 master J !B ✓T ✓R 1/2−
LU-20542 gss: retry callback RPC when reverse ctx is stale
mine needs rebase — checkpatch: cannot be cherry-picked
M +87 −3
2d
needs rebase — checkpatch: cannot be cherry-picked
no vote yet from Arshad Hussain, Aurelien Degremont, Bruno Faccini, Andreas Dilger
janitor run: 5 failing config(s), none unique to this patch — janitor results
10 optional test failure(s) — informational
build #129017 SUCCESS · tests all Maloo sessions
reviews: Sebastien Buisson +1 2026-08-10 09:49
no vote yet: Arshad Hussain, Aurelien Degremont, Bruno Faccini, Andreas Dilger
patchsets: PS1 08-05 PS2 08-09 PS3 08-10
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 30d ago · NEW · open in Gerrit ↗
commit message
LU-20542 gss: retry callback RPC when reverse ctx is stale

A server cannot renegotiate a reverse GSS context; one is only
installed from a peer's forward request. So when the peer drops its
contexts, the next callback is answered with GSS_S_NO_CONTEXT,
sptlrpc_req_replace_dead_ctx() returns -ECONNREFUSED, and
ldlm_handle_ast_error() evicts a client that was only mid-refresh.
This is the sanity-sec test_27d/27e failure.

Resend the callback a bounded number of times instead, marked with
rq_gss_rvs_resend so it skips the context refresh in
ptlrpc_check_set() and reaches the peer over the same dead context.
Every other resend still refreshes, so a reverse context that goes
stale on its own is still detected and replaced. Blocking and
completion ASTs stay bounded by rq_no_resend and the lock callback
timeout; for a glimpse AST, which sets neither, the resend count and
its backoff are the bound.

Fixes: 67acf6047e34 ("LU-17317 gss: do not continue using expired reverse context")
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I2a007b42656eb9aa0bed00291b67b6ac7db2d389
68414 master J !B ✓T ✗1R 0/2
LU-20648 mdc: fix changelog mask composition
mine enforced failed: review-dne-selinux-ssk-part-2
M +107 −8
3d
janitor run: 5 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130806 SUCCESS · tests all Maloo sessions
patchsets:
+1 earlier PS1 08-26
PS2 08-28 PS3 08-29 PS4 08-30 PS5 08-31 PS6 09-01
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS6 uploaded 8d ago · NEW · open in Gerrit ↗
commit message
LU-20648 mdc: fix changelog mask composition

A zero cf_mask means two different things and chlg_ioctl() composed
them as if it meant one.  On the server it means "this user has no
mask of its own": mdd_chlg_usermask() returns 0 for a record that
carries none.  On the client it means "do not filter": a zero
crs_user_mask skips the record type test altogether.

So "lfs changelog --user NAME --mask creat" for a user who has no
mask computed in.cf_mask & 0, which is 0, which the record test then
read as unfiltered.  The caller got every record type back and no
error, which is a wrong answer rather than a failure.

Compose the two zeros as what they mean.  A user with no mask of its
own is not restricted to nothing, it is unrestricted, so a caller's
mask passes through unchanged; only two real masks intersect.

Registration writes a rec2 when either a mask or a name was given.
It fills cur_mask when a mask was given, and otherwise only while the
server's changelog_mask is still minimal, where a maskless user is
handed CHANGELOG_DEFMASK instead.  So the reachable case today is a
registration with a name and no mask made while the server mask is
set to anything wider.  A user registered with neither name nor mask
gets the older record type, whose mask reads as zero whatever was
written, and is affected too once LU-20647 makes such a user
reachable by lookup at all.

Two real masks that do not overlap intersect to zero as well, and
zero cannot carry "nothing matches" while it also means "do not
filter".  Two absolute masks cannot reach it: cfs_str2mask() seeds an
absolute mask from its minmask argument, and both the registration and
"lfs changelog --mask" pass CHANGELOG_MINMASK, so "-m creat" is really
MARK|CREAT and "--mask mkdir" is MARK|MKDIR -- always sharing
BIT(CL_MARK).  A relative one does.  llapi_convert_str2mask() seeds
from *oldmask when the first token carries an operator, and
lfs_changelog() seeds that with CHANGELOG_DEFMASK, so "--mask
=-mark,-creat" is DEFMASK without those two: non-zero, and sharing no
bit with a user registered "-m creat".  So the wrong answer is
reachable from the shell, and against such a user "lfs changelog
--user clN --mask=-mark,-creat" printed every record it had.

So crs_user_mask now names the record types to deliver: it starts as
all bits, an empty intersection is no bits, and the record test is a
plain bit test with no special case for zero.

test_160z asserts that the user carries no mask before going on,
rather than reasoning that it must: changelog_register() sets +hsm
before registering, and mdd_changelog_mask_seq_write() rebases a
relative mask onto CHANGELOG_DEFMASK while the proc mask is still
minimal, so the branch that hands a maskless user a default one
cannot fire there.  The test widens the mask so that CREAT and MKDIR
are recorded whatever an earlier subtest left it at, and only on
$SINGLEMDS, which is the facet its trap restores.  It asks for --mask
creat and requires the MKDIR record to be absent, then asks without
--mask and requires it to be present.  The second half is what keeps
the fix from turning "no mask" into "no records".

160z covers the disjoint case in its second half, with a user
registered "-m creat" read back through "--mask=-mark,-creat".  An
earlier attempt used two absolute masks and was removed after a
control run showed it passing against the unfixed module as well; this
one cannot, the composition there being zero and zero meaning "do not
filter".  The control beside it is the same user read without --mask,
so a run that selects nothing because the user has nothing cannot be
mistaken for the fix working.

Fixes: 41b55cf2309d ("LU-19296 changelog: Add user-specific changelog filtering")
Test-Parameters: testlist=sanity env=ONLY=160z
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib178e4c15be40c415262bf06c08da8b061855cf9
68413 master J ✗1B ✓T ✗2R 0/2
LU-20647 mdd: look up a changelog user of either record type
mine janitor: 1 test failure unique to this patch — sanity-quota@zfs:test_48
1 unique failure M +106 −6
3d
janitor: 1 test failure unique to this patch — sanity-quota@zfs:test_48
enforced failed: review-dne-part-5, review-dne-selinux-ssk-part-2
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity-quota@zfs:test_48seen in 2 other reviews
failed enforced testplatformdetail
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: sanityn. session
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130807 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger
patchsets:
+1 earlier PS1 08-26
PS2 08-28 PS3 08-29 PS4 08-30 PS5 08-31 PS6 09-01
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS6 uploaded 8d ago · NEW · open in Gerrit ↗
commit message
LU-20647 mdd: look up a changelog user of either record type

mdd_changelog_user_register() writes CHANGELOG_USER_REC when the
caller gave neither a mask nor a name, deliberately, for
compatibility.  mdd_changelog_user_lookup_cb() matched
CHANGELOG_USER_REC2 only, so such a user was never found and
mdd_changelog_user_lookup() returned -ENOENT.  That is the shipped
"lfs changelog --user", which reaches the callback through
OBD_IOC_CHANGELOG_FILTER, not only the library call.

Every other MDD walker already takes both types and guards the fields
that exist in only one of them, so the callback was alone in
rejecting the record rather than the field.

The type test cannot simply be dropped.  struct
llog_changelog_user_rec is half the size of rec2 and carries neither
cur_mask nor cur_name, and the || short-circuited before strcmp()
reached cur_name; removing the first clause would read past the end
of the record into whatever the llog buffer holds next.  So a lookup
by ID matches on cur_id alone, a lookup by name skips an old record
instead of comparing against it, and the reply's username is filled
only from a rec2.  The reply's mask already came from
mdd_chlg_usermask(), which returns 0 for the old type, and a zero
mask is what the client reads as "do not filter".

The reply's username comes from the record's type and not from how the
lookup was made.  mdd_iocontrol() passes one buffer as both request
and reply, so cf_user_id holds the record's ID by the time the old
"was this a lookup by ID" test read it -- true on every lookup, and
harmless only because a name lookup had matched that same name.

The name comparison is bounded as well.  cur_name comes off disk with
no guarantee of a NUL inside its 16 bytes, and
mdd_changelog_name_check_cb() already allows for that on the same
field.

test_160w covers this command but registers both of its users with a
name and a mask, so both records are rec2 and the plain path was
never walked.  test_160y registers one user plainly, one with a mask
and one with a name, and asks for the records of each; the masked user
is the control that has always worked.  The named user is coverage
rather than a regression case: the old callback rejected a
CHANGELOG_USER_REC in the first clause of its ||, before strcmp() ran,
so a lookup by name walked past a plain record before this patch as it
does after it.  Only the cf_user_id != 0 path changes behaviour, and
the client parses "cl<N>" into an ID, so that is the path lfs takes.

This wants LU-20648 beside it.  Making a plain user reachable makes
cf_mask == 0 the ordinary answer, and a client without that fix
composes in.cf_mask & 0 and reads the zero as "do not filter": so
"lfs changelog --user cl1 --mask creat" on a plain user would print
every record type where it used to stop at -ENOENT.  Landing this one
alone trades a lookup failure for a wrong answer.

Fixes: 5b85a4eb7510 ("LU-19296 changelog: retrive changelog user info from MDT")
Test-Parameters: testlist=sanity env=ONLY=160y
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12ef76c773ce31e4548db9022f381cabfe7f5ddc
64945 master J !B ✓T ✗2R 1/2−
LU-20050 ptlrpc: fix import ref leak on sepol failure
mine enforced failed: review-dne-zfs-part-1, review-dne-zfs-part-7
2 unresolved XS +7 −1
6d
enforced failed: review-dne-zfs-part-1, review-dne-zfs-part-7
Sebastien Buisson voted -1
2 unresolved thread(s) await your reply — last from Gerrit AI review for Lustre
janitor run: 3 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-zfs-part-1 RHEL 9.7 / x86_64 ran 3 tests. 1 tests failed: sanity. session
review-dne-zfs-part-7 RHEL 9.7 / x86_64 ran 6 tests. 1 tests failed: sanity-ec. session
build #130839 SUCCESS · tests all Maloo sessions
reviews: Sebastien Buisson -1 2026-09-02 13:56 · Andreas Dilger +1 2026-09-02 13:56
no vote yet: Colin Faber, wangdi
patchsets: PS1 03-27 PS2 03-27 PS3 03-27 PS4 09-02
threads: 2 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS4 2026-09-03 08:01 Gerrit AI review for Lustre COMMIT_MSG:30 — (minor) This does not seem to hold, and it argues against the Fixes: tag just below it.
Gerrit AI review for Lustre PS4 · 2026-09-03 08:01
(minor) This does not seem to hold, and it argues against the Fixes: tag just below it.

sptlrpc_sepol_get() already returned genuine error pointers before 8a6788b508db:

    if (imp_sec == NULL)
        RETURN(ERR_PTR(-EINVAL));
    ...
    out = sptlrpc_sepol_get_cached(imp_sec);
    if (!out)
        RETURN(ERR_PTR(-ENODATA));

sepol_helper() also returns `ret >> 8` of call_usermodehelper(), so when the helper cannot be exec'd at all (missing /usr/sbin/l_getsepol gives -ENOENT) rc comes out as -1, which IS_ERR() accepts.

What 8a6788b508db changed is the l_getsepol-exits-nonzero case, where the status was positive. Stating that the leak is only reachable after it may keep the fix from being picked up on branches that predate it. Would it be more accurate to say 8a6788b508db made the path much easier to hit?
reply PS4 2026-09-03 08:01 Gerrit AI review for Lustre import.c:770 — (suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:32
Gerrit AI review for Lustre PS4 · 2026-09-03 08:01
(suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:326/481/545 all jump straight from the IS_ERR() branch to err_free_rq, which calls ptlrpc_request_free(), so they leak the import reference and the imp_reqs count in the same way.

That is pre-existing and this patch does not have to carry it. It is not a one-line swap either, since err_free_rq is shared with the post-pack failures reached through err_put_sepol and those must keep ptlrpc_request_free(). Is a follow-on change under the same ticket planned for them?
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS4 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20050 ptlrpc: fix import ref leak on sepol failure

ptlrpc_connect_import_locked() calls ptlrpc_request_alloc(), which
takes a reference on the import via class_import_get(). When
sptlrpc_sepol_get() fails, the IS_ERR() branch calls
ptlrpc_request_free(). That only returns the request to its slab
and does not drop the import reference.

The leaked reference keeps client_obd_cleanup() from running, so
the LDLM namespace kobject is left in sysfs and the next mount
fails with -EEXIST.

Use ptlrpc_req_put() on this path instead. It drops rq_refcount to
zero and calls __ptlrpc_free_req(), which releases the import.

The second failure path, after ptlrpc_request_bufs_pack() returns
an error, still uses ptlrpc_request_free(). That is correct: the
out_free label inside ptlrpc_request_bufs_pack() has already
dropped the import reference, so only the slab is left to free.
Both paths now carry a comment saying which case they are.

This bug is only reachable after 8a6788b508db, which stopped a
positive errno from reaching ERR_PTR() and so made the IS_ERR()
branch above actually trigger.

Fixes: dd200e5530fd ("LU-8955 ptlrpc: manage SELinux policy info at connect time")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia4267f87219e2a7e4fbcb996978543174bcb7197
68582 master J !B ✓T ✗2R 0/2
LU-20546 ptlrpc: fix import ref on request setup failures
mine enforced failed: review-dne-zfs-part-4, review-dne-zfs-part-7
3 unresolved XS +5 −2
6d
enforced failed: review-dne-zfs-part-4, review-dne-zfs-part-7
3 unresolved thread(s) await your reply — last from Gerrit AI review for Lustre
janitor run: 2 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-zfs-part-4 RHEL 9.7 / x86_64 ran 9 tests. 1 tests failed: sanity-quota. session
review-dne-zfs-part-7 RHEL 9.7 / x86_64 ran 6 tests. 1 tests failed: sanity-ec. session
build #130838 SUCCESS · tests all Maloo sessions
threads: 3 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre COMMIT_MSG:25 — (minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync_new_unlink64_job() on paths that run *after* a successful pack, where dropping rq_import is correct. The ptlrpc_req_put() on the pack error in osp_sync_new_job() itself goes back to 1d371ca47e67 ("LU-1303 osp: OSP logging functionality"), and the class_import_put() at out_free that leaves rq_import set is older still.

So the double release predates 8e86156c34f3. Either drop this tag or point it at the commit that actually introduced the imbalance.
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre client.c:933 — (minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1) without clearing rq_cli_ctx, so a caller that uses ptlrpc_req_put() reaches sptlrpc_req_put_ctx() in __ptlrpc_free_req() and drops the same context reference a second time.

That is exactly gss_cli_ctx_fini_rpc(): it passes a non-NULL ctx, so out_free is unreachable for it and out_ctx is its only failure exit, and it disposes of the request with ptlrpc_req_put(). It is pre-existing rather than introduced here, but the commit message says clearing rq_import "lets callers use either free or put", which isn't true while the ctx is still dangling. Would sptlrpc_req_put_ctx(request, 1) at out_ctx (it does the put and NULLs the field) make the claim hold?
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre client.c:1051 — (defect) Is ptlrpc_req_put() safe here for a request that came from the pool?
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(defect) Is ptlrpc_req_put() safe here for a request that came from the pool?

__ptlrpc_request_alloc() falls back to ptlrpc_prep_req_from_pool() when the slab allocation fails, and that request keeps the pool's preallocated rq_reqbuf while rq_cli_ctx is still NULL (nothing has packed yet). __ptlrpc_free_req() then does:

    if (request->rq_reqbuf || request->rq_clrbuf)
            sptlrpc_cli_free_reqbuf(request);

and sptlrpc_cli_free_reqbuf() starts with LASSERT(ctx) on req->rq_cli_ctx, so this looks like an LBUG rather than a clean free.

The path seems reachable: osc_brw_prep_request() uses ptlrpc_request_alloc_pool() for OST_WRITE, so under memory pressure the pool fallback is taken; if the OSC import is idle at that point, ptlrpc_reconnect_if_idle() -> ptlrpc_connect_import_locked() itself does ptlrpc_request_alloc() and returns -ENOMEM under the same pressure. The old code went through ptlrpc_request_free(), which just returned the request to the pool.

The request also has no capsule yet (req_capsule_init() runs below), so it is not really in a state ptlrpc_req_put() expects. Would keeping the explicit atomic_dec()/class_import_put() plus ptlrpc_request_free(), or clearing rq_import and then freeing, be safer than routing this through the full teardown?
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS1 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20546 ptlrpc: fix import ref on request setup failures

ptlrpc_request_alloc() takes a reference on the import. Two
setup paths get its release wrong, in opposite directions.

ptlrpc_request_alloc_internal() open-codes atomic_dec() on
imp_reqs and then calls ptlrpc_request_free() when an idle
reconnect fails, so class_import_put() is never reached and the
reference leaks. Call ptlrpc_req_put() instead, which does both.

ptlrpc_request_bufs_pack() has the reverse problem: out_free
drops the reference but leaves rq_import set, so a caller that
disposes of the failed request with ptlrpc_req_put() releases it
a second time. gss_cli_ctx_fini_rpc() and osp_sync_new_job() both
do that on a pack error. Clear rq_import once it has been
dropped, which also lets callers use either free or put.

Fixes: 93d20d171c20 ("LU-11128 ptlrpc: new request vs disconnect race")
Fixes: 8e86156c34f3 ("LU-10486 osp: fix request leak on error in osp_sync")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Iec07b093e380f262b452ff4fc5f5fac64f1c9fb0
65026 master J !B ✓T ✗0R 0/2
LU-20050 ptlrpc: fix sepol_helper status decode
mine Maloo voted -1 — tests failed (details on the change)
M +203 −15
6d
janitor run: 3 failing config(s), none unique to this patch — janitor results
no vote yet: Andreas Dilger, Sebastien Buisson
patchsets:
+5 earlier PS1 04-01 PS2 04-20 PS3 07-22 PS4 07-29 PS5 07-30
PS6 07-31 PS7 08-03 PS8 09-02 PS9 09-02 PS10 09-02
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20050 ptlrpc: fix sepol_helper status decode

sepol_helper() declared ret as signed short while
call_usermodehelper() returns int, and decoded it with a single
"rc = ret>>8". That shift is only correct for a normal exit. A
kernel launch failure returns a negative errno, not a wait
status, and shifting collapses it (-ENOMEM becomes -EPERM). A
helper killed by a signal leaves the signal in the low 7 bits,
so ret>>8 is 0 and the failure is reported as success.

Widen ret to int and decode the three cases separately, with raw
bit tests since WIFEXITED/WEXITSTATUS are userspace macros:

  ret < 0            ->  kernel failed to launch the helper
  (ret & 0x7f) == 0  ->  normal exit (WIFEXITED)
  (ret >> 8) & 0xff  ->  exit code   (WEXITSTATUS)

The exit code must be sign-extended, not negated: l_getsepol
returns -errno whenever stdin is not a tty, always the case
under call_usermodehelper(), and exit() keeps only the low byte.
So -ENODEV arrives as exit byte 237, and (signed char)237 is
-19. sptlrpc_sepol_get() special-cases -ENODEV as "SELinux is
disabled, report nothing", so negating it made that branch
unreachable and turned those mounts into "Unknown error 237".
This extends 8a6788b508db.

A positive exit status is not an errno either, so negating it
invented plausible but wrong errors: 127 became EKEYEXPIRED and
126 ENOKEY, pointing at key management rather than a broken
helper. Report -EPROTO and log the raw status. sepol_helper()
can then no longer return a positive value, so drop the
unreachable rc > 0 case in sptlrpc_sepol_get().

The eight sptlrpc_sepol_get() error paths in mdc_locks.c,
mdc_reint.c and mdc_request.c leaked the request's import
reference and the LDLM locks gathered for early cancel: the
sepol check runs before the prep call, so the request still owns
both, but the error tail only called ptlrpc_request_free(). Keep
one exit per function and make it correct on both sides of the
prep call, through the new mdc_elc_req_put() helper, clearing
@count once the prep call owns the locks. This needs LU-20546,
which clears rq_import when packing fails so the reference is
dropped exactly once.

Add sanity-selinux test_21d: stub l_getsepol, mount with
send_sepol=-1, and check that exit 237 still mounts while 234
and a missing helper fail with the expected errno text. It
creates no nodemap on purpose -- with nodemap.<nm>.sepol set,
req_check_sepol() would refuse the -ENODEV connect for an
unrelated reason.

Suggested-by: Sebastien Buisson <sbuisson@ddn.com>

Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-1
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Fixes: c61168239eff ("LU-8955 sec: create new function sptlrpc_get_sepol()")
Fixes: 0a773f04b288 ("LU-8955 ptlrpc: manage SELinux policy info for metadata ops")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: If9312fce842fca133de21c4898cafd949ace6aa6

Longstanding (signal older than a week)

(5)
68340 master J !B ✓T ✗4R 0/2
LU-20643 utils: clear stale lmd fields on reuse
mine enforced failed: review-dne-part-2, review-dne-part-4, review-dne-part-5, review-dne-selinux-ssk-part-2
S +18 −2
3d
janitor run: 7 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-part-2 RHEL 9.7 / x86_64 ran 11 tests. 1 tests failed: sanity-lfsck. session
review-dne-part-4 RHEL 9.7 / x86_64 ran 9 tests. 2 tests failed: sanity-quota, sanity-dom. session
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: recovery-small. session
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130562 SUCCESS · tests all Maloo sessions
patchsets: PS1 08-25 PS2 08-28 PS3 08-28
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 11d ago · NEW · open in Gerrit ↗
commit message
LU-20643 utils: clear stale lmd fields on reuse

get_lmd_info_fd() reuses one struct lov_user_mds_data for every object
and convert_lmd_statx() writes only the fields lstat(2) supplies, so
stx_attributes, stx_attributes_mask, stx_btime and the mask bits above
STATX_BASIC_STATS keep whatever was there before.

Two ways that is not this object's data.  On the V1 ioctl path the
lstat_t is copied over the front of the record, so stx_attributes_mask
ends up holding st_atim.tv_sec -- never 0 in practice -- and stx_mask
picks up bits from st_nlink, which reads as STATX_BTIME on a directory
whose link count has 0x800 set.  It does not reach as far as it looks:
lmd_st is 144 bytes against 272 for lmd_fid and lmd_stx, so it stops
at the end of stx_mtime and stx_rdev_major onwards is still the
previous conversion's.  On the lstat fallback the buffer holds the
previous object's data outright, or the file name written in for the
ioctl.

So a walk that crosses onto a non-Lustre subtree can answer -btime and
the attribute predicates from a file name.

Clear what is about to be reused on both paths, with the same line:
everything up to lmd_lmm, which is what the record's own fields amount
to.  That takes lmd_fid, lmd_flags, lmd_lmmsize and lmd_padding with
it -- nothing in lustre/utils reads lmd_fid today, but a short name
there passes fid_is_sane() as an IGIF, and llapi_get_lum_file_fd()
copies by lmd_lmmsize.  On the V1 path it goes after the memmove, so
the layout it has just moved up is not touched.  The glimpse path is
left alone: there the MDT's answer is still wanted.

Fixes: 11aa7f8704c4 ("LU-11367 som: integrate LSOM with lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6126e73bb120bd6a12a904563e13303d63e75c1c
67052 master J !B ✓T ✗0R 0/2
LU-20119 dbg: test lustre_helper labeling fix for test_21a
mine Maloo voted -1 — tests failed (details on the change)
fortestonly S +32 −0
27d
janitor run: 2 failing config(s), none unique to this patch — janitor results
build #127246 SUCCESS · tests all Maloo sessions
patchsets: PS1 07-04 PS2 07-04 PS3 07-06
tags: fortestonly
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 65d ago · NEW · open in Gerrit ↗
commit message
LU-20119 dbg: test lustre_helper labeling fix for test_21a

fortestonly patch validating the fix for the sanity-selinux
test_21a failure on the DNE + RHEL 8.10 server + RHEL 10.0 client
config, where the client mount fails because l_getsepol returns
-EACCES (-25 before the errno fix below) from the sepol connect
upcall.

Root cause: the l_getsepol/lctl/l_getidentity/l_getauth helpers
were left labeled bin_t instead of lustre_helper_exec_t, so the
kernel execs them into the unprivileged kernel_generic_helper_t
domain (via the base "kernel_t bin_t:process
kernel_generic_helper_t" transition) rather than lustre_helper_t.
That domain cannot read selinuxfs or the on-disk SELinux policy,
so l_getsepol fails with EACCES.  Two reasons the label was never
applied:

1. lustre.spec.in %post loaded the policy module with "semodule
   -i" but never ran restorecon.  semodule -i does not relabel
   already-installed files, and the binaries were labeled bin_t at
   install time (before the module existed).  Add a restorecon of
   the four helpers to %post (the make-install path already does
   this, but it is skipped for staged/RPM builds where DESTDIR is
   set).

2. lustre_helpers.fc labeled only /usr/bin/*.  On RHEL/Rocky 10.2+
   the /usr/sbin=/usr/bin file-context equivalency makes that
   work, but on el8/el9 and el10.0/10.1 /usr/sbin is a real path
   with no such equivalency, so restorecon of the /usr/sbin binary
   finds no rule.  List both /usr/bin and /usr/sbin.

The sepol_helper() CERROR and the errlog() errno fix from the
prior diagnostic patch are retained so that, if the mount still
fails, the kernel log shows l_getsepol's real exit code.

Not intended to land; validates the fix before it goes on the
LU-20119 review.

Test-Parameters: fortestonly
Test-Parameters: livedebug mdscount=2 mdtcount=4 osscount=1 ostcount=8 clientcount=2 testlist=sanity-selinux clientdistro=el10.0 serverdistro=el8.10 env=ONLY="21a",SHARED_KEY=true clientselinux
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ifd207da02862849c90500a40b4ff71dff5bc711b
65848 master J !B ✓T ✗2R 0/2
LU-20238 ec: branch-free byte-indexed scalar base path
mine enforced failed: review-dne-part-2, review-dne-part-5
pt_ecro XL +503 −4660
33d
janitor run: 1 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-part-2 RHEL 9.7 / x86_64 ran 11 tests. 1 tests failed: sanity-lfsck. session
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: lustre-rsync-test. session
build #128825 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger, Patrick Farrell
patchsets:
+5 earlier PS1 05-13 PS2 06-08 PS3 06-17 PS4 07-10 PS5 07-10
PS6 07-30 PS7 07-31 PS8 08-04 PS9 08-05 PS10 08-06
tags: pt_ecro
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 34d ago · NEW · open in Gerrit ↗
commit message
LU-20238 ec: branch-free byte-indexed scalar base path

Replace the per-byte gf_mul() in the scalar *_base hot paths with
a byte-indexed per-coefficient region table:

  tbl[b] = c * b in GF(2^8), built once per coefficient
  gf_region_mul(tbl, x) = tbl[x]   (single load, inner loop)

ec_encode_data_base() at k=6/p=2, gcc -O2: 0.79x at 256 B, 1.08x
at 1 KiB, 1.32x at 64 KiB. gf_mul() alone is ~20% slower than the
64 KiB direct lookup it replaces, so regions below
GF_REGION_TBL_MIN_LEN (1 KiB) keep a scalar fallback.

Byte-indexed lookup (gf-nishida-16 "FullByte"-style) is also
patent-clean: no PSHUFB, no nibble split, no 16-byte gftbl, so it
is structurally distinct from the ISA-L SIMD pattern that maps
onto US 8,683,296 claim 21.

ec_base.c carried an unconditional "#define GF_LARGE_TABLES", so
both ec.ko and libec.a compiled the 64 KiB gf_mul_table_base and
the small-table branch was dead code. With that define and the
inert ec_base_o_CPPFLAGS in ec/Makefile both gone nothing could
set it, so the block is deleted outright: .rodata 65792 -> 1536,
.text 2217 -> 3591 for the fallbacks.

gf_vect_mul_init is renamed gf_vect_mul_init_base, with a wrapper
in ec_base_aliases.c; its 64-bit path now selects on BITS_PER_LONG
for kernel builds, since __WORDSIZE is glibc-only. No existing
entry point changes behaviour.

Validated on RHEL 10.1 (AVX2+GFNI): exhaustive gf_mul + gf_inv,
k=6/p=2 round-trip KAT, and gfni_ec_test byte-identical parity.

Test-Parameters: testlist=sanity-ec
Test-Parameters: testlist=sanity env=ONLY=910
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I905e365d145d0018cbcd2bbc919eb4e516fafe8e
65388 master J !B ✓T ✗1R 0/2
LU-20147 ptlrpc: module-level cache for SELinux policy upcall
mine enforced failed: review-dne-selinux-ssk-part-2
stalled 131d XL +693 −368
131d
janitor run: 3 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 5 tests. 1 tests failed: sanity-selinux. session
6 optional test failure(s) — informational
build #124437 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger, Sebastien Buisson
patchsets: PS1 04-16 PS2 04-29
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS2 uploaded 133d ago · NEW · open in Gerrit ↗
commit message
LU-20147 ptlrpc: module-level cache for SELinux policy upcall

Replace the per-import call_usermodehelper model with a single
module-level upcall_cache (hashsize=1, key=0) shared by all imports.

The original LU-8955 design called l_getsepol via
call_usermodehelper() on every RPC from every import independently.
On RHEL/Rocky 10.1 this fails: the kernel_generic_helper_t SELinux
domain is blocked from reading /sys/fs/selinux/policyvers and
writing to debugfs, causing a 30-second timeout on every first RPC.

Cache lifecycle and freshness:
- upcall_cache.h: add struct sepol_cache_data and union member in
  upcall_cache_entry.
- lustre_sec.h: remove ps_sepol and ps_sepol_checknext from
  struct ptlrpc_sec.
- sec.c: add sptlrpc_sepol_cache (module-level upcall_cache),
  sptlrpc_sepol_get(), sptlrpc_sepol_do_upcall() (fires l_getsepol),
  sptlrpc_sepol_parse_downcall(), and sptlrpc_sepol_cache_update()
  for unsolicited push updates.  send_sepol changed from static to
  extern.
- The cache is initialised with entry_expire = S32_MAX so the
  framework never reaps entries by time; sptlrpc_sepol_get() drives
  freshness by stat()ing the policy directory on every call and
  calling upcall_cache_flush_one() if its mtime differs from the
  value carried by the last downcall.  l_getsepol therefore runs
  only on mount or after an actual policy change.
- send_sepol == 0 disables the feature entirely (no policy info on
  any RPC).  Any non-zero value behaves identically; the legacy
  "send_sepol = N seconds" semantic is preserved as parameter-level
  backward compatibility but no longer affects runtime behaviour
  (with the cache in place, the LU-8955 reason for an N-second TTL
  doesn't apply).

Backward-compatible wire format and invocation:
- lustre_user.h: keep the pre-LU-20147 layout as
  struct sepol_downcall_data_v2 / SEPOL_DOWNCALL_MAGIC_V2 = 0x8b8bb843
  alongside the new struct sepol_downcall_data /
  SEPOL_DOWNCALL_MAGIC = 0x8b8bb844.
- sec_lproc.c sptlrpc_sepol_do_downcall() and sec.c
  sptlrpc_sepol_parse_downcall() accept both magics; only the new
  layout populates the policy-dir mtime tracking used by the
  freshness check.
- l_getsepol gained -2 / --new-format: with -2 it writes the new
  layout, without -2 it writes the v2 layout.  Default is v2 so a
  new userspace stays compatible with an old kernel.
- sec.c sptlrpc_sepol_do_upcall() keeps the pre-LU-20147 argv shape
  "--obd_type X --obd_name Y" (so old l_getsepol still recognises
  it) and just appends "-2" as a new flag.  X/Y are the type and
  name of any active OBD on the client (MGC preferred); the per-OBD
  srpc_sepol write handler funnels the result back through
  sptlrpc_sepol_do_downcall() into the module-level cache, so it
  doesn't matter which OBD is used.  We invoke under UMH_WAIT_PROC
  and on non-zero exit (eg. old userspace that doesn't recognise
  "-2") retry without "-2".
- sec_lproc.c: add module-level srpc_sepol write-only debugfs file
  at sptlrpc/srpc_sepol (LDEBUGFS_FOPS_WR_ONLY) so admin tools can
  feed the cache directly; per-OBD srpc_sepol kept for backward
  compatibility and used by the kernel upcall above.

l_getsepol changes:
- Replace file-scope static globals with per-call lgetsepol_ctx.
- Add -k/--kernel flag: writes downcall to module-level path
  sptlrpc/srpc_sepol; includes policy dir path and mtime.
- Add -2/--new-format flag (see above).
- Add -w/--watch flag: inotify daemon mode for policy change
  detection (retained as alternative; community feedback welcome).
- Add find_policy_with_highest_ver() helper used by the existing
  RHEL/Rocky 10.1 fix in LU-20045's stack; the helper itself is
  duplicated locally so this patch is self-contained against an
  older LU-20045 base.  When LU-20045 lands on master this duplicate
  can be dropped.
- Use fstatat(dirfd(dp), ...) in the policy version scan loop.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Icf813c252b7359c3dc17d80405579b50b57ab025
65120 master J !B ✓T ✓R 2/2
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini()
mine Verified -1 veto by Oleg Drokin
stalled 143d S +12 −0
143d
janitor run: 4 failing config(s), none unique to this patch — janitor results
build #123727 SUCCESS · tests all Maloo sessions
reviews: Andreas Dilger +1 2026-04-10 22:58 · Alex Zhuravlev +1 2026-04-11 05:38
no vote yet: Oleg Drokin
patchsets: PS1 04-06 PS2 04-06 PS3 04-10 PS4 04-10 Andreas Dilger
owner Hiroshi Nishida · uploader Andreas Dilger · PS4 uploaded 151d ago · NEW · open in Gerrit ↗
commit message
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini()

The LWP teardown path does not call ldlm_namespace_free_prior()
before client_obd_cleanup() -> ldlm_namespace_free_post().
This affects two paths:

Normal teardown (LCFG_CLEANUP -> lwp_process_config ->
lwp_disconnect -> lwp_device_fini): lwp_disconnect() calls
ptlrpc_disconnect_import() and ptlrpc_invalidate_import() but
never calls client_disconnect_export(), so
ldlm_namespace_free_prior() is never reached.  lwp_device_fini()
then calls class_disconnect() and client_obd_cleanup() directly,
skipping free_prior entirely.

Failed-connect path (lwp_obd_connect failure -> out_dis:
class_disconnect -> lpd_exp=NULL -> lwp_device_fini): the same
lwp_device_fini() is reached with lpd_exp NULL, so
class_disconnect is skipped and client_obd_cleanup() is again
called without free_prior.

Without ldlm_namespace_free_prior(), LDLM_NS_STOPPING is never
set.  If the pool recalc task runs concurrently with
ldlm_namespace_free_post(), the race can corrupt the pool kobject
cleanup path and cause ldlm_namespace_sysfs_unregister() to hang,
stranding the namespace kobject under
/sys/fs/lustre/ldlm/namespaces/.

Fix by calling ldlm_namespace_free_prior() in lwp_device_fini()
before client_obd_cleanup().  This is symmetric with lwp_setup()
-> client_obd_setup() where the namespace is created, and covers
all LWP teardown paths.  The import is still valid at this point
since obd_cleanup_client_import() runs inside
client_obd_cleanup() afterwards.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ie9a290ca529d18b0f837669a53c0c06cda4f767f
0 watched
Nothing watched. Add a change above — e.g. after rebasing someone's patch — and its build/test state shows up here.
34 open

Failed / broken

(20)
68288 master J ✗1B ✓T …R 0/2−
LU-20637 llapi: name a device scan's objects
janitor: 1 test failure unique to this patch — conf-sanity4@zfs:test_302
1 unique failure XL +1872 −62
4h
janitor: 1 test failure unique to this patch — conf-sanity4@zfs:test_302
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
conf-sanity4@zfs:test_302NEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
build #131391 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+7 earlier PS1 08-24 PS2 08-25 PS3 08-26 PS4 08-28 PS5 08-29 PS6 08-29 PS7 08-30
PS8 08-31 PS9 09-01 PS10 09-03 PS11 09-04 PS12 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS12 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20637 llapi: name a device scan's objects

A scan of a target holds names and parent FIDs but no paths, so lfind
prints FIDs and a user has no way to turn them into pathnames.  The
HLD names an Output Format for exactly this, beside the FID printing
that is already there.

Add llapi_scan_rec_path(), which uses the path a namespace walk
already put in the record and resolves the FID through a client mount
when there is none.  lfind gets --fid2path MOUNT for it.

The cost is one ioctl per object, so it is the consumer's to ask for
rather than the scanner's to pay: it falls on the objects a search
kept, not on every object it saw.  That is also why the HLD prices
pathname generation separately from the scan rate.

An object with no pathname is counted and reported at the end rather
than printed: the objects a target keeps for itself have none, their
FIDs being below FID_SEQ_NORMAL, so a search under --internal is
expected to report some.  An OST data object does have a pathname --
its FID resolves through the OSC to the file that owns it, which
sanity test_154C asserts -- so a scan of an OST names that file once
per stripe it has on the target.  A hardlinked object is printed once,
under its first name.  A FID that cannot be looked up at all -- no
CAP_DAC_READ_SEARCH, which ll_fid2path() checks first -- is an error
and not a missing name, so a broken search cannot read as a complete
one.

Neither half of the resolution asks anything of the target being
scanned, which is what lets it name objects on a target that is out of
service -- and a ZFS target can only be scanned out of service, its
pool being exported.

An OST data object is named by the file that owns it, read from the
object's own trusted.fid.  Its own FID is an OST-sequence one, which
__ll_fid2path() answers only by asking the OST holding it, through the
OSC: exactly the target the scan is reading.  ff_parent is the owner's
MDT FID, and the stripe index filter_fid keeps in its f_ver is
dropped,
that field being an index and not a version.  An object no file owns
yet -- precreated and never written, of which a target holds many --
carries no trusted.fid and is reported as having no name rather than
being chased through the OSC.

trusted.fid is not the only place that FID lives.  An OST whose inodes
are 256 bytes or smaller has none at all: osd_xattr_set_pfid() packs
the parent into the LMA as a struct lustre_ost_attrs and removes the
separate xattr, saying so with LMAC_STRIPE_INFO.  Reading only
trusted.fid there leaves every object on the target without an owner,
so --fid2path prints nothing and reports them all unnamed -- measured
on an OST formatted -I 256: 33 objects, no owner on any of them,
against four files whose paths it should have printed.  mkfs.lustre
defaults to -I 512 now, but -I 256 is still accepted and older OSTs
carry it.  The LMA buffer is already in hand for the classification,
so the fallback costs no second xattr, and ll_decode_filter_fid(8)
reads the same two places.

An MDT object is named from its trusted.link and a map of the target's
directories, FID to parent and name, built by a pass over the target
before the search.  A pass of its own because objects arrive in inode
order, so an object is usually delivered before its ancestors are;
resolving inline would mean holding every match until the scan ended.
The pass asks for no layout, no SOM and no HSM, and a filesystem holds
far fewer directories than files.  On an OST it stops at the first
object and leaves the map empty.

There is no fall back to the lookup once the map is in use.  The map
is
used precisely when the target cannot answer, so one object it could
not place would stop the whole search there.  Two things are nameless
in consequence and counted with the rest: an ownerless OST object, and
on DNE an object whose ancestors live on another MDT, the map covering
only the target scanned.

--paths is the same composition with no mount at all.  --fid2path
still needs one, not to resolve anything but as the filesystem the
paths hang from and for the comparison below, and a filesystem whose
only MDT is the target being scanned has none to give -- which for a
single-MDT ZFS filesystem is every scan of its MDT, the pool having
to be exported.  --paths prints filesystem-root-relative names
instead, asks nothing of any running service, and is refused on an
OST, whose objects are named by files that live on an MDT.

A mount whose own filesystem cannot be read is refused rather than
run unguarded: --fid2path was asked for explicitly, and the fallback
was the very outcome the comparison exists to prevent, another
filesystem's pathnames printed with a zero exit.

llapi_scan_rec_path.3 says the same thing to a consumer pairing that
call with llapi_scan_device() directly, because nothing in the
library can make the check for them: the call is handed a descriptor
and a record and never sees the target the record came from.  lfind
compares the mount's fsname with the target's label; anyone else has
to do that too, or know by construction that the two agree.

A MOUNT of another filesystem is refused when the target is opened,
its fsname compared with the one the target's label names.  The
comparison sits inside the scan's own open rather than in a probe
ahead of it: a second open is not free on every backend, and opening
a target twice buys nothing the one open cannot answer.  It cannot be
caught later: FID sequences are not unique across filesystems.
FID_SEQ_ROOT and FID_SEQ_DOT_LUSTRE are constants, so the root of
every filesystem has the same FID, and normal sequences overlap
because each filesystem's controller starts from FID_SEQ_NORMAL
(LUSTRE_SEQ_SPACE_RANGE).  fld_client_lookup() on the wrong mount
therefore finds a range and __ll_fid2path() answers with a pathname
from it, so the run would have printed another filesystem's paths as
though they were the target's and exited 0.

The mount point is resolved before use: a bare fsname, a
subdirectory and a trailing slash all open the same root, and none of
them names a file when prefixed to what fid2path answers.

conf-sanity test_302 stops an OST, exports its pool where the backend
is
ZFS, and compares the names a scan of it gives against what the client
saw before it went down -- a scan that resolved nothing and one that
resolved everything both exit 0, so the set is what is asserted.  Its
files are written rather than merely created, a precreated object
being
rightly nameless.

conf-sanity test_301 diffs the resolved paths against what the client
sees, and checks that a hardlinked object is named once.  It mounts a
client on the MDS for the duration, because --fid2path resolves
through a mount on the node running the scan and a server is not a
client; a configuration that cannot mount one skips.  It is ldiskfs
only: reading a target in service is what ZFS refuses.

scan_device_run()'s comment says what a second open costs rather than
calling it a deadlock, because it is not one: llapi_find_device()
builds the directory map in a pass of its own, so every --fid2path and
--paths
run already opens and closes the target twice in one process, and
conf-sanity test_302 does exactly that against a stopped OST with its
pool exported and passes on conf-sanity4@zfs.  The reason to test the
fsname inside the scan's own open is the cost of the second import and
export, not a hang.  The wasted pass on an OST -- where
scan_dirmap_cb() stops at the first object -- is left as it is;
folding the map into the search's own open would serve both from one
open and is worth doing separately.

The resolve-failure message names the FID that was looked up and the
mount only when there is one.  An OST object is resolved by its
owner's FID rather than its own, so the message named the wrong one;
and under --paths there is no mount at all, yet -ERANGE and -ELOOP
still arrive here and reported a failure "through the mount given".

lfind(8) says what --fid2path costs, which differs by target and is
easy to state as one number and be wrong. One lookup per kept object
is the OST's cost; on an MDT there is no lookup and a whole extra pass
instead, to build the map the page describes four blocks further down
-- the more expensive of the two, and paid before the search rather
than per match.  Where a name cannot be composed the page points at
lfs-fid2path(1), which is a tool that exists, rather than at a
lookup-only mode of this one, which is not.

--paths and --fid2path together goes through the same exit as every
other error in that function.  Returning CMD_HELP from main() exits 4
and prints no usage, where "ret = CMD_HELP; goto out" prints it and
exits EXIT_FAILURE.

fp_paths takes fp_unused_bit1.  struct find_param keeps those bits for
exactly this, and a trailing byte would have cost 8 with padding.

The directory map's names stay inline.  At NAME_MAX + 1 an entry is
~288 bytes, so a million directories is ~288 MB and 1.5x that while
scan_dirmap_grow() holds both tables, and interning them in an arena
would cut it several-fold.  Left for now: it is a change to the map's
memory layout for a case no measurement here has reached, and a
calloc() failure already fails the search cleanly rather than
corrupting it.  Worth revisiting when --paths is pointed at a target
big enough to need it.

conf-sanity test_303 covers --paths, where 301 and
302 both exercise only --fid2path, so the composition --paths exists
for was never run.  It scans an MDT in service with no mount given,
which is the case a filesystem whose only MDT is the target has to be
named in, and asserts the answer is the client's set: one path per
object rather than one per name, the hardlinked object once, no FID
where a path was asked for, and no mount point in the answer -- root-
relative being what separates it from --fid2path, and a mounted prefix
meaning the wrong composer ran.  It also asserts the two refusals:
--paths on an OST, whose objects are named by files that live on an
MDT, and --paths together with --fid2path.  ldiskfs only, an imported
ZFS pool answering EBUSY by design.

lfsp_fsname makes the check a public caller can ask for.
llapi_scan_rec_path.3 tells one they must establish that the mount and
the target are the same filesystem, and nothing in
<lustre/lustreapi.h> let them: scan_device_run() is internal,
llapi_scan_device() passed want_fsname NULL, and the target's label
lives in a private header.  The field is the same machinery under a
public name -- the scan refuses a target whose label names another
filesystem with -EXDEV -- and it has to be the scan that does it,
because only the scan reads the label and a lookup on the wrong mount
succeeds and answers with that filesystem's pathname.

The usage agrees with its own option list.  "The output is a
FID because a target has no paths" sat two lines below --paths and
--fid2path, both of which print pathnames; it now says the output is a
FID unless one of them is given.

test_301 and test_303 sync the MDS, not only the client.
lustre_super_operations has no .sync_fs, so a client sync writes back
page-cache data and leaves the MDT's ldiskfs to commit on its own
schedule -- and both tests scan that ldiskfs directly while it is in
service, so an object still only in memory is one the scan cannot see
and the comparison against the client's answer is a race.  test_302
needs none of this: its target is stopped, which flushes it.

test_302 also tells the scan where the OST's pool devices are.  An
exported pool is found by reading vdev labels under a search path,
which defaults to /dev, and the test framework's ZFS vdevs are files
in $TMP -- so without --search the pool is not found at all and the
test failed on every ZFS configuration with ENOENT.  On ldiskfs the
argument is empty and nothing changes.

test_301's one bare lfs becomes $LFS, as every other call in the file
is.  An interop run that points LFS at another binary would have used
whatever PATH found.

The EXAMPLES fragment in llapi_scan_rec_path.3 escapes its newline as
n, not n: troff reads the single form as a register reference,
which rendered the line as printf("%s0, path).  llapi_fid_parse.3
escapes it the same way.

The three places that said a target scan needs no client now say what
--fid2path changes.  lfind.c's file header and llapi_find_device()'s
block comment both stated it flatly, and llapi_find_device.3
documented neither fp_fid2path_mnt nor the -ENAMETOOLONG an over-long
mount path answers with; its SEE ALSO gains llapi_scan_rec_path(3),
which is the call that does the resolving.

llapi_scan_rec_path.3's NOTES point a consumer at lfsp_fsname rather
than telling it to compare the mount and the target itself.
lfsp_fsname is that comparison -- the scan refuses a target of another
filesystem with -EXDEV -- and saying otherwise on the page that
introduces it would send a caller to write what the library already
does.

test_303's one bare lfs becomes $LFS, as test_301's did.  It was added
after that fix and reintroduced it.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6476abfd28c91b2ccc04fb08c4e5860aef8fe009
68415 master J ✗2B ✓T …R 0/2−
LU-20649 llapi: a changelog as an Object Stream
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
2 unique failures XL +2327 −3
4h
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity2@ldiskfs+DNE:test_157dseen in 1 other review
sanity2@zfs:test_157dNEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
build #131390 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20649 llapi: a changelog as an Object Stream

The two scanners we have enumerate what exists.  A changelog
enumerates what happened, and llapi_scan_changelog() delivers it in
the same record, so a consumer written against llapi_scan_namespace()
or llapi_scan_device() can be fed by a delta without being rewritten.

A changelog record carries the FID, the parent FID, the name, the
event and its time, and -- where the server recorded them -- the uid
and gid of whoever caused it.  That pair is lfsr_event_uid and
lfsr_event_gid, not lfsr_uid: mdd_changelog_ns_store() fills the
uidgid extension from lu_ucred(env), the acting client's credentials,
so it says who acted and not who owns the object, which is what
lfsr_uid means in every scanner that fills it and what lfs find -uid
matches. Delivering it as lfsr_uid would have answered "-uid alice"
from the identity of whoever last touched her file.  The record
carries no size, mode, owner, times, layout or HSM state at any
setting, so those arrive absent with their lfsr_valid bit clear.  With
LLAPI_SCAN_CL_F_RESOLVE they are filled by opening the object through
a client mount, which is one open and one stat each, or two of each
for a regular file whose size was asked for; the demand mask decides
whether that happens at all, so a consumer reading only what an event
carries pays nothing.  Resolution fills what the stream lacks and
never restates what it has, which is what lets a caller ask what an
event recorded rather than what is true now.

Two modes.  Event mode delivers one record per event, in order.
Object mode holds events in a FID-keyed cache and delivers one record
per object once it has been quiet, so a file written a hundred times
is one record and one lookup rather than a hundred of each.

Clearing is off by default and never runs ahead of the callback: it is
irreversible and it purges the registered user's whole backlog, not
this reader's alone.  Registering that user stays the caller's, since
registering without consuming is how an MDT fills up.

The record gains lfsr_event_type, _flags, _time, _index and _prev, the
rename source, and a job id, behind three new validity bits.  An event
time is not an object time, so it does not land in lfsr_mtime: a
search asking for one must not be handed the other.

Only an event that implies a type answers for it -- the three
creations, and CL_RMDIR, whose target is a directory by definition --
so the mode and its validity bit are set together, and a later event
leaves both alone.  Setting them as a pair is what keeps "cannot
answer" apart from "the answer is zero": a mode written on every
event, with the bit only ever OR'd in, would leave a coalesced object
claiming a type while carrying zero for it.  An object's type does not
change, so the first answer stands for every event after it.

lfsr_parent_fd and lfsr_fd are set to -1 rather than left as the
memset found them.  Zero is stdin, and a consumer written against
llapi_scan_namespace() is told to test lfsr_parent_fd and then
fstatat() on it.

The Changelog reader is finished on every path out, the error one
included.  llapi_changelog_start_user() opens the device and sets
*priv before it issues OBD_IOC_CHANGELOG_FILTER, so a failure there --
an unregistered user, an unreachable MDT -- hands back an error with
the reader live, and the fd, its buffer and the kernel-side reader
state would stay pinned for the life of the process if nothing closed
them.

sc_user, sc_mnt and sc_stats are read from the bounded copy rather
than the caller's structure, and the advertised minimum sc_size
reaches through sc_mdtname.  A minimum stopping short of it would let
a caller declare a structure without the field the call cannot work
without, and reading that field off the caller's own memory would go
past its end before the size check had a chance to refuse it.

Clearing stops at the oldest record any cached object still needs, not
at the last index delivered.  Coalescing puts those apart: create B at
10, create A at 11, touch B at 1000, and A can age out and be accepted
while B is still held with a record at 10.  Clearing to 11 there and
then stopping the scan loses B entirely -- no callback saw it, and a
restart from sc_startrec cannot read it back, because the record is
gone.

An event with no target object is delivered per event even in object
mode.  mdd_changelog_ns_store() zeroes cr_tfid when its target is
NULL, which mdd_rename() passes whenever the destination name did not
already exist, and cr_markerflags -- a CL_MARK's union with the same
bytes -- is not a FID either.  Coalescing keys on the FID, so all of
them shared one cache entry and only the last survived: an ordinary
`mv a b` was enough to lose the one before it, and with clearing on,
the record it came from is gone as well.

The parent FID is taken only from an event that carries one.  CL_CLOSE
does not -- mdd_changelog_data_store() is called with pfid NULL -- so
assigning it from every event while only ever OR-ing the bit in left
create+write+close reporting LLAPI_SCAN_PARENT over a zero.  A name or
job id whose copy failed leaves its bit clear for the same reason: a
bit that promises a string must not be set over a NULL.

A resolve is skipped only for a demand mask every event can answer,
which LLAPI_SCAN_TYPE is not: scan_cl_mode() answers it for the four
record types that imply one and no others, so counting it there would
leave a consumer asking for FID and type with neither a lookup nor a
type on a CL_CLOSE.

Resolution opens O_PATH and takes a real open only for the regular
file whose size it came for.  llite gives a special file
init_special_inode(), so opening every object for read opened the
driver behind a device node stored in the filesystem; O_NONBLOCK holds
off the wait, not the rest of what an open does.

The uidgid extension is asked for when sc_want is 0, and that case is
tested on its own rather than by a mask.  Zero means "everything", so
it is the caller that most needs the extension -- and `0 &
~LLAPI_SCAN_EVENT` is 0, so any test of that shape would exclude
precisely the caller that wanted it, leaving LLAPI_SCAN_EVENT_UID out
of lfsr_valid.

A size is reported strict only where the OSTs were actually glimpsed.
The second open runs for a regular file whose demand mask names a
size and can still fail, so a consumer asking for an mtime alone
reaches the fill on the O_PATH descriptor; that is the MDT's lazy
answer, and LLAPI_SCAN_LAZY_SIZE and LLAPI_SCAN_LAZY_BLOCKS are what
say so.  liblustreapi_pfind.c maps the two to OBD_MD_FLSIZE and
OBD_MD_FLLAZYSIZE, so they are not interchangeable.

LLAPI_SCAN_CL_F_RESOLVE is one open and one fstat, so the page says
which six of the ten fields it fills.  The project id, the layout,
the directory stripe and the HSM state each cost an ioctl or an xattr
of their own and are not filled at any setting; the page said
otherwise by listing all ten and then naming the flag.

The checkpoint gets its caveat, in the header and the page both.
Under coalescing a record naming no object goes out as it arrives
while an object whose earliest record sits lower is still held, so
the delivered indexes are not a watermark.  A run that reaches the
end of the log flushes what it holds and the highest index is safe;
a run the consumer stopped drops it, so that run must leave the
checkpoint alone.

sc_startrec and sc_endrec are both inclusive and the page now says
so, the example resuming from the saved index plus one rather than
re-delivering the record the job already processed.  lfsr_event_flags
is cr_flags & CLF_FLAGMASK, so CLF_RENAME, CLF_JOBID and
CLF_EXTRA_FLAGS are masked off -- they say which extensions a record
carries, not what happened.

Three smaller things.  The second fstat() is taken only when the
glimpse replaced the descriptor.  scan_cl_bucket() drops a modulo
that cannot change llapi_fid_hash()'s value, that being hash_long()
on the same shift.  The sc_mdtname test after the memcpy goes: the
minimum sc_size reaches through that field and the caller's own
struct was tested at the top, so it could never fire.  Four comments
described an earlier revision of this patch rather than the code and
now describe the code.

The COALESCE flag says which event's fields the one record carries. It
promised one record per object and left open which of the object's
events that record spoke for, so a consumer matching on lfsr_name had
no way to know it sees one name per object rather than every name the
log recorded -- an object created under one name and hardlinked under
another arrives carrying only the last of them.  lfs find --changelog
-name is that consumer, and the gap showed there first.

sc_type_mask without sc_user is refused.  The mask reaches the server
only through llapi_changelog_start_user(), which needs a registered
user to filter for; on the other branch llapi_changelog_start() never
sees it, so a caller asking for CL_UNLINK alone read every event type
and nothing said so.  Refused rather than applied client-side, because
that is a different thing -- the records still cross the wire -- and
sc_filter is already the client-side spelling.  The man page said only
that the mask is intersected with the user's own, which does not tell
a caller what happens when there is no user.

llapi_scan_changelog_test covers the five things the shape of this
stream turns on: that a record describes the event it came from, that
object mode collapses a burst into one record, that the demand mask
keeps a lookup from happening at all, that a consumer's own return
value comes back unchanged, and that bad parameters are refused rather
than crashed on.  It takes -m for the MDT, -u for a registered user
and -d for a client mount, since three of those need a live log.
Nothing in lustre/tests drives it yet, unlike its two siblings, which
land their suite case with the scanner: that case wants a changelog
registered and cleared around it and a skip where the MDT records
nothing, and is left for its own patch rather than grown here.

sanity 157d runs it, so the binary is not merely built.  Its events
have to be on the MDT whose log it reads, which under DNE a mkdir
below a striped root does not guarantee, so the test makes the
directory on MDT0 itself rather than leaving the binary to notice and
refuse.  -u passes the registered Changelog user the clearing cases
need; without one they skip themselves.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12efd3bd2150592a4507533129ab56573ef924d9
68416 master J !B ✗T –R 0/2
LU-20650 llapi: fill a scan record for one FID
build failure (#131392)
L +781 −10
4h
janitor run: build failed — no tests ran — janitor results
build #131392 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 llapi: fill a scan record for one FID

llapi_scan_namespace() walks what exists, llapi_scan_device() reads
what exists on a target, and llapi_scan_changelog() reads what
happened.  A consumer holding a FID had no way in, so the changelog
module resolved privately and with fstat() alone: mode, nlink, uid,
gid, three times, size and blocks, and nothing else.  A record filled
that way answers -size and -mtime but not -stripe-count, --hsm-state
or -projid, so "lfs find --since -stripe-count 4" would answer
differently from a plain find on the same filesystem.  That is a
defect rather than a difference of provenance: --since promises what
is true now, verified through the mount.  This patch adds the call;
the search moves onto it in "lfs: find --changelog, the log as
source", which is where that example starts answering.

The default demand mask is the walk's, so --hsm-state and -projid
still want naming in lfsp_want; what the call fixes without being
asked is the layout, which is what the example above turns on.

llapi_scan_fid() is the fourth way into the record and fills it the
way the walk does, because it is the walk's own gather.  It resolves
the FID to a pathname and gathers there rather than gathering from a
descriptor opened by FID: scan_rec_gather() is path-oriented
throughout -- get_lmd_info_fd() wants a parent descriptor and a name,
get_projid() opens by path, and the HSM and MDT-index branches open
the path themselves -- so the alternative is an fd-shaped twin of
every one of those, which is the second copy of the knowledge
LU-20611 existed to remove.  Resolution is not an added cost on the
caller that wants this: --since prints pathnames and prefix-matches
them for its subtree restriction, so it needs the pathname anyway.

What that costs is stated rather than buried: per object one fid2path,
one type-only statx and one open of the object or its parent, and
CAP_DAC_READ_SEARCH unless llite.*.user_fid2path is set.  The statx
asks for STATX_TYPE alone deliberately: anything touching size, blocks
or mtime keeps need_glimpse set in ll_getattr_dentry() and buys an OST
glimpse per regular file that the gather then discards.
An object unlinked since the caller learned of it no longer resolves
and comes back -ENOENT, which for a consumer asking what is true now
is the answer and not an error.

The call fills a fresh record and never reads one in.  A consumer
merging it into a record it already holds decides which fields it
lets this one supply, which is what will keep --changelog --resolve
filling only what the stream lacks.

A filter that returns a positive value skips the object, as it does
for the other three scanners, and the call returns 0 rather than
handing that value back to a caller testing `if (rc)`.  The gather
runs against a pathname, so the FID it found is checked against the
one asked for and a name unlinked and recreated underneath it answers
-ESTALE rather than a record for a different object.

Only lfsp_want and lfsp_filter are read from the parameter block.
lfsp_max_depth, lfsp_stats, lfsp_search and lfsp_fsname describe a
traversal there is none of and are ignored; lfsp_thread_count above 1
is refused rather than ignored, because a caller setting it has
misunderstood the call.

It widens a lfsp_want of 0 through LLAPI_SCAN_WANT_DEFAULT, the
definition llapi_scan_namespace() already uses, so the two mount-side
entry points mean the same thing by 0.

A trailing slash on mnt_path is trimmed before the pathname is
composed.  The leading slashes of fid2path's answer were already
stripped, but "/mnt/lustre/" with the root FID composed
"/mnt/lustre/" and left lfsr_name on the terminating NUL -- which
lustreapi.h says means NULL, not empty -- and every other FID got a
doubled separator in the published lfsr_path.

The kernel-doc no longer lists lfsp_flags among the fields that are
ignored.  Every flag it defines is, but a bit outside them is refused
with -EINVAL, as the code does and llapi_scan_fid.3 already says.

llapi_find_device() clears lfsp_got.  scan_param_copyin() copies the
caller's block whole, so the caller's pointer came through and was
answered with find's own demand mask -- a mask for a question the
caller never asked.  The field's kernel-doc is rewritten with it, to
say that it is per scan rather than per library and why it is reached
through a pointer.

The syscalls go through mnt_fd, not through the composed pathname.
The FID was already resolved through that descriptor; the statx, the
object's open and the parent's now use it too, at the name fid2path
answered, so mnt_fd alone says which filesystem is read.  An fd pins
its mount where a path string is resolved afresh every time, and a
mount replaced between the open and the scan would otherwise have the
resolve read one filesystem and the gather another, with rc 0 and
nothing said.  It also takes the mount prefix off every lookup, three
per object, which over a changelog's worth of FIDs is what this entry
point is for.

mnt_fd has to be the mount root now.  The ioctl behind
llapi_fid2path_at() takes any descriptor in the filesystem, and the
page always named llapi_root_path_open(), which returns the root; a
caller that passed something else answered before and answers -ENOENT
now.  mnt_path stays required and stays what lfsr_path is built from,
so a wrong one still mislabels the answer -- it just no longer decides
what was read.

llapi_scan_test gains a case for it, so sanity 157c runs it: one
record for a known FID, checked against the pathname and the basename
the client sees; a filter that skips, which is a success with nothing
delivered; the argument refusals, including the -EBADF that a
negative mnt_fd has to answer rather than the -EINVAL that also means
"this FID has no name"; and a FID that resolves to nothing, which
must fail and deliver no record whichever way it fails.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I796db84d3fe596b1cfa93658dcfeb240db8e33a5
68417 master J !B ✗T –R 0/2
LU-20650 lfs: find --since, from the changelog
build failure (#131393)
1 unresolved XL +995 −8
4h
build failure (#131393)
1 thread(s) waiting on others
janitor run: build failed — no tests ran — janitor results
build #131393 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --since, from the changelog

The three pieces were already here.  llapi_scan_changelog() says which
objects had an event, llapi_scan_fid() fills each one's record the way
a walk fills it, and find_decide() judges it with the predicates a
walk uses.  --since composes them, so it answers over fewer objects
and judges each the same way: the objects are a strict subset,
narrowed and never different.  The lines are not a subset of the
lines -- a walk reaches a hardlinked file once per directory entry
and prints it once per link, while --since reaches it once per FID
and prints it once, under the first of its names that is under the
root and matches.  No new predicates and no second copy of the
vocabulary.

The argument is never guessed.  A bare integer is an index, which is
what "lfs changelog MDT <startrec>" already takes; a single-unit
duration (30m, 2h, 7d) is that far back from now; a timestamp or
@<seconds> is that time; anything else is an error naming the four
spellings.  A leading sign is refused rather than wrapped through
strtoull(), and a duration reaching past the epoch is a typo rather
than a window.

cr_index belongs to one MDT, so a bare index is refused on a
filesystem with more than one and the message says which time
spellings do work.  That count has to be right, and the obvious source
is not: LL_IOC_GETOBDCOUNT answers with the size of the LMV's target
array, so a single-MDT filesystem reports 64.  It is used as an upper
bound and each index confirmed with llapi_search_tgt() before its
changelog is read.

A changelog covers the whole filesystem, so the subtree the caller
named is enforced against the pathname llapi_scan_fid() resolved, and
the depth the walk would have counted is recovered from it for
--maxdepth and --mindepth.  Without that, a find rooted at one
directory would answer for its siblings.

Against every name the object has, not just the resolved one.
llapi_scan_fid() resolves linkno 0, so a hardlinked file arrives under
whichever name fid2path answered with first: with /a/f and /b/f the
same inode, "lfs find /a --since 2h" would be handed b/f, reject it,
and lose a file a walk of /a prints.  -name has the same shape, which
is why a target scan walks every linkea name.  The names come from
fid2path here, one ioctl each, and only a file whose link count says
it has another name pays for any of them.

A time anchor is cut on the record rather than at the start index,
because a time cannot be turned into a per-MDT index without reading
the log; coalescing has already reduced an object to its latest event,
so an object whose latest event is older than the window did not
change in it.  An object unlinked between its event and now cannot be
verified, so it is not in the answer -- that is what the flag gives up
-- and the run says how many were dropped that way.

--since is refused for a target scan rather than ignored.  The option
table is compiled into lfind as well, so `lfind --local --since 2h`
parsed and then scanned the whole target: llapi_find_device() has no
changelog to anchor on and never reads fp_since_kind.  It joins the
list find_device_supported() already keeps for --maxdepth, --threads
and -printf.

llapi_find_since() refuses fp_thread_count above 1 for itself, before
it looks at anything else: this source reads one MDT's changelog at a
time and has no work to divide.  That is why lfs_find() now computes
the default thread count on the walk's arm of the dispatch rather than
above it -- computed for every search, the default alone was above 1
and the refusal fired on every --since run, which is not a setting the
caller made.

The mount point is found from the resolved root and not from the
spelling, which are the same string only when the caller already gave
a canonical absolute path.  /home/user/data -> /mnt/lustre/data
answered -ENODEV, llapi_search_mounts() matching mnt_dir literally,
and a relative path went through the fsname to the *first* mount of
that filesystem -- so under $MOUNT2 every object fell outside the
subtree and the search printed nothing and exited 0.

What is printed is then the caller's spelling of that root with the
resolved remainder appended, because a walk prints the path as it was
given -- param_callback() copies the argument and
llapi_semantic_traverse() appends to it.  Named from the mount every
time, "lfs find . --since 2h" would answer /mnt/lustre/sub/f where
"lfs find ." answers ./sub/f, and --since promises the same answer as
the search without it.

ONLY=56 is the regression check that the shared paths still answer as
they did; the subtests for the new options arrive with the tests
patch at the end of this series.

Two things the page is careful about.  An MDT with no registered user
does not record at all, rather than recording and purging:
mdd_changelog_off() clears CLM_ON when the last user deregisters and
mdd_changelog_enabled() requires it.  And the usage string spells the
argument TIME|INDEX rather than TIME, because a bare integer is a
changelog index -- the one distinction the parser goes out of its way
to make, and the one a reader is most likely to get wrong.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I13426ef1ed12cd08d78aa5a5da9f8ba4465690d2
68418 master J !B ✗T –R 0/2
LU-20650 lfs: find --changelog, the log as source
build failure (#131395)
XL +952 −79
5h
janitor run: build failed — no tests ran — janitor results
build #131395 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --changelog, the log as source

--since treats the changelog as a candidate set and verifies each hit
against the live object, so it answers as it is now.  --changelog
replaces the source: it reads the log and nothing else, and answers as
it was recorded.  That is why -name here matches the name in the event
rather than the object's current one, and why an object unlinked since
the event is in this answer and cannot be in --since's.

It takes an MDT name or "all", required rather than optional because
getopt_long() accepts an optional argument only as --changelog=VALUE.
One named MDT is also what makes a bare --since index unambiguous, the
index being paired with the log it came from.

Every predicate a record cannot answer is refused naming itself,
rather than answered from a zero.  Without --resolve that is -size,
-blocks, -perm, -links, --attrs, --xattr, the layout options, -projid,
-printf, --mdt-count and --mdt-hash; --xattr was the loud one,
reaching llistxattr(NULL, ...) once per object, and the rest were
quiet and worse, "--perm 644" matching nothing and its negation
everything.  -uid and -gid are refused for a different reason and at
every setting short of --resolve: a changelog records who caused an
event, from the acting client's credentials, which is not who owns the
object.  An object time is refused outright, a record carrying an
event time and the two being different questions; the message says
--since 1d is how to ask.  Only --mdt, --maxdepth and --mindepth are
refused at every setting -- they describe a walk, or need something a
per-MDT log does not have.  --ost and the object times are refused
without --resolve alone, the lookup supplying both.

-type is the one field with a third answer.  A record says what kind
of object was created and only for four record types, so an object
whose event does not carry one is counted undecided rather than
compared against a mode of zero, which reads as "not a file" and "not
a directory" at once.

Output is a pathname where the FID still resolves to one and the FID
where it does not, which is the whole point of asking: an object
unlinked since its event is the case --changelog exists for, and
counting it as "no pathname" the way a target scan counts its own
objects would drop exactly that case in silence.  The count of those
is reported at the end, so a caller knows some lines are FIDs.

The path argument names the filesystem, not a subtree, a changelog
knowing nothing about where an object sits; a path below the mount is
refused unless --resolve makes a pathname to match against.

--resolve looks each object up through the mount and hands
find_decide() what it found -- the path, the parent descriptor and the
object's own -- so -size glimpses the OSTs, -projid opens through a
descriptor of ours, and -printf gathers as a walk gathers.  It fills
what the stream lacks and never restates what it has: the merge starts
from the looked-up record and puts every field the event answered for
back over it, taking only the type bits of the mode, so -perm reads
the live one.  The subtree is tested on that pathname, over every name
the object has rather than the one linkno 0 answered with.

An object that has gone between its event and the lookup keeps the
answer honest in the other direction.  It is still in this answer --
resolution fills and never removes -- but only where the record can
carry the question: where the search asks something only a lookup
supplies, or names a subtree the object can no longer be placed in, it
is counted undecided instead.

find_device_supported() refuses --changelog for the same reason it
refuses --since: the option table is compiled into lfind too, so it
parses there, and llapi_find_device() never reads it -- `lfind
--changelog all` would otherwise have run the target scan, printed
everything the other predicates kept, and exited 0.

-name joins -type as a predicate the record cannot always answer.
cr_namelen is 0 for an event on a FID whose name the log never held,
so CL_CLOSE, CL_SETATTR, CL_TRUNC and CL_LAYOUT all
arrive nameless -- a file created before the window and written inside
it is the ordinary case.  Matching those against "" decided them both
ways and said nothing about it, and is not even reliably a non-match,
fnmatch("*", "") succeeding.  They are counted undecided instead.

The first resolve failure that is not "this object has no name"
decides the exit status, as it already does for a target scan, so a
run that could resolve nothing cannot report itself complete.

fc_d is set only for a directory, as find_since_rec_cb() already had
it.  printf_format_lustre() reads d == -1 as "not a directory", and
sr_fd is not always -1 for a regular file: under -printf the gather
widens, scan_rec_gather() opens the object for the MDT index and
leaves that descriptor in sr_fd.  Passing it through sent every
striped file down the directory branch and onto an lmv buffer
find_rec_to_lmv() had just zeroed, so %Lc and %Li printed 0, %Lo
[0] and %Lp nothing.

-name is answered against the recorded name under --changelog.
find_since_pick() walks the object's other links to place it in the
subtree, and was substituting each link's basename into the record
before testing: right for --since, which asks what the object is now,
and wrong here, where the record is the answer.  The alternate path
still settles the subtree; only the name stays as recorded.

The pathname a match prints goes through the caller's spelling
whichever way it was composed.  Without --resolve it comes from
llapi_scan_rec_path(), which builds an absolute name from the mount,
so "lfs find ." answered /mnt/lustre/sub/f there and ./sub/f
everywhere else.  find_since_respell() splits into a find_respell()
taking the two roots, and the print site uses it.

The demand mask -printf widens is the one handed to llapi_scan_fid():
find_want() builds fss_want with the -printf flag set and the resolve
passes it as sp_want.  llapi_scan_changelog()'s own sc_want is a
separate literal and does not depend on -printf.  What the two sources
share is fss_want, which is why --since and --changelog widen
together.
The comment above it claimed %s, %b and %LP as the reason; those come
from find_decide()'s own stat and find_get_projid(), gated on
fc_gather_all, and find_want() clears LLAPI_SCAN_PROJID on its way
out regardless.  What the widening actually buys is LLAPI_SCAN_LMV,
for %Lc and its neighbours on a directory.

The cookie is not mentioned any more.  fp_since_cookie arrives with
--since-cookie in the next patch, and this one documented it in
llapi_find_since()'s Return: block and in llapi_find_since.3 --
an -EINVAL for giving both an anchor and a cookie, and an -ESTALE,
neither of which exists here.  -ENOTSUP does exist and was missing
from both.

A CL_MARK is dropped rather than delivered.
mdd_changelog_write_header() writes the marker flags into the union
with cr_tfid, so cr_tfid.f_seq would read back as CLM_ON|CLM_START,
0x10001 -- and fid_seq_is_igif() accepts anything in [12, 0xffffffff],
so the FID gate would pass a value that is not a FID and find_decide()
would print it as a FID with no pathname.

No such record can arrive today, so this is a guard and not a fix for
an observed answer: CHANGELOG_MINMASK is BIT(CL_MARK) and
mdd_changelog_mask_seq_write() hands it to cfs_str2mask() as the
floor, so CL_MARK cannot be masked out -- and
mdd_changelog_write_header() returns early whenever CL_MARK is in the
current mask, so no marker record is ever written.  Asking for "creat
mkdir" gives back "MARK CREAT MKDIR", and deregistering every user and
re-registering leaves an empty log.  The guard costs one test per
record and stops the flags being read as a FID if that ever changes.

It sits at the top of find_since_cand_cb(), above the FID gate.  The
anchor is not taken there but in find_since_filter_cb(), which runs
first on every record the log delivered, marks included: it has to see
them, or it lags the end of the log by whatever trailing run of them
the scan finished on.

-printf's widening now reaches --since.  find_want() clears
LLAPI_SCAN_PROJID on its way out and find_decide() gates
find_get_projid() and the OST glimpse on fc_gather_all rather than on
the demand mask, so a flag this patch sets for --changelog and not for
--since meant "--since -printf %LP" answered DEFAULT_PROJID for every
object where a walk answered the real one, and %s/%b answered from the
MDT instead of a glimpse.

The comment on the nameless case said cr_namelen is 0 for a MARK.  It
is not: mdd_changelog_write_header() sets it to strlen(obd->obd_name)
and copies the device name in, so a mark arrives named "lustre-
MDT0000" -- which is also why "-name 'lustre-MDT*'" matched one before
the record was dropped above.  A data event on a FID whose name the
log never held is the real case, and the sentence now says only that.

The undecided warning no longer claims a missing field.
find_since_cand_cb() also counts an object undecided when it has gone
and the search needed a lookup or named a subtree, and there the
record answered -- the object is what is missing.  "could not be
decided from their changelog record" covers both.

LLAPI_SCAN_CL_F_ONCE is set for --since and not for --changelog.  Once
means once per scan, which is what "as it is now" wants; a source
answering as it was recorded has a second burst to report, so an
object touched in two of them prints twice.  The man page says so.

The dropped-objects warning names what the count is of -- "%llu of
%llu changed objects could not be read" -- rather than leaving a bare
number beside the errno it belongs to.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: If9ead118b5fc1dee86cfac6646106e982841d8e2
68419 master J !B ✗T –R 0/2
LU-20650 lfs: find --since-cookie, per-MDT anchor
build failure (#131394)
L +708 −36
5h
janitor run: build failed — no tests ran — janitor results
build #131394 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 lfs: find --since-cookie, per-MDT anchor

cr_index belongs to one MDT, so "where I got to" on a DNE filesystem
is one number per MDT and not one number.  --since <time> is the human
spelling and means the same thing everywhere, but only as far as the
clocks agree; a cookie is exact on every MDT, and is what a repeated
job should use.

The file is a line per MDT, "<mdtname> <index>", read as the anchors
and rewritten with the indexes this run reached.  It is written whole
and renamed over, so an interrupted run leaves the previous anchors
rather than half of the new ones -- a half-written cookie is worse
than a stale one, because the stale one is caught.

Which is the reason a cookie is worth having.  If an MDT has purged
past its anchor the records in between are gone, and reading from the
oldest one left would return a short answer with nothing to say so.
So the oldest surviving index is read first, by a one-record scan that
stops at the first callback, and an anchor older than it is refused
naming both numbers.  Every MDT is checked before any of them is
scanned: refusing from inside the scan would leave the earlier MDTs'
matches on stdout with no cookie written, and the next run would
repeat them.  An MDT with no anchor is not probed at all -- 0 has
nothing to be stale against, and that is every MDT of a first run.

Zero is reserved for "the log is empty", which is why a record that
arrives without an index is reported rather than recorded as index 0.
Changelog indexes start at 1, so storing 0 for a record that has one
would make a log holding records look like a log holding none, and 0
compares false against every anchor -- the check would go on being
skipped and say nothing about it.  An empty log does proceed: from a
client, "nothing happened since the anchor" and "everything since it
was purged" look the same, and refusing on that would fail on every
idle filesystem.

A cookie from another filesystem would anchor this one at indexes that
mean nothing here, so a line whose name does not match the filesystem
is ignored, and a file whose header names another filesystem is
refused.  Ignoring every one of its lines instead would leave no
anchors at all, rescan this filesystem's whole log, and then drop the
other one's anchors in the rewrite -- three wrong answers, none of
them said out loud.

The rest of the file is input the caller does not control either.  The
scanf width is not optional: %s into a fixed buffer is how such a file
overruns it.  Nor is the type of the bound test, since %x accepts a
sign: "<fsname>-MDT-1" reads back as 0xffffffff, which passes a test
made against an int and then indexes the array far past its end.  So
the sign is refused at the parse and the bound is compared against the
value that does the indexing.

--since and --since-cookie are two ways to say where to start, so
giving both is refused rather than one of them silently winning.  More
than one path is refused for a related reason: lfs find runs the
search once per path and every run advances the anchors, so the second
path started past the end of the log, printed nothing, and left the
records it should have matched behind the anchor for good.

The cookie is proved writable in the same pre-pass that proves the
anchors are not stale, and for the same reason -- a cookie that cannot
be written leaves the matches on stdout and the next run repeating
them.  A missing directory component reads as ENOENT, which
find_cookie_read() takes for a first run, so the typo was not noticed
until every log had been scanned.

The temporary file is fsync()ed before the rename, and the directory
after it.  The rename makes the replacement atomic against another
process, which is not the same as durable: after a host crash the
rename can be on disk while the contents are not, and an empty cookie
parses as no anchors at all.

The pre-pass probes the temporary name the write will use rather than
the cookie itself, because the write creates "<cookie>.new" and
renames over: what it needs is permission on the directory, which
opening an existing cookie for append does not test.  A cookie owned
by the job user in a root-owned directory passed and then failed
EACCES with every match already printed.

The anchors are left where they were when a run did not finish.  A
resolve failure that is not "this object has no name" drops matches,
and writing the cookie anyway would advance past the very records a
rerun needs, so the exit status is settled before the rewrite rather
than after it.

An index is read with strtoull() and a leading sign refused, the MDT
number having been refused for the same reason: "%llu" negates, so
"-2" would anchor a run at 0xfffffffffffffffe, match nothing, and
write that value back.

find_device_supported() refuses --since-cookie along with --since:
the option table is compiled into lfind too, so it parses there, and
llapi_find_device() never reads it -- a target scan would have
printed every object the other predicates kept and exited 0, without
even reading or writing the cookie it was given.

ONLY=56 is the regression check that the shared paths still answer as
they did; --since-cookie's own subtests are in the tests patch at the
end of this series.

The cookie names the search as well as the filesystem, and a
mismatch is refused the way a wrong fsname already was.  The anchors
say how far each log was read, not how far a subtree was: a record is
counted before the FID and the subtree are tested, so a run over one
subtree carries every anchor to the end of the log and a second
subtree sharing the cookie starts past its own changes and prints
nothing.  That is the collision lfs_find() refuses when two paths are
given to one command, reached by running the command twice.  A cookie
written before this carries no root and is still accepted.

A cookie that exists and is not a regular file is refused up front.
The probe settles the containing directory, not the name: the write
ends in rename(), which answers EISDIR over a directory, while the
read succeeds and returns no anchors -- so naming a directory scanned
every log to the end, printed every match, and only then said it
could not rewrite.  An empty name is refused with it.

The comment on the file format said a comment or a blank line costs
nothing.  It costs the read nothing; the rewrite emits the header and
the anchors and nothing else, so it does not survive the run.  Said
so, the sentence otherwise reading as an invitation to annotate the
file.

The documentation for fp_since_cookie arrives here rather than in the
previous patch, which described the field, its -EINVAL and its
-ESTALE before any of them existed.

The root is the rest of the header line rather than a field.  A
pathname can hold a space and %s cuts it at the first one, so a search
under "/mnt/lustre/my data" was recorded whole and read back as
"/mnt/lustre/my": the comparison then failed for every run after the
first, and the message named a path the caller never gave.  The root
is whatever realpath() returned for the caller's path, so a directory
with a space in it was enough to reach it.

And the root is escaped rather than written raw, because the rest of a
line still cannot hold a newline.  Only '/' and NUL are excluded from
a name, so "/mnt/lustre/anb" was written whole, read back as
"/mnt/lustre/a", and refused for every run after the first naming a
path the caller never gave -- the space case one character further
out, with the remainder becoming its own line and being dropped.  Both
sides escape and the reader compares the escaped forms, so nothing
decodes: the mapping only has to be injective for a comparison, which
is also why backslash is escaped.  The refusal names both sides in
that spelling, or a newline would break the line meant to name it.

find_cookie_check()'s two other refusals say so as well.  An empty
--since-cookie and a name too long to append ".new" to both returned
the errno alone, beside a "is not a regular file" case that reports
itself, so the run ended at the caller's "failed for '<path>': Invalid
argument" with nothing naming the argument at fault.

Two diagnostics that this patch makes reachable are corrected with it.
The refusal of --resolve without --changelog said "--since already
reads the object", which names an option the caller need not have
given once --since-cookie exists; it names both anchored spellings
now.  And a cookie refused for its filesystem or its root was reported
twice, the second line saying "cannot read" over a file that read
perfectly well -- find_cookie_read() has already named which refusal
it was, so the wrapper is left with what fopen() passed up.

The MDT number is exactly the four hex digits find_cookie_write()
emits.  %4x converts the leading digits and ignores what follows, so
"<fsname>-MDT00001" and "<fsname>-MDT0000_UUID" both anchored MDT0000
at an index never written for it -- the short answer with nothing said
that the stale check exists to catch, from a file this function's own
comment says it ignores anything but "<mdtname> <index>" in.  The span
refuses the sign as well, so the separate test for it goes.

ERRORS gains -ESTALE, which the previous patch dropped on the
understanding it would arrive with the cookie and which the kernel-doc
Return: block has listed since.  It is the error the whole option is
built around, and the man page was the one place a caller of the API
could not find it.

Test-Parameters: testlist=sanity env=ONLY=56
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ia8ce93368a2c2c8c2ea2ee88c8e7f7e099b1bfa5
68420 master J !B ✗T –R 0/2
LU-20650 tests: sanity cases for find's changelog flags
build failure (#131396)
1 unresolved L +478 −42
5h
build failure (#131396)
1 thread(s) waiting on others
janitor run: build failed — no tests ran — janitor results
build #131396 FAILURE
patchsets:
+5 earlier PS1 08-26 PS2 08-28 PS3 08-29 PS4 08-29 PS5 08-30
PS6 08-31 PS7 09-01 PS8 09-03 PS9 09-04 PS10 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20650 tests: sanity cases for find's changelog flags

lfs-find.1 gains --since, --since-cookie, --changelog and --resolve:
what each answers, that --since is a strict subset of the same search
without it, that a bare index belongs to one MDT and is refused where
that is ambiguous, that a number which could be an index or an epoch
second is never guessed at, and that reading a changelog neither
consumes it nor needs a registered user while records exist only while
some user is registered.  TIME's spellings are given in full, since
set_since() takes eight of them and its error text offers one the page
would otherwise not list.

The page says what --resolve does not answer for -- an object gone
between its event and the lookup is counted undecided and dropped, so
--resolve can leave out an object the same search without it reports
by FID -- and which name -name is tested against.  The log is
coalesced to one record per object before the search sees it, so the
name that can match is the one the object's latest event to carry one
carried: a file created as f and hardlinked as g is matched by "-name
g" alone, and the pathname printed is its first name rather than the
one that matched.  A plain walk meets each name in turn and matches
all of them.  The behaviour is deliberate -- substituting a current
name would answer -name against something the record never said -- but
a reader had no way to know it.

Four sanity cases.  160aa asserts the promise --since is worth having
-- that its answer is a subset of the same find without it, and a
smaller one -- rather than asserting a particular file, so it does not
depend on what else the filesystem is doing.  It names the files it
expects rather than only counting them, since a count that merely
shrank is also satisfied by returning nothing, and it widens the gap
around its window so a node that spends seconds in sync does not
change what the test means.  Its locals stay separate from the
assignments that fill them, and it sorts off the pipeline rather than
in it: "local x=$(...)" takes local's own exit status, which is always
0, and a command piped into sort reports sort's, so either shape would
let a failing lfs find read as the empty answer the index case
asserts.

160ab walks the refusals: -size, an object time, --maxdepth, a path
below the mount, an unknown MDT, -perm, -links, --mdt-count,
--mdt-hash and --xattr are each refused, --resolve on its own is
refused rather than dropped, and a named MDT is accepted as the other
half of --changelog's argument.  It asserts the --resolve answer
rather than the exit status alone: -size needs a strict size, the MDT
holds only the lazy one, and a resolution that did not glimpse left
every object "could not be decided" while still exiting 0.  It also
asserts -printf's %s, which reads the same size through a widened
gather, that --projid is answered rather than failing on the first
record, and that a subtree search under --resolve answers for that
subtree and not the whole filesystem.

160ab covers the two things that make --changelog worth having over
--since, which the man page leads with: an object unlinked since its
event is still in the answer, and its FID names it once no pathname
does.  It ends on -name, which is answered from the record: the log is
cleared, one file that predates the clear is written to -- data
stores, which carry no name -- and another is created.  The named one
has to come back and the nameless one must not, so a -name that
matched everything fails the second assertion and one that matched
nothing fails the first.

160ac runs the cookie three times: the first writes it, the second
returns the new object and not the old one, the third returns nothing,
and then a stale anchor and both anchors together are refused.  It
asks for creations only.  A wide mask records the test's own reads,
and then "nothing changed since" is never true -- which is what hid an
off-by-one in the cookie until a narrow mask made every run repeat the
last object of the one before it.

A test that asserts a precondition instead of creating one only holds
where that precondition happens to be true.  160ac's stale anchor is
made rather than assumed -- records the cookie has not consumed, then
a purge, then one more past the hole -- because writing index 1 is
only older than the oldest surviving record on a filesystem that
already has changelog history.  On a fresh one index 1 IS the oldest
record, nothing was purged, and refusing would be wrong.

160ac also feeds the cookie two files it is never written with: one
whose MDT index is negative, and one whose header names another
filesystem.  Neither is a bad anchor to ignore -- the first indexes
the anchor array far past its end, and the second is a whole-log
rescan of this filesystem plus the loss of the other one's anchors --
so one has to be ignored and the other refused, and the file it
refuses has to still be there afterwards.

160ad hardlinks one file into two directories and searches under each.
A changelog names an object by FID, and fid2path answers with one of
its names -- the first linkea entry, the one the file was created
under -- so a search rooted at the other directory is answered about a
name that is not under it, and -name about a basename the object does
not go by there.  Without the links being walked it finds the file
under a and loses it under b, which is a silent miss and the worst
shape a wrong answer can take.

The tag is tests: rather than doc: because that is where the bulk of
the change is; the man page rides along because the options it
describes are the ones these cases exercise.

There is no version gate.  Nothing in this series touches server code
-- it is lustreapi.h, lustre/utils/ and Documentation/ -- so the
options work against any server old enough to have changelogs, and a
gate on the MDS would skip interop runs that pass.  A client gate
would say nothing either: this script ships with the client whose lfs
has the options.  The third Test-Parameters line below is that claim
under test rather than only asserted: an old server, a new client, and
these four cases.

Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad
Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad mdscount=2 mdtcount=4
Test-Parameters: testlist=sanity env=ONLY=160aa,160ab,160ac,160ad serverversion=2.15.6
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Id4001c6821e72f15b83a9733bbb28cef4248b822
68726 master J ✗2B ✓T …R 0/2
LU-20603 llapi: pull a scan's records in batches
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
2 unique failures XL +1257 −1
5h
janitor: 2 test failures unique to this patch — sanity2@ldiskfs+DNE:test_157d, sanity2@zfs:test_157d
build OK — testing in progress (typically 6–14h)
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity2@ldiskfs+DNE:test_157dNEW unique failure for this branch in the last 30 days, and was seen 0 times across 0 other branches 0 reviews
sanity2@zfs:test_157dseen in 1 other review
build #131397 SUCCESS · tests all Maloo sessions · test queue
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS1 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20603 llapi: pull a scan's records in batches

Add llapi_scan_namespace_open(), llapi_scan_next() and
llapi_scan_close(): the namespace scan turned around, so a consumer
pulls records in batches on its own thread instead of being called
once per object on the scan's.  A layer over llapi_scan_namespace()
and not a second scanner -- the scan runs on a thread of its own, its
callback copies each record into the batch being filled, and
llapi_scan_next() hands out a full batch while the scan fills the
other.

The copy is what a batch costs.  A callback record points into
buffers the scan reuses for the next object, so a record that
outlives its callback has to own what it points at: the path, the
name, the layout, the directory stripe, the linkea, the rename source
and the jobid are copied into an arena the batch owns and the
pointers rewritten.  The descriptors are not carried and read -1.
Two batches suffice, the scan blocking when the one it fills is full,
so what is in flight is bounded by the batch size and not by the
tree.  The caller sees an array of record pointers rather than of
records, so its stride is a pointer's and a record that grows does
not move its neighbours under a caller built against the older size.

A filter in lfsp_filter runs on the scan's threads with the caller's
data, which the layer keeps beside its own.  An error is reported
after every record produced before it has been handed out, so the
batches before it are whole; a close on a running scan stops it, and
that is not an error.

A handle belongs to one consumer thread, and that is enforced rather
than only stated: llapi_scan_next() and llapi_scan_close() refuse a
second caller with -EBUSY.  Without it a second consumer resets the
batch the first is still reading and installs one whose sb_count is
already sb_cap, and the next append writes past the end of sb_recs --
reproduced under ASan on the unfixed build, five runs out of five, and
gone on the fixed one, which serves every object once.  It is the
opposite of the callback API's rule, where the callback runs on every
scan thread at once, so it is said in the header and the man page too.

The arena keeps every chunk across a reset rather than the largest.
A batch needing two chunks -- about 320 bytes of payload per record at
the 1024 default, which a path plus an 8-stripe layout reaches --
otherwise malloc'd and freed the second on every batch, which is what
the comment said it did not do.

Measured on a 20k-object tree off Lustre, where per-object work is a
cached lstat() and the copy is at its largest share: a batch of 1024
costs 76 ns per object over the callback at 4 threads (+8%), 135 ns
at one (+5%); a batch of 1 costs 2-4 us, the lock round trip per
record.  On a scan whose per-object work is an ioctl the same copy is
a fraction of a percent, which the man page says.

test16 rides along: it asks llapi_scan_namespace() what lfsp_got
reports, which is the callback API and not this one, and belongs with
whichever change lands lfsp_got.

llapi_scan_next.3 says when a batch stops being the consumer's, not
only that it does.  The release happens at the top of the following
llapi_scan_next(), before that call waits for the scan rather than
when it returns, so a consumer that hands a batch to another thread
and calls again to overlap the next fill has released what that thread
is reading -- and the -EBUSY that refuses a second caller does not
refuse a reader racing one.  The arena is rewound rather than freed,
which is what makes a steady-state scan allocate nothing per batch, so
that reader finds the following batch's bytes and not a fault.

The -EBUSY case is made rather than waited for.  A second consumer is
refused only while the first is inside llapi_scan_next(), and a test
that calls from between two of them never reaches the guard it is
about -- the ASan overflow above cannot fail it.  A filter that sleeps
holds the scan back so the third call blocks; two calls come first,
the layer filling one batch ahead and the consumer not blocking until
it has drained that.  The overlap is then measured, not assumed: a
machine slow enough to put the second call outside the window makes
the case unreachable, and an unreachable case is reported rather than
failed.

Test-Parameters: trivial testlist=sanity env=ONLY="56El 157c"
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I29aaa31a4c0bd34ca591316601a653f9b5ae70c2
67840 master J ✓B ✓T ✓R 1/2−
LU-20564 utils: probe md RAID geometry for OST mkfs/mount
needs rebase — checkpatch: cannot be cherry-picked
L +342 −0
2d
needs rebase — checkpatch: cannot be cherry-picked
no vote yet from Alex Zhuravlev, Li Dongyang
reviews: Andreas Dilger +1 2026-08-11 19:33
no vote yet: Alex Zhuravlev, Li Dongyang
patchsets: PS1 08-07 PS2 08-07
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS2 uploaded 32d ago · NEW · open in Gerrit ↗
commit message
LU-20564 utils: probe md RAID geometry for OST mkfs/mount

mkfs.lustre now probes the md array underneath an OST target (through
stacked dm devices) and seeds -E stride/stripe_width so mballoc can
align and size allocations to full stripes.  mke2fs can derive these
from the blkid topology, but that is absent on some stacked
configurations.  raid0, raid4/5/6, raid10 and raidkm (level 71) are
decoded; the replication factor of raid10 and the declustered g/m of
raidkm come from the md layout word.  User-supplied
stride/stripe_width values are respected, misaligned partition starts
draw a warning at mkfs time, and mount.lustre now warns when an OST on
a striped array has no (or mismatched) fs geometry.  A stripe_width
that is a multiple of the array row is still stripe-aligned and is not
reported.

mkfs.lustre also warns when the array's data row (k * chunk) is not a
power of two.  Such a row cannot be written whole by any client: bulk
RPC sizes are powers of two in practice, so a row divides some RPC
size if and only if the row is itself a power of two.  A geometry like
k=5 at 64K (320KiB) or k=14 at 64K (896KiB) leaves a partial row at
the tail of every RPC, which the array services as a read-modify-
write, at every chunk size and with no tuning available.  Unlike
stride/stripe_width this cannot be corrected afterwards -- it is a
property of the array -- so the warning is issued at mkfs time, while
the geometry can still be chosen, and is not suppressed by a
user-supplied stride/stripe_width.

Tested on RHEL 9.7 against live loop-backed md arrays: raid0/4/5/6,
raid10 near, far and offset layouts, a non-power-of-two row,
user-supplied values, dm-linear over one array and spanning two with
conflicting geometry, and aligned and misaligned partition starts.
Also tested against live raidkm (md level 71) arrays: plain m=2 and
m=3, parity-last, and declustered g=6/m=2 and g=8/m=3 over wider
pools.  In every case the computed row matches md's own
queue/optimal_io_size.

Test-Parameters: trivial
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: Claude:Fable-5 claude-code
Assisted-by: Claude:Opus-5 claude-code
Change-Id: I442528cea51a55c6b74f7f2282a1db566eab9783
67783 master J !B ✓T ✓R 1/2−
LU-20542 gss: retry callback RPC when reverse ctx is stale
needs rebase — checkpatch: cannot be cherry-picked
M +87 −3
2d
needs rebase — checkpatch: cannot be cherry-picked
no vote yet from Arshad Hussain, Aurelien Degremont, Bruno Faccini, Andreas Dilger
janitor run: 5 failing config(s), none unique to this patch — janitor results
10 optional test failure(s) — informational
build #129017 SUCCESS · tests all Maloo sessions
reviews: Sebastien Buisson +1 2026-08-10 09:49
no vote yet: Arshad Hussain, Aurelien Degremont, Bruno Faccini, Andreas Dilger
patchsets: PS1 08-05 PS2 08-09 PS3 08-10
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 30d ago · NEW · open in Gerrit ↗
commit message
LU-20542 gss: retry callback RPC when reverse ctx is stale

A server cannot renegotiate a reverse GSS context; one is only
installed from a peer's forward request. So when the peer drops its
contexts, the next callback is answered with GSS_S_NO_CONTEXT,
sptlrpc_req_replace_dead_ctx() returns -ECONNREFUSED, and
ldlm_handle_ast_error() evicts a client that was only mid-refresh.
This is the sanity-sec test_27d/27e failure.

Resend the callback a bounded number of times instead, marked with
rq_gss_rvs_resend so it skips the context refresh in
ptlrpc_check_set() and reaches the peer over the same dead context.
Every other resend still refreshes, so a reverse context that goes
stale on its own is still detected and replaced. Blocking and
completion ASTs stay bounded by rq_no_resend and the lock callback
timeout; for a glimpse AST, which sets neither, the resend count and
its backoff are the bound.

Fixes: 67acf6047e34 ("LU-17317 gss: do not continue using expired reverse context")
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I2a007b42656eb9aa0bed00291b67b6ac7db2d389
68414 master J !B ✓T ✗1R 0/2
LU-20648 mdc: fix changelog mask composition
enforced failed: review-dne-selinux-ssk-part-2
M +107 −8
3d
janitor run: 5 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130806 SUCCESS · tests all Maloo sessions
patchsets:
+1 earlier PS1 08-26
PS2 08-28 PS3 08-29 PS4 08-30 PS5 08-31 PS6 09-01
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS6 uploaded 8d ago · NEW · open in Gerrit ↗
commit message
LU-20648 mdc: fix changelog mask composition

A zero cf_mask means two different things and chlg_ioctl() composed
them as if it meant one.  On the server it means "this user has no
mask of its own": mdd_chlg_usermask() returns 0 for a record that
carries none.  On the client it means "do not filter": a zero
crs_user_mask skips the record type test altogether.

So "lfs changelog --user NAME --mask creat" for a user who has no
mask computed in.cf_mask & 0, which is 0, which the record test then
read as unfiltered.  The caller got every record type back and no
error, which is a wrong answer rather than a failure.

Compose the two zeros as what they mean.  A user with no mask of its
own is not restricted to nothing, it is unrestricted, so a caller's
mask passes through unchanged; only two real masks intersect.

Registration writes a rec2 when either a mask or a name was given.
It fills cur_mask when a mask was given, and otherwise only while the
server's changelog_mask is still minimal, where a maskless user is
handed CHANGELOG_DEFMASK instead.  So the reachable case today is a
registration with a name and no mask made while the server mask is
set to anything wider.  A user registered with neither name nor mask
gets the older record type, whose mask reads as zero whatever was
written, and is affected too once LU-20647 makes such a user
reachable by lookup at all.

Two real masks that do not overlap intersect to zero as well, and
zero cannot carry "nothing matches" while it also means "do not
filter".  Two absolute masks cannot reach it: cfs_str2mask() seeds an
absolute mask from its minmask argument, and both the registration and
"lfs changelog --mask" pass CHANGELOG_MINMASK, so "-m creat" is really
MARK|CREAT and "--mask mkdir" is MARK|MKDIR -- always sharing
BIT(CL_MARK).  A relative one does.  llapi_convert_str2mask() seeds
from *oldmask when the first token carries an operator, and
lfs_changelog() seeds that with CHANGELOG_DEFMASK, so "--mask
=-mark,-creat" is DEFMASK without those two: non-zero, and sharing no
bit with a user registered "-m creat".  So the wrong answer is
reachable from the shell, and against such a user "lfs changelog
--user clN --mask=-mark,-creat" printed every record it had.

So crs_user_mask now names the record types to deliver: it starts as
all bits, an empty intersection is no bits, and the record test is a
plain bit test with no special case for zero.

test_160z asserts that the user carries no mask before going on,
rather than reasoning that it must: changelog_register() sets +hsm
before registering, and mdd_changelog_mask_seq_write() rebases a
relative mask onto CHANGELOG_DEFMASK while the proc mask is still
minimal, so the branch that hands a maskless user a default one
cannot fire there.  The test widens the mask so that CREAT and MKDIR
are recorded whatever an earlier subtest left it at, and only on
$SINGLEMDS, which is the facet its trap restores.  It asks for --mask
creat and requires the MKDIR record to be absent, then asks without
--mask and requires it to be present.  The second half is what keeps
the fix from turning "no mask" into "no records".

160z covers the disjoint case in its second half, with a user
registered "-m creat" read back through "--mask=-mark,-creat".  An
earlier attempt used two absolute masks and was removed after a
control run showed it passing against the unfixed module as well; this
one cannot, the composition there being zero and zero meaning "do not
filter".  The control beside it is the same user read without --mask,
so a run that selects nothing because the user has nothing cannot be
mistaken for the fix working.

Fixes: 41b55cf2309d ("LU-19296 changelog: Add user-specific changelog filtering")
Test-Parameters: testlist=sanity env=ONLY=160z
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib178e4c15be40c415262bf06c08da8b061855cf9
68413 master J ✗1B ✓T ✗2R 0/2
LU-20647 mdd: look up a changelog user of either record type
janitor: 1 test failure unique to this patch — sanity-quota@zfs:test_48
1 unique failure M +106 −6
3d
janitor: 1 test failure unique to this patch — sanity-quota@zfs:test_48
enforced failed: review-dne-part-5, review-dne-selinux-ssk-part-2
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
sanity-quota@zfs:test_48seen in 2 other reviews
failed enforced testplatformdetail
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: sanityn. session
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130807 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger
patchsets:
+1 earlier PS1 08-26
PS2 08-28 PS3 08-29 PS4 08-30 PS5 08-31 PS6 09-01
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS6 uploaded 8d ago · NEW · open in Gerrit ↗
commit message
LU-20647 mdd: look up a changelog user of either record type

mdd_changelog_user_register() writes CHANGELOG_USER_REC when the
caller gave neither a mask nor a name, deliberately, for
compatibility.  mdd_changelog_user_lookup_cb() matched
CHANGELOG_USER_REC2 only, so such a user was never found and
mdd_changelog_user_lookup() returned -ENOENT.  That is the shipped
"lfs changelog --user", which reaches the callback through
OBD_IOC_CHANGELOG_FILTER, not only the library call.

Every other MDD walker already takes both types and guards the fields
that exist in only one of them, so the callback was alone in
rejecting the record rather than the field.

The type test cannot simply be dropped.  struct
llog_changelog_user_rec is half the size of rec2 and carries neither
cur_mask nor cur_name, and the || short-circuited before strcmp()
reached cur_name; removing the first clause would read past the end
of the record into whatever the llog buffer holds next.  So a lookup
by ID matches on cur_id alone, a lookup by name skips an old record
instead of comparing against it, and the reply's username is filled
only from a rec2.  The reply's mask already came from
mdd_chlg_usermask(), which returns 0 for the old type, and a zero
mask is what the client reads as "do not filter".

The reply's username comes from the record's type and not from how the
lookup was made.  mdd_iocontrol() passes one buffer as both request
and reply, so cf_user_id holds the record's ID by the time the old
"was this a lookup by ID" test read it -- true on every lookup, and
harmless only because a name lookup had matched that same name.

The name comparison is bounded as well.  cur_name comes off disk with
no guarantee of a NUL inside its 16 bytes, and
mdd_changelog_name_check_cb() already allows for that on the same
field.

test_160w covers this command but registers both of its users with a
name and a mask, so both records are rec2 and the plain path was
never walked.  test_160y registers one user plainly, one with a mask
and one with a name, and asks for the records of each; the masked user
is the control that has always worked.  The named user is coverage
rather than a regression case: the old callback rejected a
CHANGELOG_USER_REC in the first clause of its ||, before strcmp() ran,
so a lookup by name walked past a plain record before this patch as it
does after it.  Only the cf_user_id != 0 path changes behaviour, and
the client parses "cl<N>" into an ID, so that is the path lfs takes.

This wants LU-20648 beside it.  Making a plain user reachable makes
cf_mask == 0 the ordinary answer, and a client without that fix
composes in.cf_mask & 0 and reads the zero as "do not filter": so
"lfs changelog --user cl1 --mask creat" on a plain user would print
every record type where it used to stop at -ENOENT.  Landing this one
alone trades a lookup failure for a wrong answer.

Fixes: 5b85a4eb7510 ("LU-19296 changelog: retrive changelog user info from MDT")
Test-Parameters: testlist=sanity env=ONLY=160y
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I12ef76c773ce31e4548db9022f381cabfe7f5ddc
68340 master J !B ✓T ✗4R 0/2
LU-20643 utils: clear stale lmd fields on reuse
enforced failed: review-dne-part-2, review-dne-part-4, review-dne-part-5, review-dne-selinux-ssk-part-2
S +18 −2
3d
janitor run: 7 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-part-2 RHEL 9.7 / x86_64 ran 11 tests. 1 tests failed: sanity-lfsck. session
review-dne-part-4 RHEL 9.7 / x86_64 ran 9 tests. 2 tests failed: sanity-quota, sanity-dom. session
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: recovery-small. session
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 7 tests. 1 tests failed: sanity-sec. session
build #130562 SUCCESS · tests all Maloo sessions
patchsets: PS1 08-25 PS2 08-28 PS3 08-28
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 11d ago · NEW · open in Gerrit ↗
commit message
LU-20643 utils: clear stale lmd fields on reuse

get_lmd_info_fd() reuses one struct lov_user_mds_data for every object
and convert_lmd_statx() writes only the fields lstat(2) supplies, so
stx_attributes, stx_attributes_mask, stx_btime and the mask bits above
STATX_BASIC_STATS keep whatever was there before.

Two ways that is not this object's data.  On the V1 ioctl path the
lstat_t is copied over the front of the record, so stx_attributes_mask
ends up holding st_atim.tv_sec -- never 0 in practice -- and stx_mask
picks up bits from st_nlink, which reads as STATX_BTIME on a directory
whose link count has 0x800 set.  It does not reach as far as it looks:
lmd_st is 144 bytes against 272 for lmd_fid and lmd_stx, so it stops
at the end of stx_mtime and stx_rdev_major onwards is still the
previous conversion's.  On the lstat fallback the buffer holds the
previous object's data outright, or the file name written in for the
ioctl.

So a walk that crosses onto a non-Lustre subtree can answer -btime and
the attribute predicates from a file name.

Clear what is about to be reused on both paths, with the same line:
everything up to lmd_lmm, which is what the record's own fields amount
to.  That takes lmd_fid, lmd_flags, lmd_lmmsize and lmd_padding with
it -- nothing in lustre/utils reads lmd_fid today, but a short name
there passes fid_is_sane() as an IGIF, and llapi_get_lum_file_fd()
copies by lmd_lmmsize.  On the V1 path it goes after the memmove, so
the layout it has just moved up is not touched.  The glimpse path is
left alone: there the MDT's answer is still wanted.

Fixes: 11aa7f8704c4 ("LU-11367 som: integrate LSOM with lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6126e73bb120bd6a12a904563e13303d63e75c1c
64945 master J !B ✓T ✗2R 1/2−
LU-20050 ptlrpc: fix import ref leak on sepol failure
enforced failed: review-dne-zfs-part-1, review-dne-zfs-part-7
2 unresolved XS +7 −1
6d
enforced failed: review-dne-zfs-part-1, review-dne-zfs-part-7
Sebastien Buisson voted -1
2 unresolved thread(s) await your reply — last from Gerrit AI review for Lustre
janitor run: 3 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-zfs-part-1 RHEL 9.7 / x86_64 ran 3 tests. 1 tests failed: sanity. session
review-dne-zfs-part-7 RHEL 9.7 / x86_64 ran 6 tests. 1 tests failed: sanity-ec. session
build #130839 SUCCESS · tests all Maloo sessions
reviews: Sebastien Buisson -1 2026-09-02 13:56 · Andreas Dilger +1 2026-09-02 13:56
no vote yet: Colin Faber, wangdi
patchsets: PS1 03-27 PS2 03-27 PS3 03-27 PS4 09-02
threads: 2 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS4 2026-09-03 08:01 Gerrit AI review for Lustre COMMIT_MSG:30 — (minor) This does not seem to hold, and it argues against the Fixes: tag just below it.
Gerrit AI review for Lustre PS4 · 2026-09-03 08:01
(minor) This does not seem to hold, and it argues against the Fixes: tag just below it.

sptlrpc_sepol_get() already returned genuine error pointers before 8a6788b508db:

    if (imp_sec == NULL)
        RETURN(ERR_PTR(-EINVAL));
    ...
    out = sptlrpc_sepol_get_cached(imp_sec);
    if (!out)
        RETURN(ERR_PTR(-ENODATA));

sepol_helper() also returns `ret >> 8` of call_usermodehelper(), so when the helper cannot be exec'd at all (missing /usr/sbin/l_getsepol gives -ENOENT) rc comes out as -1, which IS_ERR() accepts.

What 8a6788b508db changed is the l_getsepol-exits-nonzero case, where the status was positive. Stating that the leak is only reachable after it may keep the fix from being picked up on branches that predate it. Would it be more accurate to say 8a6788b508db made the path much easier to hit?
reply PS4 2026-09-03 08:01 Gerrit AI review for Lustre import.c:770 — (suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:32
Gerrit AI review for Lustre PS4 · 2026-09-03 08:01
(suggestion) The other sptlrpc_sepol_get() callers have the same sequence: mdc_request.c:373, mdc_reint.c:199/345/409/487 and mdc_locks.c:326/481/545 all jump straight from the IS_ERR() branch to err_free_rq, which calls ptlrpc_request_free(), so they leak the import reference and the imp_reqs count in the same way.

That is pre-existing and this patch does not have to carry it. It is not a one-line swap either, since err_free_rq is shared with the post-pack failures reached through err_put_sepol and those must keep ptlrpc_request_free(). Is a follow-on change under the same ticket planned for them?
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS4 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20050 ptlrpc: fix import ref leak on sepol failure

ptlrpc_connect_import_locked() calls ptlrpc_request_alloc(), which
takes a reference on the import via class_import_get(). When
sptlrpc_sepol_get() fails, the IS_ERR() branch calls
ptlrpc_request_free(). That only returns the request to its slab
and does not drop the import reference.

The leaked reference keeps client_obd_cleanup() from running, so
the LDLM namespace kobject is left in sysfs and the next mount
fails with -EEXIST.

Use ptlrpc_req_put() on this path instead. It drops rq_refcount to
zero and calls __ptlrpc_free_req(), which releases the import.

The second failure path, after ptlrpc_request_bufs_pack() returns
an error, still uses ptlrpc_request_free(). That is correct: the
out_free label inside ptlrpc_request_bufs_pack() has already
dropped the import reference, so only the slab is left to free.
Both paths now carry a comment saying which case they are.

This bug is only reachable after 8a6788b508db, which stopped a
positive errno from reaching ERR_PTR() and so made the IS_ERR()
branch above actually trigger.

Fixes: dd200e5530fd ("LU-8955 ptlrpc: manage SELinux policy info at connect time")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia4267f87219e2a7e4fbcb996978543174bcb7197
68582 master J !B ✓T ✗2R 0/2
LU-20546 ptlrpc: fix import ref on request setup failures
enforced failed: review-dne-zfs-part-4, review-dne-zfs-part-7
3 unresolved XS +5 −2
6d
enforced failed: review-dne-zfs-part-4, review-dne-zfs-part-7
3 unresolved thread(s) await your reply — last from Gerrit AI review for Lustre
janitor run: 2 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-zfs-part-4 RHEL 9.7 / x86_64 ran 9 tests. 1 tests failed: sanity-quota. session
review-dne-zfs-part-7 RHEL 9.7 / x86_64 ran 6 tests. 1 tests failed: sanity-ec. session
build #130838 SUCCESS · tests all Maloo sessions
threads: 3 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre COMMIT_MSG:25 — (minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(minor) Does this second Fixes: tag point at the right commit? 8e86156c34f3 added ptlrpc_req_put() to osp_sync_new_unlink_job() and osp_sync_new_unlink64_job() on paths that run *after* a successful pack, where dropping rq_import is correct. The ptlrpc_req_put() on the pack error in osp_sync_new_job() itself goes back to 1d371ca47e67 ("LU-1303 osp: OSP logging functionality"), and the class_import_put() at out_free that leaves rq_import set is older still.

So the double release predates 8e86156c34f3. Either drop this tag or point it at the commit that actually introduced the imbalance.
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre client.c:933 — (minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(minor) The comment promises more than the function delivers. out_ctx falls through to here after sptlrpc_cli_ctx_put(request->rq_cli_ctx, 1) without clearing rq_cli_ctx, so a caller that uses ptlrpc_req_put() reaches sptlrpc_req_put_ctx() in __ptlrpc_free_req() and drops the same context reference a second time.

That is exactly gss_cli_ctx_fini_rpc(): it passes a non-NULL ctx, so out_free is unreachable for it and out_ctx is its only failure exit, and it disposes of the request with ptlrpc_req_put(). It is pre-existing rather than introduced here, but the commit message says clearing rq_import "lets callers use either free or put", which isn't true while the ctx is still dangling. Would sptlrpc_req_put_ctx(request, 1) at out_ctx (it does the put and NULLs the field) make the claim hold?
reply PS1 2026-09-03 06:12 Gerrit AI review for Lustre client.c:1051 — (defect) Is ptlrpc_req_put() safe here for a request that came from the pool?
Gerrit AI review for Lustre PS1 · 2026-09-03 06:12
(defect) Is ptlrpc_req_put() safe here for a request that came from the pool?

__ptlrpc_request_alloc() falls back to ptlrpc_prep_req_from_pool() when the slab allocation fails, and that request keeps the pool's preallocated rq_reqbuf while rq_cli_ctx is still NULL (nothing has packed yet). __ptlrpc_free_req() then does:

    if (request->rq_reqbuf || request->rq_clrbuf)
            sptlrpc_cli_free_reqbuf(request);

and sptlrpc_cli_free_reqbuf() starts with LASSERT(ctx) on req->rq_cli_ctx, so this looks like an LBUG rather than a clean free.

The path seems reachable: osc_brw_prep_request() uses ptlrpc_request_alloc_pool() for OST_WRITE, so under memory pressure the pool fallback is taken; if the OSC import is idle at that point, ptlrpc_reconnect_if_idle() -> ptlrpc_connect_import_locked() itself does ptlrpc_request_alloc() and returns -ENOMEM under the same pressure. The old code went through ptlrpc_request_free(), which just returned the request to the pool.

The request also has no capsule yet (req_capsule_init() runs below), so it is not really in a state ptlrpc_req_put() expects. Would keeping the explicit atomic_dec()/class_import_put() plus ptlrpc_request_free(), or clearing rq_import and then freeing, be safer than routing this through the full teardown?
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS1 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20546 ptlrpc: fix import ref on request setup failures

ptlrpc_request_alloc() takes a reference on the import. Two
setup paths get its release wrong, in opposite directions.

ptlrpc_request_alloc_internal() open-codes atomic_dec() on
imp_reqs and then calls ptlrpc_request_free() when an idle
reconnect fails, so class_import_put() is never reached and the
reference leaks. Call ptlrpc_req_put() instead, which does both.

ptlrpc_request_bufs_pack() has the reverse problem: out_free
drops the reference but leaves rq_import set, so a caller that
disposes of the failed request with ptlrpc_req_put() releases it
a second time. gss_cli_ctx_fini_rpc() and osp_sync_new_job() both
do that on a pack error. Clear rq_import once it has been
dropped, which also lets callers use either free or put.

Fixes: 93d20d171c20 ("LU-11128 ptlrpc: new request vs disconnect race")
Fixes: 8e86156c34f3 ("LU-10486 osp: fix request leak on error in osp_sync")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Iec07b093e380f262b452ff4fc5f5fac64f1c9fb0
65026 master J !B ✓T ✗0R 0/2
LU-20050 ptlrpc: fix sepol_helper status decode
Maloo voted -1 — tests failed (details on the change)
M +203 −15
6d
janitor run: 3 failing config(s), none unique to this patch — janitor results
no vote yet: Andreas Dilger, Sebastien Buisson
patchsets:
+5 earlier PS1 04-01 PS2 04-20 PS3 07-22 PS4 07-29 PS5 07-30
PS6 07-31 PS7 08-03 PS8 09-02 PS9 09-02 PS10 09-02
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 7d ago · NEW · open in Gerrit ↗
commit message
LU-20050 ptlrpc: fix sepol_helper status decode

sepol_helper() declared ret as signed short while
call_usermodehelper() returns int, and decoded it with a single
"rc = ret>>8". That shift is only correct for a normal exit. A
kernel launch failure returns a negative errno, not a wait
status, and shifting collapses it (-ENOMEM becomes -EPERM). A
helper killed by a signal leaves the signal in the low 7 bits,
so ret>>8 is 0 and the failure is reported as success.

Widen ret to int and decode the three cases separately, with raw
bit tests since WIFEXITED/WEXITSTATUS are userspace macros:

  ret < 0            ->  kernel failed to launch the helper
  (ret & 0x7f) == 0  ->  normal exit (WIFEXITED)
  (ret >> 8) & 0xff  ->  exit code   (WEXITSTATUS)

The exit code must be sign-extended, not negated: l_getsepol
returns -errno whenever stdin is not a tty, always the case
under call_usermodehelper(), and exit() keeps only the low byte.
So -ENODEV arrives as exit byte 237, and (signed char)237 is
-19. sptlrpc_sepol_get() special-cases -ENODEV as "SELinux is
disabled, report nothing", so negating it made that branch
unreachable and turned those mounts into "Unknown error 237".
This extends 8a6788b508db.

A positive exit status is not an errno either, so negating it
invented plausible but wrong errors: 127 became EKEYEXPIRED and
126 ENOKEY, pointing at key management rather than a broken
helper. Report -EPROTO and log the raw status. sepol_helper()
can then no longer return a positive value, so drop the
unreachable rc > 0 case in sptlrpc_sepol_get().

The eight sptlrpc_sepol_get() error paths in mdc_locks.c,
mdc_reint.c and mdc_request.c leaked the request's import
reference and the LDLM locks gathered for early cancel: the
sepol check runs before the prep call, so the request still owns
both, but the error tail only called ptlrpc_request_free(). Keep
one exit per function and make it correct on both sides of the
prep call, through the new mdc_elc_req_put() helper, clearing
@count once the prep call owns the locks. This needs LU-20546,
which clears rq_import when packing fails so the reference is
dropped exactly once.

Add sanity-selinux test_21d: stub l_getsepol, mount with
send_sepol=-1, and check that exit 237 still mounts while 234
and a missing helper fail with the expected errno text. It
creates no nodemap on purpose -- with nodemap.<nm>.sepol set,
req_check_sepol() would refuse the -ENODEV connect for an
unrelated reason.

Suggested-by: Sebastien Buisson <sbuisson@ddn.com>

Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-1
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Fixes: c61168239eff ("LU-8955 sec: create new function sptlrpc_get_sepol()")
Fixes: 0a773f04b288 ("LU-8955 ptlrpc: manage SELinux policy info for metadata ops")
Assisted-by: Claude:Opus-5 claude-code
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: If9312fce842fca133de21c4898cafd949ace6aa6
67052 master J !B ✓T ✗0R 0/2
LU-20119 dbg: test lustre_helper labeling fix for test_21a
Maloo voted -1 — tests failed (details on the change)
fortestonly S +32 −0
27d
janitor run: 2 failing config(s), none unique to this patch — janitor results
build #127246 SUCCESS · tests all Maloo sessions
patchsets: PS1 07-04 PS2 07-04 PS3 07-06
tags: fortestonly
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 65d ago · NEW · open in Gerrit ↗
commit message
LU-20119 dbg: test lustre_helper labeling fix for test_21a

fortestonly patch validating the fix for the sanity-selinux
test_21a failure on the DNE + RHEL 8.10 server + RHEL 10.0 client
config, where the client mount fails because l_getsepol returns
-EACCES (-25 before the errno fix below) from the sepol connect
upcall.

Root cause: the l_getsepol/lctl/l_getidentity/l_getauth helpers
were left labeled bin_t instead of lustre_helper_exec_t, so the
kernel execs them into the unprivileged kernel_generic_helper_t
domain (via the base "kernel_t bin_t:process
kernel_generic_helper_t" transition) rather than lustre_helper_t.
That domain cannot read selinuxfs or the on-disk SELinux policy,
so l_getsepol fails with EACCES.  Two reasons the label was never
applied:

1. lustre.spec.in %post loaded the policy module with "semodule
   -i" but never ran restorecon.  semodule -i does not relabel
   already-installed files, and the binaries were labeled bin_t at
   install time (before the module existed).  Add a restorecon of
   the four helpers to %post (the make-install path already does
   this, but it is skipped for staged/RPM builds where DESTDIR is
   set).

2. lustre_helpers.fc labeled only /usr/bin/*.  On RHEL/Rocky 10.2+
   the /usr/sbin=/usr/bin file-context equivalency makes that
   work, but on el8/el9 and el10.0/10.1 /usr/sbin is a real path
   with no such equivalency, so restorecon of the /usr/sbin binary
   finds no rule.  List both /usr/bin and /usr/sbin.

The sepol_helper() CERROR and the errlog() errno fix from the
prior diagnostic patch are retained so that, if the mount still
fails, the kernel log shows l_getsepol's real exit code.

Not intended to land; validates the fix before it goes on the
LU-20119 review.

Test-Parameters: fortestonly
Test-Parameters: livedebug mdscount=2 mdtcount=4 osscount=1 ostcount=8 clientcount=2 testlist=sanity-selinux clientdistro=el10.0 serverdistro=el8.10 env=ONLY="21a",SHARED_KEY=true clientselinux
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ifd207da02862849c90500a40b4ff71dff5bc711b
65848 master J !B ✓T ✗2R 0/2
LU-20238 ec: branch-free byte-indexed scalar base path
enforced failed: review-dne-part-2, review-dne-part-5
pt_ecro XL +503 −4660
33d
janitor run: 1 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-part-2 RHEL 9.7 / x86_64 ran 11 tests. 1 tests failed: sanity-lfsck. session
review-dne-part-5 RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: lustre-rsync-test. session
build #128825 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger, Patrick Farrell
patchsets:
+5 earlier PS1 05-13 PS2 06-08 PS3 06-17 PS4 07-10 PS5 07-10
PS6 07-30 PS7 07-31 PS8 08-04 PS9 08-05 PS10 08-06
tags: pt_ecro
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS10 uploaded 34d ago · NEW · open in Gerrit ↗
commit message
LU-20238 ec: branch-free byte-indexed scalar base path

Replace the per-byte gf_mul() in the scalar *_base hot paths with
a byte-indexed per-coefficient region table:

  tbl[b] = c * b in GF(2^8), built once per coefficient
  gf_region_mul(tbl, x) = tbl[x]   (single load, inner loop)

ec_encode_data_base() at k=6/p=2, gcc -O2: 0.79x at 256 B, 1.08x
at 1 KiB, 1.32x at 64 KiB. gf_mul() alone is ~20% slower than the
64 KiB direct lookup it replaces, so regions below
GF_REGION_TBL_MIN_LEN (1 KiB) keep a scalar fallback.

Byte-indexed lookup (gf-nishida-16 "FullByte"-style) is also
patent-clean: no PSHUFB, no nibble split, no 16-byte gftbl, so it
is structurally distinct from the ISA-L SIMD pattern that maps
onto US 8,683,296 claim 21.

ec_base.c carried an unconditional "#define GF_LARGE_TABLES", so
both ec.ko and libec.a compiled the 64 KiB gf_mul_table_base and
the small-table branch was dead code. With that define and the
inert ec_base_o_CPPFLAGS in ec/Makefile both gone nothing could
set it, so the block is deleted outright: .rodata 65792 -> 1536,
.text 2217 -> 3591 for the fallbacks.

gf_vect_mul_init is renamed gf_vect_mul_init_base, with a wrapper
in ec_base_aliases.c; its 64-bit path now selects on BITS_PER_LONG
for kernel builds, since __WORDSIZE is glibc-only. No existing
entry point changes behaviour.

Validated on RHEL 10.1 (AVX2+GFNI): exhaustive gf_mul + gf_inv,
k=6/p=2 round-trip KAT, and gfni_ec_test byte-identical parity.

Test-Parameters: testlist=sanity-ec
Test-Parameters: testlist=sanity env=ONLY=910
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I905e365d145d0018cbcd2bbc919eb4e516fafe8e
65388 master J !B ✓T ✗1R 0/2
LU-20147 ptlrpc: module-level cache for SELinux policy upcall
enforced failed: review-dne-selinux-ssk-part-2
stalled 131d XL +693 −368
131d
janitor run: 3 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-selinux-ssk-part-2 RHEL 8.10 / x86_64 ran 5 tests. 1 tests failed: sanity-selinux. session
6 optional test failure(s) — informational
build #124437 SUCCESS · tests all Maloo sessions
no vote yet: Andreas Dilger, Sebastien Buisson
patchsets: PS1 04-16 PS2 04-29
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS2 uploaded 133d ago · NEW · open in Gerrit ↗
commit message
LU-20147 ptlrpc: module-level cache for SELinux policy upcall

Replace the per-import call_usermodehelper model with a single
module-level upcall_cache (hashsize=1, key=0) shared by all imports.

The original LU-8955 design called l_getsepol via
call_usermodehelper() on every RPC from every import independently.
On RHEL/Rocky 10.1 this fails: the kernel_generic_helper_t SELinux
domain is blocked from reading /sys/fs/selinux/policyvers and
writing to debugfs, causing a 30-second timeout on every first RPC.

Cache lifecycle and freshness:
- upcall_cache.h: add struct sepol_cache_data and union member in
  upcall_cache_entry.
- lustre_sec.h: remove ps_sepol and ps_sepol_checknext from
  struct ptlrpc_sec.
- sec.c: add sptlrpc_sepol_cache (module-level upcall_cache),
  sptlrpc_sepol_get(), sptlrpc_sepol_do_upcall() (fires l_getsepol),
  sptlrpc_sepol_parse_downcall(), and sptlrpc_sepol_cache_update()
  for unsolicited push updates.  send_sepol changed from static to
  extern.
- The cache is initialised with entry_expire = S32_MAX so the
  framework never reaps entries by time; sptlrpc_sepol_get() drives
  freshness by stat()ing the policy directory on every call and
  calling upcall_cache_flush_one() if its mtime differs from the
  value carried by the last downcall.  l_getsepol therefore runs
  only on mount or after an actual policy change.
- send_sepol == 0 disables the feature entirely (no policy info on
  any RPC).  Any non-zero value behaves identically; the legacy
  "send_sepol = N seconds" semantic is preserved as parameter-level
  backward compatibility but no longer affects runtime behaviour
  (with the cache in place, the LU-8955 reason for an N-second TTL
  doesn't apply).

Backward-compatible wire format and invocation:
- lustre_user.h: keep the pre-LU-20147 layout as
  struct sepol_downcall_data_v2 / SEPOL_DOWNCALL_MAGIC_V2 = 0x8b8bb843
  alongside the new struct sepol_downcall_data /
  SEPOL_DOWNCALL_MAGIC = 0x8b8bb844.
- sec_lproc.c sptlrpc_sepol_do_downcall() and sec.c
  sptlrpc_sepol_parse_downcall() accept both magics; only the new
  layout populates the policy-dir mtime tracking used by the
  freshness check.
- l_getsepol gained -2 / --new-format: with -2 it writes the new
  layout, without -2 it writes the v2 layout.  Default is v2 so a
  new userspace stays compatible with an old kernel.
- sec.c sptlrpc_sepol_do_upcall() keeps the pre-LU-20147 argv shape
  "--obd_type X --obd_name Y" (so old l_getsepol still recognises
  it) and just appends "-2" as a new flag.  X/Y are the type and
  name of any active OBD on the client (MGC preferred); the per-OBD
  srpc_sepol write handler funnels the result back through
  sptlrpc_sepol_do_downcall() into the module-level cache, so it
  doesn't matter which OBD is used.  We invoke under UMH_WAIT_PROC
  and on non-zero exit (eg. old userspace that doesn't recognise
  "-2") retry without "-2".
- sec_lproc.c: add module-level srpc_sepol write-only debugfs file
  at sptlrpc/srpc_sepol (LDEBUGFS_FOPS_WR_ONLY) so admin tools can
  feed the cache directly; per-OBD srpc_sepol kept for backward
  compatibility and used by the kernel upcall above.

l_getsepol changes:
- Replace file-scope static globals with per-call lgetsepol_ctx.
- Add -k/--kernel flag: writes downcall to module-level path
  sptlrpc/srpc_sepol; includes policy dir path and mtime.
- Add -2/--new-format flag (see above).
- Add -w/--watch flag: inotify daemon mode for policy change
  detection (retained as alternative; community feedback welcome).
- Add find_policy_with_highest_ver() helper used by the existing
  RHEL/Rocky 10.1 fix in LU-20045's stack; the helper itself is
  duplicated locally so this patch is self-contained against an
  older LU-20045 base.  When LU-20045 lands on master this duplicate
  can be dropped.
- Use fstatat(dirfd(dp), ...) in the policy version scan loop.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Icf813c252b7359c3dc17d80405579b50b57ab025
65120 master J !B ✓T ✓R 2/2
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini()
Verified -1 veto by Oleg Drokin
stalled 143d S +12 −0
143d
janitor run: 4 failing config(s), none unique to this patch — janitor results
build #123727 SUCCESS · tests all Maloo sessions
reviews: Andreas Dilger +1 2026-04-10 22:58 · Alex Zhuravlev +1 2026-04-11 05:38
no vote yet: Oleg Drokin
patchsets: PS1 04-06 PS2 04-06 PS3 04-10 PS4 04-10 Andreas Dilger
owner Hiroshi Nishida · uploader Andreas Dilger · PS4 uploaded 151d ago · NEW · open in Gerrit ↗
commit message
LU-20084 osp: fix LDLM ns kobject leak in lwp_device_fini()

The LWP teardown path does not call ldlm_namespace_free_prior()
before client_obd_cleanup() -> ldlm_namespace_free_post().
This affects two paths:

Normal teardown (LCFG_CLEANUP -> lwp_process_config ->
lwp_disconnect -> lwp_device_fini): lwp_disconnect() calls
ptlrpc_disconnect_import() and ptlrpc_invalidate_import() but
never calls client_disconnect_export(), so
ldlm_namespace_free_prior() is never reached.  lwp_device_fini()
then calls class_disconnect() and client_obd_cleanup() directly,
skipping free_prior entirely.

Failed-connect path (lwp_obd_connect failure -> out_dis:
class_disconnect -> lpd_exp=NULL -> lwp_device_fini): the same
lwp_device_fini() is reached with lpd_exp NULL, so
class_disconnect is skipped and client_obd_cleanup() is again
called without free_prior.

Without ldlm_namespace_free_prior(), LDLM_NS_STOPPING is never
set.  If the pool recalc task runs concurrently with
ldlm_namespace_free_post(), the race can corrupt the pool kobject
cleanup path and cause ldlm_namespace_sysfs_unregister() to hang,
stranding the namespace kobject under
/sys/fs/lustre/ldlm/namespaces/.

Fix by calling ldlm_namespace_free_prior() in lwp_device_fini()
before client_obd_cleanup().  This is symmetric with lwp_setup()
-> client_obd_setup() where the namespace is created, and covers
all LWP teardown paths.  The import is still valid at this point
since obd_cleanup_client_import() runs inside
client_obd_cleanup() afterwards.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ie9a290ca529d18b0f837669a53c0c06cda4f767f

Review feedback — your turn

(1)
68156 master J ✓B ✓T …R 0/2
LU-20606 llapi: scan an ldiskfs target directly
1 unresolved thread(s) await your reply — last from Andreas Dilger
10 unresolved XL +3136 −34
4h
1 unresolved thread(s) await your reply — last from Andreas Dilger
build OK — testing in progress (typically 6–14h)
9 thread(s) waiting on others
build #131384 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-23 PS7 08-24 PS8 08-25 PS9 08-26 PS10 08-28 PS11 08-29 PS12 08-29 PS13 08-30
PS14 08-31 PS15 09-01 PS16 09-03 PS17 09-04 PS18 09-09
threads: 1 need your reply · 9 waiting on others · 0 own notes · 0 bot
reply PS9 2026-09-05 08:53 Andreas Dilger COMMIT_MSG:22 — API changes are not critical within the development releases.
Gerrit AI review for Lustre PS9 · 2026-08-27 20:07
(minor) This paragraph lists what the record grew, but two other pieces of new public API aren't accounted for anywhere in the body: `struct llapi_scan_stats` with the `sp_stats` parameter field, and the `LLAPI_SCAN_F_INTERNAL` flag.  Both are things a caller has to know about, and the counters in particular are described in the man page as part of the answer rather than a diagnostic.  Worth a sentence each so the diff holds no surprises.
Hiroshi Nishida PS17 · 2026-09-04 22:04
Done. Held back through several rounds as prose-only, but you are right that new public API is not prose. The message now names `struct llapi_scan_stats` with `sp_stats` and says what LLAPI_SCAN_F_INTERNAL is for: the objects a target holds that its namespace never shows -- the OSD's own, the DNE agent inodes, the ones with no LMA -- which a scan otherwise counts in ss_class and keeps back. Checked against `enum llapi_scan_class` and the gate in liblustreapi_scan_device.c rather than written from the flag's name.
Andreas Dilger PS9 · 2026-09-05 08:53
API changes are not critical within the development releases.
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20606 llapi: scan an ldiskfs target directly

Add llapi_scan_device(), which reads an MDT's or OST's objects
straight off the device with libext2fs and delivers the same records
llapi_scan_namespace() does.  No mount, no MDS, no kernel change: it
runs against a stopped target, a snapshot or a failover partner's LUN,
read-only and with no write path.

libext2fs stays off liblustreapi.  The backend builds as
scan_osd_ldiskfs.so and is dlopen'ed on first use, named and found the
way mount.lustre's own mount_osd_ldiskfs.so is, so a client build has
no plugin and the call answers ENOTSUP.  Three refusals get three
answers, because a caller keys off them: ENOTSUP where there is no
backend to ask, ENOPKG where the backend is there and refused the
target -- stock e2fsprogs will not open a filesystem with dirdata set,
and the package to install is the answer -- and EINVAL for a device
that is not a Lustre target at all.  Giving the first two the same
errno would have conf-sanity test_300 skip itself on a node carrying a
real MDT and a build that can scan it.

The record grows what a device answers for and a walk does not: the
object id, the inode generation, the LMA flags, the raw linkea and a
class.  The label is read the four ways mkfs.lustre writes it -- '-',
':', '=' and the '+' that tunefs.lustre --nolocallogs leaves on disk
while the target is stopped, which is exactly when this is what there
is to read it with.

Two more pieces of public API, so the diff holds no surprises. struct
llapi_scan_stats is what the new lfsp_stats points at: the caller sets
ss_size and owns the storage, the scan fills the rest on return, and
the man page describes it as part of the answer rather than as a
diagnostic.  And LLAPI_SCAN_F_INTERNAL asks for the objects a target
holds that its namespace never shows -- the OSD's own, the DNE agent
inodes, the ones with no LMA -- which a scan otherwise counts in
ss_class and keeps back.  Without it "every object on this device" and
"every object a walk would find" would be the same question, and only
one of them is what a target scan is for.

The counters are one accounting rather than several: ss_seen is
incremented once per object, in the pre-filter, so a skip the backend
raises after that point adds to ss_skipped alone.  ss_seen equals
ss_filtered plus ss_skipped plus the sum of ss_class on any completed
scan, and llapi_scan_device_test asserts it -- an inequality would not
have caught counting the same object on both sides of the pre-filter.

An object with no LMA is answered with its IGIF rather than with the
parts to build one from.  The inode number and generation of such an
object are its FID -- osd_scrub.c builds the same one, and it is not a
reconstruction, which is LFSCK's job -- so lfsr_fid carries it and
LLAPI_SCAN_FID is set.  That leaves the generation with no consumer of
its own and it is not a field: what remains is lfsr_ino, which every
object has and which names the ones no IGIF can, a ZFS object id being
wider than an IGIF holds.  There is no LLAPI_SCAN_GEN bit for it.

llapi_scan_namespace.3 gains the pointer to this page here rather than
in the patch that wrote it, where it would have referred to a page the
tree did not yet have.

A worker that cannot be initialised degrades the scan rather than
failing it, once one worker exists, as the pthread_create() arm beside
it does: a backend opens the device again per worker,
lfsp_thread_count is a __u8, and the cursor feeds whatever workers
there are, so running out of them is not a reason to turn a complete
result into an error.

The page says what stx_attributes carries across scanners.  A device
scan narrows the inode's own flags to the set llite also declares, so
one file answers "lfs find -attrs d" the same way whichever scanner
ran, and so_attrs_mask declares that set in the record's
stx_attributes_mask -- which is what lets a consumer tell "not
immutable" from "this scanner cannot say".  ERRORS also spells out the
two EINVAL cases a caller is most likely to trip, a short lfsp_size
and a short ss_size.

A directory's own stripe takes LMV_MAGIC_V1.  LMV_USER_MAGIC is how
the tree marks a *default* LMV: cb_get_dirstripe() sets it exactly
when fp_get_default_lmv is asked for and LMV_MAGIC_V1 otherwise, and a
default lives in trusted.dmv, which is never read here -- trusted.lmv
is the directory's actual stripe.  llite fills LMV_MAGIC_V1 for
LL_IOC_LMV_GETSTRIPE, so LMV_USER_MAGIC here would have the two
scanners answer one striped directory with two different values in the
same field -- the opposite of what lfsr_lmv promises, which is that
the field means one thing whichever scanner filled it.

No in-tree consumer reaches that difference from a scan record yet, so
this is a contract broken rather than a wrong answer observed.
lmv_dump_user_lmm() is what it would break: the magic decides its
"(Default)" prefix and which fields a bare -v shows, and
llapi_lov_dump_user_lmm() accepts both values, so a caller pointed at
a scan record instead of an ioctl reply would print a device scan's
directories as defaults.  The size is unchanged either way,
lmv_user_md_size() growing only for LMV_USER_MAGIC_SPECIFIC.

A parent FID the scan will not vouch for is left cleared rather than
written: LLAPI_SCAN_PARENT is set only where fid_is_sane() accepts
what trusted.link held, and the field follows the bit, so a caller
reading it without testing lfsr_valid does not get the raw xattr.

llapi_scan_device.3 gives lfsr_parent_fid an entry of its own, since
LLAPI_SCAN_PARENT is both what a caller puts in lfsp_want and what it
has to test in lfsr_valid, and a field named in neither place is a
field a caller cannot use.

run_tests() in lustre/tests/llapi_test_utils.{c,h} is split so that
run_test_tbl() takes the table: the new test is handed a device where
the others take a mountpoint.  llapi_scan_test, llapi_root_test and
llapi_pool_test share that helper, which is why the split is worth
naming here rather than left to be read out of the diff.

so_flags is narrowed to the attributes a namespace scan can also
report.  ll_dir_ioctl() puts only IMMUTABLE, APPEND and, under
HAVE_LUSTRE_CRYPTO, ENCRYPTED into stx_attributes_mask, and
llapi_scan_namespace() takes stx_attributes from that mask -- so
reporting COMPR and NODUMP here would make one file answer "lfs find
--attrs d" differently depending on which scanner ran, where
stx_attributes is meant to mean one thing whichever filled it.  The
bit is on the MDT inode either way: making it answerable is llite's to
do, for both scanners at once.

so_attrs_mask carries that same set into the record's
stx_attributes_mask, rather than the scanner assuming one set for
every backend: a bit clear there is "cannot say", which is a different
answer from the attribute itself being clear.  It is carved from
so_padding, so every offset above it is unchanged, and a backend that
leaves it zero has its records leave LLAPI_SCAN_ATTRS clear rather
than claim attributes nothing vouches for.

scan_lmv_to_user() clears the shard area, not just the header.  One
buffer serves every object a worker meets, and a directory's converted
LMV carries the real lum_stripe_count while the size answered is
header-only -- the shard FIDs being left out on purpose, an MDT index
needing an FLD lookup this has no client to make.  A consumer sizing
lum_objects[] by the count rather than by lfsr_lmvsize, which
lmv_dump_user_lmm() does, then read whatever the last object left
there: a preceding foreign directory's opaque value, in full.  The
clear now reaches as far as such a read can, bounded by the room
before a count off a device measures anything, so what it finds is
zeroes.  lfsr_lmvsize is the measure either way, and the header says
so.

The buffer is a union of the two structures it holds rather than a
bare char[], which carries neither one's alignment while the call site
casts to both.

An unreadable inode is counted by the chunk that owns it and no
other.  The skip arm continued without asking either question the
readable path asks below it, so an inode just past end_ino was
consumed by this chunk and read again by the chunk starting there,
and ss_seen and ss_skipped each took it twice; a reserved inode that
could not be read was counted as an object, where a readable one is
not.  ext2fs_get_next_inode_full() assigns *ino before returning any
of the three errors this arm catches -- unlike the errors above it,
which leave it untouched -- so both questions can be asked here.
Nothing was ever delivered twice: it was the accounting that
overstated, which is what the counters exist to be trusted for.

fid_is_root() moves to the UAPI header and the scan calls it.  It was
in lustre_fid.h by history rather than by need: LU_ROOT_FID and
lu_fid_eq() are both already public, so the helper is a one-line
predicate over two things userspace has, and userspace now has the
same question to ask -- a scan of a target has to tell the root from
the rest of FID_SEQ_ROOT.  unlikely() is dropped, that header being
compiled in userspace too, and the definition is removed from
lustre_fid.h, which includes the UAPI one, so every existing caller in
llite, lmv, lod, mdd and mdt is unaffected.

That also settles a test that had drifted.  lu_fid_eq() is a
whole-struct compare, so a hand-written test of f_seq and f_oid called
a FID in FID_SEQ_ROOT with FID_OID_ROOT and a non-zero f_ver the root,
where fid_is_namespace_visible() on the MDT would not.  The narrower
point the spelling exists for is unchanged: the echo client's root
shares the sequence and is still not visible.

Both scanners fill lfsp_stats.  The structure arrives with this patch,
which is why the walk did not fill it before -- the field came in with
the code that needed it, not with the walk -- and there is no reason
for it to stay that way: a walk counts an object seen once, then
filtered, skipped, or delivered and namespace-visible, which is every
class a walk can meet, and the same seen == filtered + skipped +
sum(class) holds.  It counts atomically rather than per-worker,
llapi_scan_namespace() running its callback on every thread at once
where the device scan merges per-worker counters at the end.
lfsp_stats is checked against LLAPI_SCAN_STATS_MIN_SIZE there too, and
after the copy-in for the same reason: a short lfsp_size may not reach
the field.

The struct comment says what the field is rather than which scanners
use it.  llapi_scan_device.3 also says the stats structure is
extensible -- ss_size negotiates it exactly as lfsp_size does, so
counters appended later leave an older caller reading its own fields
-- which was true and written down nowhere.

The struct comments describe the fields rather than which scanner uses
them.  What each scanner offers lfsp_filter, and what each takes 0 in
lfsp_want to mean, were spelled out in the header and again on both
pages, and the header's copy is the one that goes stale: it claimed a
device scan "offers no path and no name", which LU-20637 makes untrue
two patches later by naming an OST object from the parent FID in
trusted.fid.  What is left is the part that does not change -- the
filter sees only what the scan already had, and 0 means a default the
two scanners do not share -- with each page saying which fields that
is for itself.  llapi_scan_device.3 gains the one sentence it was
missing, what 0 means there.

stack_trap is called without EXIT, which is its default, and the
setupall after stopall carries no justification -- it is the ordinary
shape and needs none.  The base tree passes an explicit EXIT eighteen
times of ninety-two; this series had added thirteen more, so dropping
ours leaves the file at the eighteen that were already there.

The EXAMPLES fragment escapes its newline with a single backslash.
troff reads that as a register reference, so the page rendered
printf("%llu files; %llu seen, %llu skipped0, with the escape and the
closing quote swallowed.  It needs the doubled form that
llapi_fid_parse.3 and llapi_scan_rec_path.3 already use.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I8a28a8173b0b8f03ba8f9dc5a58a821118ac6296

In CI

(9)
68163 master J ✓B ✓T …R 0/2
LU-20613 llapi: ZFS backend for llapi_scan_device
build OK — testing in progress (typically 6–14h)
XL +1323 −95
3h
build #131389 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+12 earlier PS1 08-20 PS2 08-20 PS3 08-20 PS4 08-20 PS5 08-23 PS6 08-24 PS7 08-25 PS8 08-26 PS9 08-28 PS10 08-29 PS11 08-29 PS12 08-30
PS13 08-31 PS14 09-01 PS15 09-03 PS16 09-04 PS17 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS17 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20613 llapi: ZFS backend for llapi_scan_device

Add scan_osd_zfs.so, the ZFS half of llapi_scan_device(), which reads
objects out of a pool through libzpool the way zdb does, behind the
same llapi_scan_sink the ldiskfs backend is behind.

A pool that is still imported answers EBUSY rather than being read
underneath its server: reading an imported pool from outside is not
supported, so scan a snapshot, an exported pool or a failover
partner's copy.  sp_search names the directories to look in for a
pool's vdevs, since a pool on file vdevs is invisible to a scan of
/dev.

A dataset that holds no ZPL objects -- a zvol, or a snapshot of one --
answers EMEDIUMTYPE, which is this backend refusing this dataset and
not the ENOTSUP the library returns when it has no backend to ask at
all.  The ldiskfs backend separates the same two with ENOPKG; a caller
that skips its work where a build cannot scan must not also skip it
where the build can and the target was misnamed.

conf-sanity 300 is extended to cover it rather than left ldiskfs-only:
its gate becomes ldiskfs-or-zfs, the OST half keys off
ost1_FSTYPE == mds1_FSTYPE, and both facets grow an export_zpool and
import_zpool pair around the scan, because a pool still imported by
its server answers EBUSY.  -p and --search are passed so a pool on
file vdevs is found.

Its own autotest still cannot exercise that half: the build hosts
answer no to "zfs libzpool headers usable from userspace", so they
install no scan_osd_zfs.so and the ZFS arm of 300 skips there.  The
Test-Parameters lines below ask for the two sessions that can say
something.

lustre.spec.in changes with it: %files osd-zfs-mount becomes a
generated lustre-osd-zfs-mount.files, carrying its %dir and zed.d
entries over, because the plugin is built only when configure found
libzpool and a static file list cannot say that.

A leading slash separates a path from a dataset name, and that one
rule settles where a name goes and which errno comes back.  A name
beginning with '/' is a path: scan_device_exists() stats it and
returns -ENOENT, -ENOTDIR, -ELOOP or -EACCES before any backend is
chosen, so a mistyped device answers the error that happened even on
a build with no ZFS in it.  A name without one is a dataset, which is
what the ZFS backend takes, and never reaches the stat.  So
scan_backend_kind() needs no slash case of its own, and -ENOTSUP
keeps the meaning llapi_scan_device.3 gives it and conf-sanity 300
keys its skip on: this build has no backend for the target.

SCAN_CHUNKS_MAX is DN_MAX_OBJECT from <sys/dnode.h>, the bound the
walk needs, so a ZFS that widens the object id carries this along
with it.

-EBUSY names both of the things that raise it: a pool another mount
holds, and a dataset already owned within this process, which is a
second scan of the same target.  dmu_objset_own() answers the same
errno through dsl_dataset_tryown() for the second, where the pool is
exported and there is nothing to export.  llapi_scan_device.3 says
the same.

Test-Parameters: testlist=sanity env=ONLY=157c
Test-Parameters: fstype=zfs testlist=conf-sanity env=ONLY=300
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Fable-5 llm_code_and_review_tools
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I97a6d8815924b2671c818152f42d66cc580a609d
68160 master J ✓B ✓T …R 0/2
LU-20611 utils: lfind, find over a target
build OK — testing in progress (typically 6–14h)
L +755 −14
5h
build #131388 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+14 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-20 PS7 08-23 PS8 08-24 PS9 08-25 PS10 08-26 PS11 08-28 PS12 08-29 PS13 08-29 PS14 08-30
PS15 08-31 PS16 09-01 PS17 09-03 PS18 09-04 PS19 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS19 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20611 utils: lfind, find over a target

Add lfind(8), a server-side command.  It runs lfs find's predicates
against a Lustre target's own objects, read straight off the device on
the MDS or OSS whose disks hold it, and it is built under SERVER so a
client package does not carry a command with nothing to read.

Not a mode on lfs find, which is a client command by construction:
llapi_is_lustre_mnt() requires ":/" in the mount's device name, so a
server target mount is deliberately not recognised, and lfs ships in
lustre-client while the scan backend does not.

The predicates are the same ones, not a second copy.  What differs is
in lfind(8): it prints FIDs because a target has no paths, one
invocation covers one target, and a predicate the target cannot answer
is refused rather than quietly matching nothing.  The target is named
with --device, --target, --local, or as a plain argument; naming two
is an error rather than a scan of the first.

conf-sanity 300 grows the other half of the same question, which is a
third of this diff.  It already compared a target scan's FIDs against
what a client walk sees; now it runs lfind over that scan with a
predicate -- --type f -- and requires every regular file the client
listed to come back and no directory with it, taking the
subdirectory's own FID from the client first because after stopall
there is nothing left to ask.  The test is renamed to say so.

Its capability gate moves rather than goes.  The old check was [[ -x
$scanner ]] against the local build tree, which is the wrong node: the
scanner and lfind both run on mds1, so both are now checked there,
with do_facet mds1 "test -x $scanner" and a which for lfind.  Both are
tested up front rather than around the block that uses them, so a
build without either leaves a skip in the results instead of a pass
that ran none of the predicate half.

main() seeds the parameter block through lfs_find_parse_init() rather
than repeating lfs_find()'s initialiser, which is the second copy that
would otherwise have to be kept in step -- and was already one field
short of it here, fp_min_depth having been left to the designated
initialiser rather than named.

main() clears opterr, as lfs.c's does.  lfs_find_parse() prints its
own diagnostic for an unrecognised option and asks the caller for
opterr == 0 so getopt_long_only() does not print a second one; glibc
defaults it to 1, so every option error here was printed twice where
the same mistake under lfs printed it once.  optind wants no such
setting: lfind_parse_target() compares strings rather than calling
getopt(), so it is still 1 when the parser is reached.

conf-sanity 300 covers the three selectors this adds.  --internal
is the same scan without the class filter, so the answer without it
has to be a subset and strictly smaller -- an MDT that reported none
of its own objects would pass a count test and fail the point.
--local and --target are checked where the suite has already stopped
everything: each has to name the target it could not find rather than
scan something else, or report an empty node as an empty filesystem.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib25109943ad40afbc2d272a65e70c364c376b2b7
68095 master J !B ✓T …R 0/2
LU-20605 llapi: build find on the scan record
build OK — testing in progress (typically 6–14h)
1 unresolved L +290 −153
5h
build OK — testing in progress (typically 6–14h)
1 thread(s) waiting on others
janitor run: 1 failing config(s), none unique to this patch — janitor results
build #131383 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+14 earlier PS1 08-18 PS2 08-18 PS3 08-19 PS4 08-20 PS5 08-20 PS6 08-20 PS7 08-23 PS8 08-24 PS9 08-25 PS10 08-26 PS11 08-28 PS12 08-29 PS13 08-29 PS14 08-30
PS15 08-31 PS16 09-01 PS17 09-03 PS18 09-04 PS19 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS19 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20605 llapi: build find on the scan record

Rebuild the front half of cb_find_init() -- what the directory entry
settles, what to ask the MDT for, and the asking -- on the code
llapi_scan_namespace() uses, so lfs find and the scanner API share one
gather path and one record.  scan_rec_dirent() fills what the entry
alone says, find_prefilter() decides what that is enough to reject,
find_want() turns the search into a demand mask, and scan_rec_gather()
asks the MDT for what the mask names.

Four behaviour deltas.  The directory-stripe checks now run after the
stat ioctl, so a directory they reject costs one ioctl it did not
before, and the unstriped --foreign shortcut now prints this object's
stat attributes under -printf rather than the previous object's -- it
still takes the same early exit as before, so a project id it did not
fetch and an OST size it did not glimpse are unchanged.  An MDT index
lfs find cannot fetch no longer fails the object.  And a directory
that is not on Lustre answers ENOTTY, which is now treated as an
answer rather than an error, so the walk descends into it instead of
losing the subtree -- four paths recovered in a tmpfs-under-Lustre
test.

Two of those are bug fixes rather than deltas, and both became
reachable for an ordinary search when -printf put gather_all into the
cb_get_dirstripe() condition.

The last one is why printf_format_lustre() changes too: an object off
Lustre now reaches %LF and the file-layout fetch, and neither should
call that an error.  The layout arm tests ENOTTY, which is the
convention llapi_layout_get_by_fd() implements by mapping EOPNOTSUPP
onto it.  llapi_path2fid() does not follow that convention -- its only
ENOTTY is the EOVERFLOW arm of llapi_fd2fid(), which means a Lustre
handle would have fit -- so an object off Lustre arrives as ENODATA
from lgetxattr(trusted.lma), or ENOTSUP without server support, and
the %LF arm tests all three.  Making llapi_fd2fid() follow the
convention would be the better fix, but it changes a public call for
every caller and belongs in its own patch.

The project-id hunks are two more deltas.  get_projid() stops logging
on ENOTTY, and cb_find_init() now decodes that answer, so --projid N
rejects an object with no project id at all instead of failing the
walk, ! --projid N accepts it, and -printf %LP prints 0 for it.
An object with no project id is answered by the negated form only:
comparing the stand-in 0 against the number asked for made --projid 0
and ! --projid 0 both reject it, where the arm says the negated form
can always be answered.

One delta not listed above, in -name.  It now tests rec->lfsr_name,
which scan_rec_dirent() sets to the whole path when the last '/' is a
trailing one; the old code took everything after the last '/', which
is "" for a start point written /mnt/lustre/.  So --name '*lustre*'
now matches that start point where it did not, and --name '*/' too.
Neither is find(1), which tests the basename with the slash stripped
and so matches --name lustre and not --name '*/'; the value of
lfsr_name for a trailing-slash start point is worth settling on its
own rather than inside this patch.

sanity 56El covers the recovered subtree: a tmpfs mounted under the
Lustre mount, three files below it, and a walk that must both find
them and say nothing on stderr while printing %LF and %Lc for them.
The stderr half is the half that fails without the ENODATA arm above.

sanity 56* is unchanged before and after.

find_prefilter() answers a bool.  Nothing in it can fail -- it is
fnmatch() and a mode test, with no allocation and no I/O -- so there
is
no errno for it to return, and a bool is the one type a caller cannot
mistake for one.  true rejects the object here; false goes on to
gather.

Fixes: 6b8e97b76c47 ("LU-10378 utils: add formatted printf to lfs find")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ica2b5a8bd7a55f8ee00bd55c5b7771bb09a208fc
68094 master J ✓B ✓T …R 0/2
LU-20603 llapi: namespace scanner API
build OK — testing in progress (typically 6–14h)
5 unresolved XL +2342 −7
5h
build OK — testing in progress (typically 6–14h)
5 thread(s) waiting on others
build #131382 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-18 PS2 08-18 PS3 08-19 PS4 08-20 PS5 08-20 PS6 08-20 PS7 08-23 PS8 08-24 PS9 08-25 PS10 08-26 PS11 08-28 PS12 08-29 PS13 08-29
PS14 08-30 PS15 08-31 PS16 09-03 PS17 09-04 PS18 09-09
threads: 0 need your reply · 5 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20603 llapi: namespace scanner API

Add llapi_scan_namespace(), which walks a mounted filesystem and hands
a consumer one record per object rather than a formatted line.

The record is versioned twice over: lfsp_size says how much of the
parameter block the caller built, and a validity mask says which
fields the scanner could answer for, so a field it could not fetch
leaves its bit clear instead of reading as zero.  lfsp_want is a demand
mask, so a consumer pays only for what it asks for -- a name test
costs no ioctl.  An lfsp_flags bit this library does not define is
refused, because setting a flag leaves lfsp_size unchanged and nothing
else would keep a newer caller's flag from being ignored in silence.
The reserved bytes are must-be-zero for the same reason: they are
already counted in lfsp_size, so a field carved out of them later can
only be refused here, never noticed.

A foreign directory's LMV is left alone.  LL_IOC_LMV_GETSTRIPE answers
0 with a struct lmv_foreign_md in the same buffer, so fp_get_lmv is
set for one, and lum_stripe_offset and lfm_type both sit at offset 8,
so storing the MDT index there would land on the foreign type.  The
record publishes that buffer as lfsr_lmv with LLAPI_SCAN_LMV_FOREIGN
set, so a consumer reading exactly what the bit promises would have
got an MDT index where the type belongs.

A stat answers for an object the ioctl could not, and lmd_fid then
holds whatever was written there last: for the walk, the name it
passed to the ioctl, which fid_is_sane() reads as an IGIF.  So a
record off Lustre came back with LLAPI_SCAN_FID set and a FID built
from the object's own name, [0x676962:0x0:0x0] for a file called
"big".  Cleared where the stat answer is built, in
convert_lmd_statx(), which is the fallback's last step and also runs
on the V1-ioctl path, so both callers are covered by the one clear.

llapi_scan_get_lmv() clears lum_pool_name for the same reason.  A
directory that answers ENODATA or ENOTTY keeps the caller's one lmv
buffer, and the four fields a consumer reads for shape are zeroed
there; the pool name was not, so what stayed was the previous
directory's.

No consumer can observe that yet.  A directory with no stripe of its
own leaves lfsr_lmv NULL, so nothing reads the buffer at this patch,
and the clear is what makes the helper safe to share rather than a
wrong answer being fixed.  cb_find_init() fills the lmv itself here
and joins the helper in LU-20605, which is where the clear first
reaches a caller -- lfs find -printf %Lp.

That makes the HLD's POSIX Input Scanner a contract rather than an
accident: the same scanner with a different attribute source, filling
what a stat answers and leaving FID, layout, LMV, MDT index and HSM
clear.  test10 asserts both halves on a tree that is not Lustre, and
skips itself where the tree it was given is Lustre, the FID and the
layout being present there for a good reason.

The copy-in takes whole fields only.  lfsp_size is the caller's and the
range test alone does not bound it to a field: lfsp_filter is a function
pointer ending at 32 while the minimum is 24, so a size of 25..31
passed and memcpy left the pointer built from some of the caller's
bytes and some zeros -- which the scan then calls.  scan_param_whole()
rounds the length down to the last field that fits, which is what a
short size already means, and the field it stops inside is exactly the
one to drop.

get_projid() loses its static and moves into lustreapi_internal.h.
The gather needs it, and it is the only function this patch takes out
of liblustreapi_pfind.c's file scope.  Its own per-object messages
drop to LLAPI_MSG_DEBUG at the same time, marking them diagnostic: a
scan calls it once per object where lfs find called it once per match,
and scan_rec_gather() answers the failure by leaving LLAPI_SCAN_PROJID
clear rather than by reporting.  The level does not quiet them by
itself -- llapi_msg_level starts at LLAPI_MSG_MAX and neither lfs nor
lfind lowers it -- so what changes for those two is the dropped
"warning: " prefix; what the level buys is a caller able to silence
them with llapi_msg_set_level().

STATX_INO joins LLAPI_SCAN_MDT_MASK.  LLAPI_SCAN_STATX_MASK promises
the whole low half whether this API names the bit or not, and the
ioctl fills stx_ino, but nothing else does and the bit was in neither
that mask nor LLAPI_SCAN_DIRENT_MASK -- so lfsp_want = STATX_INO alone
returned before any fetch and the field arrived zero with its bit
clear, while STATX_INO|STATX_SIZE answered it.

The lmd_fid clear moves out of convert_lmd_statx() to the two callers
that have no FID to give.  The third, cb_find_init() under gather_all,
runs after the V2 ioctl has put a real one there, so clearing inside
the shared helper threw it away.  Nothing reads lmd_fid on that path
today; the patch after this one moves lfs find onto the record, where
lfsr_fid is filled from it.

llapi_scan_namespace.3 says how long each part of a record lasts,
rather than only that it does not outlive the callback.  The answers
differ and two are shorter than that rule reads: lfsr_name points into
lfsr_path rather than beside it, and a directory's lfsr_path is rewritten
by its own descent before any callback below it runs, while lfsr_fd for
anything but a directory is closed the moment the callback returns.
The layout and LMV buffers last longer than the rule instead, until
the next object is gathered, which is the more dangerous half -- code
that keeps them works until it does not.

The record begins with its statx, so a consumer can cast it to a
struct statx and read the Lustre fields past it.  Two static_asserts
hold that: lfsr_stx at offset 0 and lstatx_t exactly 256 bytes, so a
foreign binding that mirrors the layout by hand is caught at compile
time rather than reading every field at the wrong offset.

lfsp_size no longer gates.  A caller built against a longer parameter
structure is accepted when the bytes past this library's definition
are zero, rather than refused: the size says what the caller knows,
not which version it must be.  lfsp_want becomes advisory the same
way -- a bit this library does not know, or knows but this scan cannot
answer for, is dropped rather than refused -- and lfsp_got reports the
subset that was answered, so an application built against a newer
header can ask an older library for everything it might want and find
out what it got.

lfsr_padding names the hole the compiler was leaving.  Three
consecutive __u32 before a pointer left four bytes unnamed inside
lfsr_size, and a field carved out of them later can only be used if a
consumer could not have been reading something else there.

The prefixes are lfsr_ and lfsp_ rather than sr_ and sp_.  sp_ is
already md_op_spec's and ptlrpc_sec_policy's, and a public header
should not make a reader guess which one a field belongs to.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I0939ae4ea94e094f93c738027c9c6662e6a4f780
68159 master J ✓B ✓T …R 0/2
LU-20611 llapi: run find over a device scan
build OK — testing in progress (typically 6–14h)
1 unresolved XL +1010 −67
6h
build OK — testing in progress (typically 6–14h)
1 thread(s) waiting on others
build #131387 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-23 PS7 08-24 PS8 08-25 PS9 08-26 PS10 08-28 PS11 08-29 PS12 08-29 PS13 08-30
PS14 08-31 PS15 09-01 PS16 09-03 PS17 09-04 PS18 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20611 llapi: run find over a device scan

Add llapi_find_device(), which puts llapi_scan_device()'s records
through the same checks llapi_find() applies to a namespace walk.  The
checks are not reimplemented: the record is presented as the
lov_user_mds_data every find_check_* helper already reads.

A field the scan could not answer for is left out of stx_mask, as
llite leaves it out when the MDT's answer is not authoritative.  Both
the layout and the directory-stripe buffers live for the whole scan,
so an object without one has to say so rather than inherit the last
one's.

A search needing something a target scan has not -- a path (-printf),
a mounted filesystem (--ost, --mdt), an xattr read, or a walk to
describe (--maxdepth, --threads) -- is refused before the scan starts,
because afterwards "no matches" and "cannot answer" look alike.  A
field the inode format has no room for, a project id or a birth time,
counts as undecided and is reported at the end rather than ending the
search.  sp_filter is cleared with the other fields the search
decides: there is no caller data to pair a callback with, since the
scan carries this function's own state.

The layout is bounded before it is swabbed, not after.
layout_swab_lov_user_md() walks lcm_entry_count and stores through
lcme_offset, both read from bytes that came off a target something
may have been writing, so find_lmm_fits() has to see them first.  It
reads every field through whichever byte order it found, so running
ahead of the swab costs it nothing.  That is the order
llapi_layout_get_by_xattr() uses on the same bytes.

A top-level foreign magic in the target's byte order is refused there
rather than accepted, because layout_swab_lov_user_md() does not swab
one: accepting it would leave the EA swapped with lmd_lmmsize set, and
--foreign would then miss every HSM-released or PCC file on a big-
endian host.  Refusing sends it down the no-layout path instead.

The floor that check starts from is per-magic, not a blanket
sizeof(lov_user_md_v1).  A foreign layout's is the smaller
offsetof(lov_foreign_md, lfm_value): llapi_file_create_foreign()
accepts any non-zero length and lod_verify_striping() checks the
foreign magic before the 32-byte test that covers the normal ones, so
`lfs setstripe --foreign=none --xattr=abc` stores a legal 19-byte
trusted.lov.  A blanket floor refused it before the magic was read,
sending the file down the no-layout path to be answered off a forged
default -- so --foreign missed it and --stripe-count answered from the
forgery, which is the silent wrong answer the SEL and SPECIFIC arms
below exist to avoid.  Only lmm_magic is read before the switch, and
every arm carries its own floor.

Three shapes only a target scan meets are accepted there.  A
directory's default layout has lmm_stripe_count set and no
lmm_objects[] at all -- lod_gen_component_ea() sizes the EA with
op_count, 0 for a directory -- so `lfs setstripe -c 4 dir` is 32
bytes claiming four stripes, and
bounding by the count refused it.  The array is measured by the buffer
instead, as llapi_layout_objects_in_lum() measures it.  LOV_MAGIC_SEL
and LOV_USER_MAGIC_SPECIFIC are the other two: lod writes both and
rewrites neither for a client, so they reached the no-layout path and
every --stripe-*, --pool and --component-* answer for such an object
was compared against a forged filesystem default.  Both are given the
form a client would have been shown, SEL as COMP_V1 and SPECIFIC as
V3, so the checks below read one shape.

The linkea side of the diff.  scan_linkea() gains a
scan_linkea_entry() that indexes into trusted.link rather than always
reading the first record, exported through lustreapi_internal.h, and
find_device_prefilter() runs --name against every name the object has.
A walk meets a hardlinked file once per link and tests the name it
arrived by; a scan meets the object once, so testing one name would
answer for a file by whichever link the linkea happened to hold first.
The object is still reported once.  llapi_find_device.3 says so beside
the size and time notes, those being the other places a target scan
answers differently.

That changes one thing worth naming: a linkea whose header parses but
whose first entry does not no longer sets LLAPI_SCAN_LINKEA, where it
used to.  A record claiming a linkea nothing can read is worse than
one admitting it has none, and --name now has entries beyond the
first to lose.

A torn linkea further in is the same case and is counted the same way.
leh_reccount is capped by the smallest entry that could fit, so a
buffer claiming three names can spell one and still report three; the
loop over the names then ran out partway and fell through to a
rejection, reporting the object as a non-match on a name list it never
finished reading.  It asks the other predicates and counts the object
undecided instead, which is what the no-name case beside it does.

find_device_prefilter() asks the other predicates before calling an
object undecided.  Answering 2 for "no name anywhere" without running
them would, over an OST target -- where no record has a linkea --
count every data object undecided for `-name X -type d` even though
-type rejects all of them, and llapi_find_device() would then warn
that N objects could not be decided for a search whose answer was
complete.  Every other undecided point sits after the checks that
precede it, and this one does too.

find_prefilter() leaves -name alone for a record with no name, so
asking it costs nothing and answers exactly the other predicates.  It
has to: fnmatch() dereferences its string argument, so testing a NULL
sr_name would fault rather than not match, and an OST data object
reaches it that way -- the object carries no linkea, and neither do
the CLS_INTERNAL, CLS_NO_LMA and CLS_AGENT records
LLAPI_SCAN_F_INTERNAL delivers.

A foreign directory answers --links here.  find_decide()'s nlink gate
reads fp_lmv_md->lum_stripe_count to decide whether a stat is needed,
and lmv_foreign_md's lfm_length sits at that offset, so a foreign
LMV's value length was read as a stripe count and asked for one.  A
walk pays the stat and answers correctly; a scan has neither path nor
descriptor, so the object went undecided and was dropped out of both
--links N and its negation.  The gate now tests lmv_is_foreign()
first, as the --mdt-count, --hash-type and --hash-flag arms beside it
already do.

llapi_find_device.3 describes an object by its FID, and by its object
id where there is no FID.  It does not describe one with neither: the
scan sets the object id on every record, so the second form always
applies where the first does not.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ibf2dcf71e5444914f306ad37a551262cddd8e1de
68727 master J ✓B ✓T …R 0/2
LU-20605 llapi: trim a start point's trailing slashes
build OK — testing in progress (typically 6–14h)
S +39 −10
7h
build #131398 SUCCESS · tests all Maloo sessions · test queue
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS1 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20605 llapi: trim a start point's trailing slashes

-name tests rec->lfsr_name, and lfsr_name points into the walked path,
so for a start point spelled "/mnt/lustre/d/" the basename is not a
suffix of the string and no pointer into it spells "d".

It goes in llapi_find_with_cb(), which is the one way into the
traversal: llapi_find() reaches it and so does llapi_scan_namespace(),
which had been carrying the loop itself since the scanner landed.  One
place, so the two cannot answer differently for the same spelling, and
an external caller of the exported walk gets the fix without asking.

llapi_scan_fid() and llapi_find_since() keep loops that look like this
one and are not: neither walks anything.  They trim a mount path and a
caller's spelling before *composing* a name from it, where a trailing
slash doubles a separator rather than hiding a basename.  Folding them
into the traversal would be a shape match, not a common cause.

This is not a regression of the record work.  Measured against a stock
lfs built from this series' base, 2.17.57_43_g5afbab2: for a
trailing-slash start point stock matches nothing at all, its fname
being "", so against find(1) stock answers 1 of 3 patterns correctly
and the record-based -name also answers 1 of 3 -- different wrong
answers rather than worse ones.  With the trim it is 3 of 3.

The caller's buffer is left alone, since it is argv in lfs(1), and
both param_callback() and pfind_param_callback() copy what they are
given.

Trimming also removes the doubled separator from every child line
below such a start point, which brings them to what find(1) prints.
The one remaining difference is the start-point line itself: find(1)
echoes the spelling it was given and this prints the trimmed form.
That separator is composed by an unconditional strcat(path, "/") in
llapi_semantic_traverse() which predates this series and is unchanged
by it -- stock doubles it identically, including for "/" -- so making
the start-point line agree as well belongs to a fix for that, not
here.

Covered out of tree for now, over all three spellings and the root:
13 cases, validated against the stock build, where 7 of them fail
while the no-trailing-slash control passes either way.  A sanity case
for it is worth adding.

llapi_find() answers -ENAMETOOLONG for a path that does not fit, where
param_callback() answered -EINVAL for the same path further in.  The
limit is unchanged and the diagnostic naming the path is kept; only
the errno a caller sees is better.

Test-Parameters: trivial
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I01729e3816785c298fd44f9fa93596d578834883
68231 master J ✓B ✓T …R 0/2
LU-20624 utils: fix stale fd in cb_get_dirstripe
build OK — testing in progress (typically 6–14h)
S +31 −13
8h
build #131380 SUCCESS · tests all Maloo sessions · test queue
no vote yet: Andreas Dilger
patchsets:
+2 earlier PS1 08-23 PS2 08-24
PS3 08-25 PS4 08-29 PS5 09-03 PS6 09-04 PS7 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS7 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20624 utils: fix stale fd in cb_get_dirstripe

cb_get_dirstripe() closed the descriptor it was given and stored an
O_NOFOLLOW reopen through its argument on the ENOTTY retry, which
fires for a foreign file or directory and for any directory not on
Lustre.  cb_find_init() and cb_getstripe() pass the address of a local
copy, so llapi_semantic_traverse() kept a number that had been closed:
it closed that number a second time on the way out and leaked the
reopen.  The second close is the dangerous one, since with lfs find
--threads the number may by then belong to another worker.

Keep the reopen on the descriptor the caller already holds, with
dup2(), rather than asking every caller to notice that its copy was
replaced.

The number no longer changes, so the parameter is read only and the
signature now says so: int rather than int *.  The pointer advertised
exactly the swap that caused this, and an assignment added back
through it would break the same callers in the same way without any
of them having to change.

Not fixed here: for a directory off Lustre the retried ioctl answers
ENOTTY as well and cb_find_init() returns that error, so the subtree
is lost to the error return rather than to the descriptor.  Measured
with strace on the main process: lfs getdirstripe -r goes from one
EBADF close to none.

Fixes: 15d44e787e17 ("LU-12682 llite: fake symlink type of foreign file/dir")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I19a3d62d231e9a932fba12a4d56fbbe691912cc9
68157 master J ✓B ✓T …R 0/2
LU-20611 llapi: split cb_find_init's decider out
build OK — testing in progress (typically 6–14h)
L +545 −448
10h
build #131385 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-23 PS7 08-24 PS8 08-25 PS9 08-26 PS10 08-28 PS11 08-29 PS12 08-29 PS13 08-30
PS14 08-31 PS15 09-01 PS16 09-03 PS17 09-04 PS18 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20611 llapi: split cb_find_init's decider out

Move the deciding half of cb_find_init() into find_decide(), taking a
struct find_ctx instead of the traversal's locals, so a caller holding
a record and no path can reach the same predicates.  What the
traversal used to read from its own stack -- the path, the parent and
directory descriptors, the stripe count, whether the object is on
Lustre -- the context carries, and a caller with none of it leaves
them NULL or -1 rather than being unable to call at all.

The project id comes out with it, as find_get_projid().  A walk opens
the object and asks the ioctl; a record that already carries one is
answered from it; and a caller with nothing to open is told so, which
is the case find_decide() reads as undecided rather than as a project
id of zero.

No behaviour change: the checks, their order and their early exits are
the ones that were there.  Line for line against the old function, the
only edits are the "goto decided;" turned into "return 0;", the "goto
out;" turned into "return ret;", and the declarations that moved into
cb_find_init().

find_decide() returns 0 or a negative errno, and nothing else.
Whether the object matched is not reported: printing is the answer,
and no caller reads it.  A positive would be the third meaning of 1
among neighbouring functions here -- find_prefilter() rejects with
one, cb_find_init() returns one for "do not descend" -- and it is
cb_find_init()'s own ret that this value lands in, safe today only
because the "decided:" label overwrites it first.

Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I6c1550b715bb505a3160c21bb68a0521c7a399da
68158 master J ✓B ✓T …R 0/2
LU-20611 lfs: share find's predicate parsing
build OK — testing in progress (typically 6–14h)
1 unresolved XL +2118 −1898
10h
build OK — testing in progress (typically 6–14h)
1 thread(s) waiting on others
build #131386 SUCCESS · tests all Maloo sessions · test queue
patchsets:
+13 earlier PS1 08-19 PS2 08-19 PS3 08-20 PS4 08-20 PS5 08-20 PS6 08-23 PS7 08-24 PS8 08-25 PS9 08-26 PS10 08-28 PS11 08-29 PS12 08-29 PS13 08-30
PS14 08-31 PS15 09-01 PS16 09-03 PS17 09-04 PS18 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS18 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-20611 lfs: share find's predicate parsing

Move lfs find's option table, the loop that reads it and the argument
helpers it calls into lfs_find_parse.c, arranged so lfind can compile
it the way callvpe.c is already shared between lfs and lustre_rsync.
A second copy of that vocabulary would be a second copy that drifts.
Nothing is exported from the library and no ABI is touched.

The helpers move verbatim.  The loop moves with four differences and
no others: param. becomes param->, every goto err becomes goto out,
the three out-parameters are initialised before it and pathstart and
pathend are written back after it, and in the --foreign arm the two
returns of CMD_HELP become a goto out -- which is what makes the
stopped flag reliable there and routes the path through
lfs_find_parse_fini().

@pathstartp and @pathendp are the subtle half of the new interface,
because -1 does not mean the same thing in the two: in @pathstartp it
is "no path at all", and in @pathendp it is "no option followed the
paths", which the caller has to read as argc.  That is the ordinary
case for "lfs find -type f /mnt/a /mnt/b", and a caller that takes it
literally walks only the first path.  The header says so; it is worth
knowing before reading the loop.

Two things come with it.  Several failures in that loop give up
without setting ret, so lfs find has always exited 0 after a bad
--comp-flags; the parser now says so through a stopped flag, preserved
rather than fixed.  And lfs_find_parse_fini() frees the four
find_param fields a parse can leave on the heap, so a second caller
does not have to copy the cleanup block out of lfs_find().

The input half of the contract is a call, not an initialiser.  Two
fields have to arrive non-zero -- fp_max_depth, which llapi_find()
reads, and fp_time_margin, which set_time() does -- so a second front
end handing over a zeroed structure would walk the top directory only
and date a "-mtime +1y" differently.  lfs_find_parse_init() sets
those, carries fp_min_depth and fp_quiet as lfs_find() set them, and
zeroes the rest, beside the fini() that was already owed, so what a
caller has to copy across is the call.

An optind of 0 is tolerated rather than mis-read.  The interactive
shell's execute_line() sets it to 0 before calling the command, which
getopt() takes as "reinitialise" and answers by scanning from argv[1]
anyway; seeding prev_optind from that 0 made pathstart 0, so
`lfs> find /mnt/lustre -type f` searched argv[0] -- "find" -- and
reported it missing before searching the path.  That is upstream's,
lfs_find() having read optind the same way, but the contract this
header states is not true of a caller in tree unless it is fixed.

Fixes: f7ce2a0ae35f ("LU-16944 utils: lfs find: handle multiple paths correctly")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ifa24de52c0b688f5c107a9efc96ad5274c7ec39a

Needs reviewers

(0)
nothing here right now

Waiting on reviewers

(4)
68617 master J –B ✓T ✓R 1/2
LU-19982 doc: fix lustreapi.7 SEE ALSO order and AVAILABILITY
1 thread(s) waiting on others
1 unresolved S +9 −9
8h
1 thread(s) waiting on others
no vote yet from Ellis Wilson, Andreas Dilger
build #131381 SUCCESS
reviews: Arshad Hussain +1 2026-09-09 03:23
no vote yet: Ellis Wilson, Andreas Dilger
patchsets: PS1 09-03 PS2 09-04 PS3 09-09
threads: 0 need your reply · 1 waiting on others · 0 own notes · 0 bot
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS3 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-19982 doc: fix lustreapi.7 SEE ALSO order and AVAILABILITY

Five findings checkpatch-man.pl reports on the page, left once the
subsection warnings stop hiding them.  With the subsection fix in and
this patch out, the page draws 1 error, 4 warnings and 4 checks; with
this patch it draws 0 errors, 0 warnings and 1 check.

Two references are out of order.  The ordering compares names with '_'
and '-' read as a space, so llapi_fid_hash sorts before llapi_fid2path
and llapi_pcc_state_get before llapi_pccdev_get; both sat after.

AVAILABILITY names '.B lustre (8)'.  The reference is to lustre(7),
not to the configuration utilities, and it is a reference, so it takes
.BR.  The wording beside it becomes the one llapi_layout.7 uses for
the same library.

The page source in .TH was Lustre where every other page describing
this library says "Lustre User API".  The modification date on the same
line moves with it, 2024-08-28 to 2026-09-03, which is the fifth
finding: a page that changes is expected to say when.

The manual section string is left alone: "The Lustre API library"
predates the series and renaming an existing page's header is not ours
to do.

AVAILABILITY says 'commit 0.9.1' where it said 'release 0.9.1'.  The
checker's model is a commit version and the release that follows it,
and the line above already gives that release as 0.10.0 -- so the page
was saying two different things about one fact.  0.9.1 is a tag in
this tree:

    commit c5050e412572 ("land v0.9.1 on HEAD, in preparation
    for a 1.0.x branch")

so there is a commit version to name.  That one word alone -- measured
with the other three already applied -- takes the page from 1 error, 0
warnings, 2 checks to 0 errors, 0 warnings, 1 check, because it clears
both the remaining ERROR and the AVAILABILITY_FORMAT_RELEASE check on
its own line.  What is left is the manual section string above.

Test-Parameters: trivial
Fixes: 155cf6d41dac ("LU-4315 doc: updating ls-tu man page style")
Fixes: 8e58f4e72b50 ("LU-19983 doc: Add missing llapi_* references to lustreapi.7")
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: Ib002aa3d2334c492545e35e598b319f6f8abaa1c
65331 master J –B ✓T ✓R 1/2
LU-20119 selinux: add lustre_helpers SELinux policy module
no vote yet from Shaun Tancheff, Matt Raso-Barnett, Andreas Dilger
L +683 −0
9h
build #128976 SUCCESS · tests all Maloo sessions
reviews: Sebastien Buisson +1 2026-09-09 07:34
no vote yet: Shaun Tancheff, Matt Raso-Barnett, Andreas Dilger
patchsets:
+15 earlier PS1 04-14 PS2 04-15 PS3 04-15 PS4 04-15 PS5 04-27 PS6 05-05 PS7 07-01 PS8 07-02 PS9 07-06 PS10 07-16 PS11 07-17 PS12 07-17 PS13 07-21 PS14 07-28 PS15 07-29
PS16 07-30 PS17 07-31 PS18 08-03 PS19 08-05 PS20 08-08
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS20 uploaded 31d ago · NEW · open in Gerrit ↗
commit message
LU-20119 selinux: add lustre_helpers SELinux policy module

On el10+ kernel-launched helpers run in kernel_t with no usable
domain, so l_getsepol, l_getidentity, l_getauth, l_foreign_symlink
and lctl are denied the policy and identity-lookup access they
need.  Add a lustre_helpers policy module transitioning them into
lustre_helper_t, and granting keyutils_request_t the socket access
NID resolution needs.  configure enables it on el10+, overridable
with --with/--without-selinux-policy.

The el10.0 part-2 and el10.1-server sessions are not requested:
both fail for reasons unrelated to SELinux (LU-20542, LU-16330).

Test-Parameters: trivial
Test-Parameters: testgroup=review-dne-selinux-ssk-part-1 clientdistro=el10.0
Test-Parameters: testgroup=review-dne-selinux-ssk-part-1 clientdistro=el10.1
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2 clientdistro=el10.1
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: I1ad770867d2e48be9b72c128d50d2f246f79bdfe
68616 master J –B ✓T ✓R 0/2
LU-4315 contrib: let SEE ALSO carry subsections
S +26 −3
11h
build #131379 SUCCESS
no vote yet: Arshad Hussain, Ellis Wilson, Andreas Dilger
patchsets: PS1 09-03 PS2 09-03 Andreas Dilger PS3 09-04 PS4 09-09
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS4 uploaded 13h ago · NEW · open in Gerrit ↗
commit message
LU-4315 contrib: let SEE ALSO carry subsections

LU-19982 grouped lustreapi.7's references into .SS subsections, each
introduced by a description line and a .PP.  checkpatch-man.pl's SEE
ALSO checker accepts only a flat run of '.BR page (N),' lines, so it
reports every one of those description lines and every .PP as a
malformed reference, and then complains that the .PP does not end with
a comma.  On lustreapi.7 that is 39 warnings -- 13 description lines
and 13 .PP reported as malformed, and 13 more for the comma -- out of
the 42 the page draws in total, and it is the tool that is wrong: the
grouping was reviewed and landed deliberately.

Treat a .SS inside SEE ALSO as starting its own run of references, and
allow the description line and the .PP that separate it from them.
Ordering is still checked within each run, which is where it means
something; the runs are thematic and not sorted against each other.

The comma and ordering checks now run only when the previous line is
itself a reference.  They were reached with a .PP as the previous line
and no reference to compare against, which is what produced the
spurious comma warning -- and, from the two unguarded captures, 39
uninitialized-value warnings from perl.  Three on the first reference
of each of the page's 13 subsections, one for $prevfile and two for
$prevnum; not one per reference, of which the page has 141.

$llapi also selects the wording AVAILABILITY is expected to use, and
it matched the filename against llapi alone, so lustreapi.7 was asked
for 'filesystem package' where its sibling llapi_layout.7 is asked for
'user application interface library'.

The macro the section header matched is captured before it is used.
Two regex matches sit between, so $1 held whichever of them last
succeeded rather than SH or SS.  Two pages do put a spacing macro
directly before a .SS -- llapi_changelog_recv.3 and lnetctl.8 -- but
both headers are quoted, and the quoted-header match between resets $1
to the right SS before it is read.  An *unquoted* .SS after a spacing
macro is the input that reached the bug, and the tree has none.

A .SS also ends the run before it, so its last reference takes the
same no-trailing-comma test as the last reference of the whole
section.  The .SS branch returned before the SEE ALSO block could
apply it, so a run ending '.BR aaa (3),' passed silently.  And .P and
.LP are exact synonyms of .PP in man(7) -- the script's own EMPTY_LINE
warning asks for .P -- so the preamble takes all three, where a .P was
reported as a malformed reference.

The .PP ends the preamble rather than being skipped alongside it.  Any
number of description lines and .PP lines in any order were accepted
until the first reference, so prose sitting after the .PP went
unreported where it used to be caught.  Clearing the flag at the .PP
keeps the run boundary the comment describes and still lets the
description through.  Nothing in the tree trips the tightened rule:
the 378 pages were swept again with these changes and not one count
moved.

Widening $llapi has one visible consequence before the companion page
fix lands: it also selects the page source .TH is expected to carry,
so lustreapi.7 reports 'Page source Lustre not "Lustre User API"'
until LU-19982 sets it.

Every man page under Documentation was checked before and after: 378
pages, and lustreapi.7 is the only one whose count changes.

Fixes: d15955d374d5 ("LU-4315 doc: add man pages to checkpatch")
Test-Parameters: trivial
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: ClaudeCode:Opus-5 llm_code_and_review_tools
Change-Id: I1b2ccbddebc8832e400647133fb447e0417ac600
65798 master J !B ✓T ✓R 1/2
LU-20238 ec: GFNI-accelerated EC primitives
no vote yet from Patrick Farrell, Andreas Dilger, Oleg Drokin
pt_ecro XL +3278 −42
2d
janitor run: 1 failing config(s), none unique to this patch — janitor results
build #128896 SUCCESS · tests all Maloo sessions
reviews: Ronnie Sahlberg +1 2026-09-07 08:51
no vote yet: Patrick Farrell, Andreas Dilger, Oleg Drokin
patchsets:
+10 earlier PS1 05-11 PS2 05-11 PS3 05-11 PS4 05-12 PS5 05-12 PS6 05-13 PS7 05-13 PS8 05-17 PS9 06-08 PS10 06-17
PS11 07-10 PS12 07-10 PS13 08-05 PS14 08-06 PS15 08-07
tags: pt_ecro
owner Hiroshi Nishida · uploader Hiroshi Nishida · PS15 uploaded 33d ago · NEW · open in Gerrit ↗
commit message
LU-20238 ec: GFNI-accelerated EC primitives

Add a runtime-dispatched GFNI fast path to libec.a (LU-19465).
On x86_64 with GFNI, ec_encode_data(), ec_encode_data_update(),
ec_init_tables(), gf_vect_dot_prod() and gf_vect_mad() route to
AVX-512 or AVX2 GFNI kernels; everywhere else the existing
ec_*_base() path is used unchanged.

The kernels are C intrinsics (_mm{256,512}_gf2p8affine_epi64_epi8)
gated per-function with __attribute__((target(...))), so the build
needs only GCC >= 8 / clang >= 8 and no NASM.  They use only the
GF(2^8) affine instruction and XOR/load/store, not the PSHUFB
nibble-table lookup the ISA-L AVX2/SSE kernels rely on.

The multi-output AVX-512 kernels take the coefficient as an EVEX
{1to8} embedded broadcast to keep it off Port5; compilers will not
emit {1to8} from intrinsics, so a short inline-asm macro is used.
Against ISA-L's NASM kernels this reaches parity on AVX-512 (+1%
to -5%) and matches or beats NASM on 4 of 5 AVX2 benchmarks, the
one slower case by 6%.

A configure probe (EC_GFNI_SUPPORT) compile-tests the intrinsic
and defines LUSTRE_EC_GFNI for the libec.a build only.  Every
dispatch block is guarded by the pair defined(LUSTRE_EC_GFNI) &&
!defined(__KERNEL__), so no shipped consumer reaches a GFNI kernel
yet: ec.ko and liblustreapi both take the base path, and the
numbers above compare kernels directly rather than describing a
user-visible speedup.  Follow-on patches add the Kbuild plumbing
and the liblustreapi wiring.

gfni_ec_test checks that the GFNI and base paths produce identical
output, and exits 77 without GFNI so a caller can skip instead of
recording a base-against-base pass.  It installs with the test
suite; sanity-ec.sh test_0 sweeps geometries and lengths through
it.  -i picks the kernel family directly, because the runtime
dispatch always prefers AVX-512 and would otherwise leave the
AVX2 kernels untested on any CPU that has both.

The vendored gf_*_gfni.c kernels are reflowed for lustre
checkpatch; see lustre/utils/erasurecode/GFNI_VENDORED.md.

Test-Parameters: testlist=sanity-ec
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Change-Id: Ia40a743c8e47c018250728668090215c7bd8365b

Ready to land

(0)
nothing here right now

Parked (WIP / self-blocked)

(0)
nothing here right now

Carrying — uploaded by me, owned by others

0 open

Failed / broken

(0)
nothing here right now

Review feedback — your turn

(0)
nothing here right now

In CI

(0)
nothing here right now

Needs reviewers

(0)
nothing here right now

Waiting on reviewers

(0)
nothing here right now

Ready to land

(0)
nothing here right now

Parked (WIP / self-blocked)

(0)
nothing here right now
5 open

Re-review needed — new patchset since your vote

(0)
nothing here right now

Awaiting your review — CI green

(2)
68662 master J ✗1B ✓T ✓R 1/2
LU-20505 lfsck: fix osd-scrub vs lfsck race
reviewing Alex Zhuravlev · CI green — awaiting your review
testfix 2 unresolved S +8 −3
8h
janitor flagged test failures unique to this patch — superseded (tests have since passed) — full janitor results
unique failing testhistory
recovery-small@ldiskfs:test_155seen in 10 other reviews
reviews: Andreas Dilger +1 2026-09-09 00:19
patchsets: PS1 09-05 PS2 09-09 Andreas Dilger
threads: 2 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS1 2026-09-06 23:28 Gerrit AI review for Lustre osd_scrub.c:786 — (minor) What is this barrier pairing with? `dev->od_scrub.os_oic` is written here and consumed only by the same scrub thread, via osd_scrub_
Gerrit AI review for Lustre PS1 · 2026-09-06 23:28
(minor) What is this barrier pairing with? `dev->od_scrub.os_oic` is written here and consumed only by the same scrub thread, via osd_scrub_next()->osd_scrub_exec()->osd_scrub_check_update(); the LFSCK/otable side reads its own it->ooi_cache.ooc_cache[], which osd_preload_next() fills in the LFSCK thread.

None of the readers of os_pos_current -- osd_preload_next(), osd_scrub_has_window(), osd_otable_it_next() -- has a matching read-side barrier either.

So oic_fid/oic_lid are never published to another thread and nothing observes the ordering. If there is a reader this is meant to pair with, it isn't obvious; otherwise it looks droppable.
reply PS1 2026-09-06 23:28 Gerrit AI review for Lustre osd_scrub.c:787 — (defect) Does publishing the position here close the whole window? The OI mapping for `next` is only repaired afterwards, in osd_scrub_exec(
Gerrit AI review for Lustre PS1 · 2026-09-06 23:28
(defect) Does publishing the position here close the whole window? The OI mapping for `next` is only repaired afterwards, in osd_scrub_exec()->osd_scrub_check_update(), so this still advertises an inode the scrub has not finished with.

The consumer tests its gate before it advances:

    if (scrub->os_running &&
        ooc->ooc_pos_preload >= scrub->os_pos_current)
            return SCRUB_NEXT_EXIT;
    rc = osd_iit_next(param, &ooc->ooc_pos_preload);

A preload sitting below os_pos_current passes the gate and osd_iit_next() then lands it on exactly os_pos_current, where it igets and caches that inode. Because the batch only ends once ooc_pos_preload >= os_pos_current, hitting that inode is how each preload batch normally terminates, not a corner case.

osd_iit_iget() is also the cheap half of the per-inode work: osd_scrub_check_update() does another osd_iget(), osd_oi_lookup() and the osd_scrub_refresh_mapping() transaction. So the larger part of the window is untouched.

Would moving the assignment past osd_scrub_exec(), or restoring the pre-e2ad44de38 order in osd_preload_next(), be the more complete fix?
tags: testfix
owner Alex Zhuravlev · uploader Andreas Dilger · PS2 uploaded 16h ago · NEW · open in Gerrit ↗
commit message
LU-20505 lfsck: fix osd-scrub vs lfsck race

osd scrub and lfsck share same iterator, where current position is
stored in os_pos_current and corresponding fid/ino in oic_fid/oic_lid.
osd_scrub_next() advances os_pos_current, but oic_fid/oic_lid are
updated later in osd_iit_iget(). the race should go away if we update
os_pos_current after oic_fid/oic_lid.

Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: env=ONLY=5,ONLY_REPEAT=50 testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Test-Parameters: testlist=sanity-lfsck
Fixes: e2ad44de38a4 ("LU-9488 scrub: reset noslot for full speed scrub")
Signed-off-by: Alex Zhuravlev <bzzz@whamcloud.com>
Assisted-by: ClaudeCode:Opus-5
Change-Id: I6f409b40173191efef6931c356a0271df1ae86e4
64927 master J !B ✓T ✓R 1/2
LU-20016: Simple benchmark tool for EC computations
reviewing Ronnie Sahlberg · CI green — awaiting your review
ec2 pt_ecro 2 unresolved M +233 −1
12d
janitor run: 6 failing config(s), none unique to this patch — janitor results
build #124990 SUCCESS · tests all Maloo sessions
reviews: Andreas Dilger +1 2026-05-13 19:18
no vote yet: Marc Vef, Patrick Farrell
patchsets:
+1 earlier PS1 03-26
PS2 03-27 PS3 03-27 PS4 05-13 Andreas Dilger PS5 05-13 Andreas Dilger PS6 05-13 Janitor Bot
threads: 2 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS3 2026-05-13 05:08 Andreas Dilger ec_perf_bench.c:153 — (minor) it is confusing to me that this is using `p` = parity and `m` = total stripe count. I thought the standard was `k` = data, `m` = pa
Andreas Dilger PS3 · 2026-05-13 05:08
(minor) it is confusing to me that this is using `p` = parity and `m` = total stripe count.  I thought the standard was `k` = data, `m` = parity, as with patch https://review.whamcloud.com/61965 ("LU-12187 lfs: add EC setstripe support with k+m notation").
reply PS6 2026-05-14 08:42 Andreas Dilger ec_perf_bench.c:119 — It would be useful if this printed some information about the local CPU architecture (e.g. `model name` and `cpu MHz` from `/proc/cpuinfo` o
Andreas Dilger PS6 · 2026-05-14 08:42
It would be useful if this printed some information about the local CPU architecture (e.g. `model name` and `cpu MHz` from `/proc/cpuinfo` or equivalent from `lscpu`) and the EC implementation that was selected, so that we have some way to identify the results that are generated.  Otherwise, we get a bunch of numbers and no way to identify how they relate.
tags: pt_ecro
owner Ronnie Sahlberg · uploader Janitor Bot · PS6 uploaded 119d ago · NEW · open in Gerrit ↗
commit message
LU-20016: Simple benchmark tool for EC computations

Note that in the ISL-L library, computing parities and recovering
lost data stripes is the same operation: ec_encode_data().
The difference is not in the computations but rather the content
of the coefficient matrix.

The cost of these operations are primarily bound by the number
of stripes to compute which amusingly means that
recovering a single lost stripe should be FASTER than
generating two parities.

Test-Parameters: trivial testlist=sanity-ec
Signed-off-by: Ronnie Sahlberg <rsahlberg@whamcloud.com>
Change-Id: I61f409b631a8f4aefe0ceb2628e56f58f4c3a07c

Awaiting review — CI not green yet

(2)
66081 master J ✗1B ✓T ✗1R 0/2
LU-20296 gss: also force reconnect if ctx nego timeout
reviewing Bruno Faccini · awaiting review (CI not green yet)
1 unique failure 4 unresolved M +95 −17
1h
janitor flagged test failures unique to this patch — full janitor results
unique failing testhistory
conf-sanity4@ldiskfs+DNE:test_152seen in 1 other review
failed enforced testplatformdetail
custom-1001 RHEL 9.7 / x86_64 ran 3 tests. 1 tests failed: sanity-krb5. session
build #131308 SUCCESS · tests all Maloo sessions
no vote yet: Aurelien Degremont, Sebastien Buisson, Mikhail Pershin
patchsets:
+17 earlier PS1 05-22 PS2 05-22 PS3 05-22 PS4 07-09 PS5 07-09 PS6 07-10 PS7 07-27 PS8 07-28 PS9 07-29 PS10 08-16 PS11 08-16 PS12 08-20 PS13 08-20 PS14 08-31 PS15 08-31 PS16 08-31 PS17 09-02
PS18 09-03 PS19 09-07 PS20 09-07 PS21 09-07 PS22 09-08
threads: 4 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS7 2026-07-28 04:12 Gerrit AI review for Lustre COMMIT_MSG:16 — (style) The body doesn't name any of the symbols it adds or changes, so this is hard to find later with `git log -S`. Worth mentioning `ctx_
Gerrit AI review for Lustre PS7 · 2026-07-28 04:12
(style) The body doesn't name any of the symbols it adds or changes, so this is hard to find later with `git log -S`. Worth mentioning `ctx_timeout_work()` and `ctx_upcall_timeout_kr()` by name.

Three hunks are also unexplained: `import_set_state_nolock()` loses `static` and gains an `EXPORT_SYMBOL`, and `ptlrpc_abort_inflight()` / `ptlrpc_pinger_wake_up()` gain `EXPORT_SYMBOL` so ptlrpc_gss can reach them. A sentence saying the new work runs in the ptlrpc_gss module and therefore needs those exports would cover them.
reply PS18 2026-09-04 14:03 Gerrit AI review for Lustre gss_keyring.c:134 — (suggestion) ptlrpc_pinger_force() already packages the IMPF_FORCE_VERIFY set_bit with the pinger wakeup, and it is exported and declared in
Gerrit AI review for Lustre PS18 · 2026-09-04 14:03
(suggestion) ptlrpc_pinger_force() already packages the IMPF_FORCE_VERIFY set_bit with the pinger wakeup, and it is exported and declared in lustre_ha.h, which lustre_net.h already pulls into this file:

    void ptlrpc_pinger_force(struct obd_import *imp)
    {
            set_bit(IMPF_FORCE_VERIFY, imp->imp_flags);
            smp_mb__after_atomic();
            if (imp->imp_state != LUSTRE_IMP_CONNECTING)
                    ptlrpc_pinger_wake_up();
    }

The state is already DISCON when this runs, so the wakeup would still happen and the behavior would be the same. Using it here, and dropping the set_bit above, would also make the new EXPORT_SYMBOL(ptlrpc_pinger_wake_up) in pinger.c unnecessary, and would pick up the smp_mb__after_atomic() that the neighbouring IMPF_ updates in import.c pair with. If the patch is refreshed, worth considering.
reply PS19 2026-09-07 14:24 Aurelien Degremont gss_keyring.c:134 — ptlrpc_pinger_force(), already exported, would be better here instead of exporting this new one.
Aurelien Degremont PS19 · 2026-09-07 14:24
ptlrpc_pinger_force(), already exported, would be better here instead of exporting this new one.
reply PS22 2026-09-08 21:42 Bruno Faccini gss_keyring.c:201 — > hmmm, don't you think that this case is covered by the imp_generation and imp_conn_cnt checks ?
Aurelien Degremont PS22 · 2026-09-08 09:05
This may trigger a new CONNECT while the current CONNECT, which is timeouting in GSS, is still on imp_send_list.
When the GSS context will be refreshed, both CONNECT RPCs will race and the old CONNECT could do unwanted stuff (back to DISCON, eviction, ...).
Bruno Faccini PS22 · 2026-09-08 11:21
hmmm, don't you think that this case is covered by the imp_generation and imp_conn_cnt checks ?
Bruno Faccini PS22 · 2026-09-08 21:42
> hmmm, don't you think that this case is covered by the imp_generation and imp_conn_cnt checks ?
owner Bruno Faccini · uploader Bruno Faccini · PS22 uploaded 1d ago · NEW · open in Gerrit ↗
commit message
LU-20296 gss: also force reconnect if ctx nego timeout

Force disconnect if a timeout occurs during security
context negotiation.

But do it in work queue instead in current IRQ/timer context.

This will trigger a new connection choice instead of looping
on the same wrong one when alternate and working ones
could be available.

Test-Parameters: kerberos=true testlist=sanity-krb5
Test-Parameters: testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Bruno Faccini <bfaccini@nvidia.com>
Change-Id: I81912a4b0679fa87f35b84c259581dc0493566ea
64937 master J –B ✗T –R 1/2−
LU-20016 ec: ISA-L SIMD for userspace and kernel
reviewing Patrick Farrell · awaiting review (CI not green yet)
stalled 118d 25 unresolved XL +36039 −19
118d
build #122958 FAILURE
reviews: Ronnie Sahlberg +1 2026-03-27 21:35 · Andreas Dilger -1 2026-04-06 21:18
no vote yet: Qian Yingjin, Marc Vef
patchsets: PS1 03-27 PS2 03-27 PS3 03-27
threads: 25 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS1 2026-03-29 08:52 Andreas Dilger COMMIT_MSG:14 — It would make sense to put an LLM to work on converting the NASM .asm files int GCC .S files. It should be possible to have it iterate on g
Andreas Dilger PS1 · 2026-03-29 08:52
It would make sense to put an LLM to work on converting the NASM .asm files int GCC .S files.  It should be possible to have it iterate on getting the GCC-compiled code to generate the same x86 byte code, or at least compile and run correctly to pass whatever correctness tests exist for this code. 

In fact, a quick search shows that NASM already has a tool to do this:

    Automatically convert Intel NASM assembly to GNU Assembler (GAS/GASM)
    syntax using the intel2gas tool, which converts between NASM and AT&T
    syntax, or by utilizing NASM's built-in output capabilities to generate
    GAS-compatible objects.
    
    $ intel2gas -i input.asm -o output.s

These generated/converted .S files should be stored alongside the original .asm files in Git.  If NASM is available the originals can be compiled, but in the common case where NASM is not available the .S files would be compiled and linked into the kernel modules.

I think it is worthwhile to keep both, since it would be easier to update the .asm files from upstream ISA-L, but it might be some time before the .S files are update to match.
reply PS1 2026-03-29 08:52 Andreas Dilger COMMIT_MSG:26 — Are these bugs in the upstream ISA-L code that should be pushed back to them?
Andreas Dilger PS1 · 2026-03-29 08:52
Are these bugs in the upstream ISA-L code that should be pushed back to them?
reply PS1 2026-03-29 08:52 Andreas Dilger COMMIT_MSG:28 — Similarly, is this code from upstream ISA-L, or a test wrapper that we developed?
Andreas Dilger PS1 · 2026-03-29 08:52
Similarly, is this code from upstream ISA-L, or a test wrapper that we developed?
reply PS3 2026-04-06 21:18 Andreas Dilger COMMIT_MSG:7 — This should probably use LU-19905, or change the description of LU-20016?
Andreas Dilger PS3 · 2026-04-06 21:18
This should probably use LU-19905, or change the description of LU-20016?
reply PS3 2026-05-13 00:10 Andreas Dilger COMMIT_MSG:71 — This table is the same between the x86 and aarch64 patches. It would be useful to fix one or the other to have the correct data for that CP
Andreas Dilger PS3 · 2026-05-13 00:10
This table is the same between the x86 and aarch64 patches.  It would be useful to fix one or the other to have the correct data for that CPU architecture.
reply PS1 2026-03-29 08:52 Andreas Dilger lustre-erasurecode.m4:23 — (minor) this should have a warning that the C version may only be 1/20-1/30th as fast, at least until the .S versions are available.
Andreas Dilger PS1 · 2026-03-29 08:52
(minor) this should have a warning that the C version may only be 1/20-1/30th as fast, at least until the .S versions are available.
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:17 — (minor) It would be better to specify which kernel version is non-functional, so that this can become conditional upon a newer kernel (assum
Andreas Dilger PS3 · 2026-04-06 21:18
(minor) It would be better to specify which kernel version is non-functional, so that this can become conditional upon a newer kernel (assuming there is a benefit to use SVE-optimized versions).
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:20 — (minor) Again, please specify kernel versions so that this can be fixed in the future for newer kernel versions.
Andreas Dilger PS3 · 2026-04-06 21:18
(minor) Again, please specify kernel versions so that this can be fixed in the future for newer kernel versions.
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:31 — (style) `bool`?
Andreas Dilger PS3 · 2026-04-06 21:18
(style) `bool`?
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:34 — These warnings are correct and the prototypes should be moved to a .h file that is included here and into the file where these functions are
wc-checkpatch PS3 · 2026-03-27 13:45
(style)  externs should be avoided in .c files
Andreas Dilger PS3 · 2026-04-06 21:18
These warnings are correct and the prototypes should be moved to a .h file that is included here and into the file where these functions are implemented.  Having the function prototype in a local .c file prevents the compiler from checking argument type/count against the implementation, and can result in hard-to-find bugs in the future.
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:87 — (defect?) According to the comments for `kernel_neon_begin()`:
Andreas Dilger PS3 · 2026-04-06 21:18
(defect?) According to the comments for `kernel_neon_begin()`:
```
 * Unless called from non-preemptible task context, @state must point to a
 * caller provided buffer that will be used to preserve the task's kernel mode
 * FPSIMD context when it is scheduled out, or if it is interrupted by kernel
 * mode FPSIMD occurring in softirq context. May be %NULL otherwise.
 */
void kernel_neon_begin(struct user_fpsimd_state *state)
```
I'm thinking that this would be declared on the stack in e.g. `gf_vect_dot_prod()` and passed to `ec_neon_usable()->kernel_neon_begin()` so that it can also be passed (as required) to `kernel_neon_end()`:
```
 * The value of @state must match the value passed to the preceding call to     
 * kernel_neon_begin().
 */
void kernel_neon_end(struct user_fpsimd_state *state)
```

The `struct user_fpsimd_state` is 528 bytes, so a _bit_ heavy to put on the stack, but at the same time this is (very likely) to be on a PAGE_SIZE=64KiB system so stack space should be available for this, and it will not have a deep call chain below this point.  I think doing a `kmalloc()` and `kfree()` for each call would add noticeable overhead and should be avoided it possible.  Alternatives would include having a dedicated slab for this, so that there is a per-CPU cache (528 does not fit into standard slabs well) and it likely has local allocations objects cached in the slab.
reply PS3 2026-04-06 21:18 Andreas Dilger ec_aarch64_neon.c:165 — (style) should this be changed to `rows >= 6` and remove the `case 6:` in the switch, as is done in `ec_encode_data_neon()` above?
Andreas Dilger PS3 · 2026-04-06 21:18
(style) should this be changed to `rows >= 6` and remove the `case 6:` in the switch, as is done in `ec_encode_data_neon()` above?
owner Patrick Farrell · uploader Patrick Farrell · PS3 uploaded 166d ago · NEW · open in Gerrit ↗
commit message
LU-20016 ec: ISA-L SIMD for userspace and kernel

Integrate Intel ISA-L optimized assembly into Lustre's
erasure coding library for both userspace and kernel,
on x86_64 and aarch64. Add debugfs kernel benchmark
for measuring FPU save/restore overhead.

Userspace x86_64 (libec.a):
- 72 NASM assembly files (SSE/AVX/AVX2/AVX-512/GFNI)
- ec_multibinary.asm: runtime CPUID dispatch
- ec_highlevel_func.c: N-vector dispatch layer
- Falls back to C scalar when NASM unavailable

Userspace aarch64 (libec.a):
- NEON + SVE .S assembly + SVE C intrinsics
- ec_aarch64_dispatcher.c: getauxval() dispatch
- ec_aarch64_highlevel_func.c: N-vector dispatch

Kernel x86_64 (ec.ko):
- Pre-assemble ISA-L NASM .asm files into .o,
  link into ec.ko (71 assembly objects)
- ec_dispatch.c: boot_cpu_has() selects AVX2/AVX/SSE
  at module init, kernel_fpu_begin/end wrapping
- ec_highlevel_func.c: N-vector dispatch layer
- Falls back to C scalar in interrupt context

Kernel aarch64 (ec.ko):
- NEON .S assembly files (GAS format, direct kbuild)
- ec_aarch64_neon.c: kernel_neon_begin/end wrapping
  with may_use_simd() check, hwcap detection
- SVE excluded from kernel (toolchain portability)

Build system:
- lustre-erasurecode.m4: detect NASM, aarch64
- erasurecode/autoMakefile.am: three-way dispatch
- utils/Makefile.am: link against libec.a
- ec/Makefile.in: NASM pre-assembly for kernel

Bug fixes in ec_perf_bench.c:
- -p flag was setting k instead of p
- frag_ptrs allocated stripe_size pointers not m
- Inverted exit code and stale rc from getopt
- Use aligned_alloc(64) for data buffers

Kernel benchmark (debugfs):
- /sys/kernel/debug/lustre/ec/benchmark
- 4 phases: FPU overhead, SIMD+FPU per-call,
  SIMD+FPU amortized, C scalar baseline
- Input format (write to debugfs entry):
    echo 1 > .../ec/benchmark        (defaults)
    echo "k=5 p=2 s=128" > .../ec/benchmark
    cat .../ec/benchmark
  Parameters (key=value, space separated):
    k  data stripes     (default 5, max k+p=16)
    p  parity stripes   (default 2)
    s  stripe size in KB (default 128, max 65536)
  "echo 1" runs with all defaults. Unrecognized
  input returns -EINVAL. cat shows results or
  usage instructions if not yet run.

Fix gf_vect_mul_init naming to use _base suffix
consistently (aligns with ISA-L convention) in
both userspace and kernel ec_base.c.

Performance (5+2, 1 thread, QEMU VM, AVX2):

Stripe | Kernel  | Kernel  | Userspace | SIMD     | Kernel vs
Size   | SIMD    | Scalar  | SIMD      | Speedup  | Userspace
       |         |         |           | in Kernel|
-------|---------|---------|-----------|----------|----------
 64 KB | 17099   | 540     | 404345*   | 31.7x    | 0.04x*
128 KB | 16888   | 531     | 197511*   | 31.8x    | 0.09x*
256 KB | 17032   | 539     | 98401*    | 31.6x    | 0.17x*
  1 MB | 17142   | 538     | 24830     | 31.9x    | 0.69x
  4 MB | 10309   | 533     | 5385      | 19.3x    | 1.91x
 16 MB | 10954   | 534     | 1048      | 20.5x    | 10.5x

All throughput in MB/s. (*) Userspace numbers at
small stripes are inflated by cache residency.
At 4-16MB the kernel is faster -- its kvmalloc
pages are physically scattered, matching real
Lustre page cache behavior. Replicating this in
userspace is non-trivial and not worth the effort;
the kernel benchmark is authoritative.

FPU save/restore overhead was measured at 25-54 ns
on this platform (Ryzen 3700X) -- negligible vs
encode time (~39us at 128KB). The benchmark
measures this separately (Phase 1) so it can be
characterized on other hardware where it may be
more significant.

Generated with Claude Code + Tools

Test-Parameters: testlist=sanity-ec
Change-Id: If4c57c328c0e23bb6769dcbc45d52d43b03a7837
Signed-off-by: Patrick Farrell <pfarrell@whamcloud.com>

Voted on current patchset

(1)
52488 master J !B ✓T ✗1R 2/2
LU-17140 utils: l_getidentity remove 'nss_files' alias
reviewing Shaun Tancheff · you voted +1 on current PS
3 unresolved M +5 −66
31d
janitor run: 5 failing config(s), none unique to this patch — janitor results
failed enforced testplatformdetail
review-dne-zfs-part-5 failed 2× crashed RHEL 9.7 / x86_64 ran 5 tests. 1 tests failed: sanityn. %% THIS TEST SESSION CRASHED %% session
build #128949 SUCCESS · tests all Maloo sessions
reviews: Hiroshi Nishida +1 2026-08-08 22:33 · Andreas Dilger +1 2026-08-08 20:59
patchsets:
+3 earlier PS1 09-23 PS2 09-23 PS3 09-23
PS4 10-23 PS5 10-27 PS6 03-31 PS7 04-02 PS8 08-08
threads: 3 need your reply · 0 waiting on others · 0 own notes · 0 bot
reply PS8 2026-08-08 19:59 Gerrit AI review for Lustre l_getidentity.8:45 — (defect) The deprecation paragraph is gone, but this list of known modules still names `nss_files`. After this patch that token no longer ha
Gerrit AI review for Lustre PS8 · 2026-08-08 19:59
(defect) The deprecation paragraph is gone, but this list of known modules still names `nss_files`. After this patch that token no longer has a special case, so `lookup nss_files` falls through to init_nss_lib_module(newmod, "nss_files") and tries to dlopen `libnss_nss_files.so.2`, which fails and exits l_getidentity(1) -- the identity upcall then fails for every user on that MDT.

Should this read `files`? That matches what the removed warning told admins ("Use 'lookup files'"), and `files` has been the working spelling since LU-17139.
reply PS8 2026-08-08 19:59 Gerrit AI review for Lustre l_getidentity.c:27 — (minor) do_warn_interval() was the only user of `struct stat`/stat()/futimens() in this file; nothing left uses <sys/stat.h> now. If the pat
Gerrit AI review for Lustre PS8 · 2026-08-08 19:59
(minor) do_warn_interval() was the only user of `struct stat`/stat()/futimens() in this file; nothing left uses <sys/stat.h> now. If the patch is refreshed, this include could go with it.
reply PS8 2026-08-08 19:59 Gerrit AI review for Lustre l_getidentity.c:811 — (defect) This part of the comment describes the alias the patch just deleted -- it still tells the reader that `nss_files` must be selected
Gerrit AI review for Lustre PS8 · 2026-08-08 19:59
(defect) This part of the comment describes the alias the patch just deleted -- it still tells the reader that `nss_files` must be selected to enable libnss_files. Since LU-17139 that is done with plain `files`, and after this change `nss_files` is not recognized at all.

Can the last sentence be dropped or reworded to say `files` maps to libnss_files and `lustre` to /etc/lustre/passwd,group?
owner Shaun Tancheff · uploader Shaun Tancheff · PS8 uploaded 32d ago · NEW · open in Gerrit ↗
commit message
LU-17140 utils: l_getidentity remove 'nss_files' alias

Fully remove the old 'nss_files' alias and associated
warning code as it is not needed.

Signed-off-by: Shaun Tancheff <shaun.tancheff@hpe.com>
Change-Id: I32cc37a8cbae7586f8c1d53f72d1f6f21900f1e0

CC'd — on your radar, review not requested

(0)
You are not CC'd on any open changes.

Merged — last 30 days

(1)
67689 master J –B ✓T ✓R 3/2+2
LU-20551 tests: fix persistent sepol cleanup in sanity-selinux
mine
S +15 −2
16d
reviews: Andreas Dilger +1 2026-08-22 03:25 · Sebastien Buisson +1 2026-08-22 03:25 · Oleg Drokin +2 2026-08-22 03:25
patchsets: PS1 08-02 PS2 08-04 PS3 08-17
owner Hiroshi Nishida · uploader Oleg Drokin · PS4 uploaded 18d ago · MERGED · open in Gerrit ↗
commit message
LU-20551 tests: fix persistent sepol cleanup in sanity-selinux

test_21c cleans up with "lctl set_param -P sptlrpc.send_sepol=0",
which does not undo the test: it leaves a persistent MGS config
log entry where there was none before. Clients replay that log on
every mount, so a later test that sets send_sepol locally loses
it on the next mount, then exercises nothing while still passing.

Delete the entry instead, and reset the running value on the
clients. A second "-P" write is racy: "-P -d" marks the matching
sections skipped in place, including a "=0" written moments
earlier, so the restore can be dropped before any node applies
it, leaving send_sepol=-1 for every later suite. send_sepol is a
ptlrpc module global that umount does not reset, and only clients
expose sptlrpc.send_sepol, so the reset is scoped there.

"set_param -P -d" returns an error when there is no entry to
delete. Guard both call sites that run from an EXIT trap, where
errexit is active and a non-zero status skips the rest of the
handler: the new one in test_21c, and remove_nodemap(), which
cleanup_21b calls before test_21b has necessarily set a nodemap
sepol -- there it would leak the nodemap and leave the client
unmounted.

Fixes: b5bbdb49faca ("LU-20046 sptlrpc: expose send_sepol via lctl get/set_param")
Test-Parameters: trivial testgroup=review-dne-selinux-ssk-part-2
Signed-off-by: Hiroshi Nishida <hnishida@thelustrecollective.com>
Assisted-by: Claude:Opus-5 claude-code
Change-Id: I53cfb326a16f14a944715dc3f7c76b107e010fe0
Reviewed-on: https://review.whamcloud.com/c/fs/lustre-release/+/67689
Reviewed-by: Andreas Dilger <adilger@thelustrecollective.com>
Tested-by: jenkins <devops@whamcloud.com>
Reviewed-by: Sebastien Buisson <sbuisson@ddn.com>
Reviewed-by: Oleg Drokin <green@whamcloud.com>
Tested-by: Maloo <maloo@whamcloud.com>