Skip to content

Commit f6ce72b

Browse files
committed
zfs hold: extend to filesystems and volumes
Allow zfs hold/release/holds to operate on filesystems and volumes, not only snapshots, so users can protect datasets from accidental destruction with the existing hold mechanism. When a dataset has one or more user holds, zfs destroy returns EBUSY until all are released, matching snapshot behavior. Signed-off-by: Christos Longros <chris.longros@gmail.com>
1 parent 9ae9f2e commit f6ce72b

10 files changed

Lines changed: 269 additions & 60 deletions

File tree

cmd/zfs/zfs_main.c

Lines changed: 79 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -410,11 +410,11 @@ get_usage(zfs_help_t idx)
410410
"\tproject -C [-k] [-r] <directory ...>\n"
411411
"\tproject [-p id] [-r] [-s] <directory ...>\n"));
412412
case HELP_HOLD:
413-
return (gettext("\thold [-r] <tag> <snapshot> ...\n"));
413+
return (gettext("\thold [-r] <tag> <snapshot|dataset> ...\n"));
414414
case HELP_HOLDS:
415-
return (gettext("\tholds [-rHp] <snapshot> ...\n"));
415+
return (gettext("\tholds [-rHp] <snapshot|dataset> ...\n"));
416416
case HELP_RELEASE:
417-
return (gettext("\trelease [-r] <tag> <snapshot> ...\n"));
417+
return (gettext("\trelease [-r] <tag> <snapshot|dataset> ...\n"));
418418
case HELP_DIFF:
419419
return (gettext("\tdiff [-FHth] <snapshot> "
420420
"[snapshot|filesystem]\n"));
@@ -6677,6 +6677,63 @@ zfs_do_unallow(int argc, char **argv)
66776677
return (zfs_do_allow_unallow_impl(argc, argv, B_TRUE));
66786678
}
66796679

6680+
typedef struct dataset_hold_arg {
6681+
nvlist_t *nvl;
6682+
const char *tag;
6683+
boolean_t holding;
6684+
boolean_t recursive;
6685+
} dataset_hold_arg_t;
6686+
6687+
static int
6688+
zfs_dataset_hold_rele_one(zfs_handle_t *zhp, void *arg)
6689+
{
6690+
dataset_hold_arg_t *ha = arg;
6691+
int rv = 0;
6692+
const char *name = zfs_get_name(zhp);
6693+
6694+
if (ha->holding) {
6695+
fnvlist_add_string(ha->nvl, name, ha->tag);
6696+
} else {
6697+
nvlist_t *torelease = fnvlist_alloc();
6698+
fnvlist_add_boolean(torelease, ha->tag);
6699+
fnvlist_add_nvlist(ha->nvl, name, torelease);
6700+
fnvlist_free(torelease);
6701+
}
6702+
6703+
if (ha->recursive)
6704+
rv = zfs_iter_filesystems_v2(zhp, 0,
6705+
zfs_dataset_hold_rele_one, ha);
6706+
zfs_close(zhp);
6707+
return (rv);
6708+
}
6709+
6710+
static int
6711+
zfs_dataset_hold_rele_recurse(zfs_handle_t *zhp, const char *tag,
6712+
boolean_t holding, boolean_t recursive)
6713+
{
6714+
dataset_hold_arg_t ha = {
6715+
.nvl = fnvlist_alloc(),
6716+
.tag = tag,
6717+
.holding = holding,
6718+
.recursive = recursive,
6719+
};
6720+
int ret;
6721+
6722+
ret = zfs_dataset_hold_rele_one(zfs_handle_dup(zhp), &ha);
6723+
if (ret != 0) {
6724+
fnvlist_free(ha.nvl);
6725+
return (ret);
6726+
}
6727+
6728+
if (holding)
6729+
ret = zfs_hold_nvl(zhp, -1, ha.nvl);
6730+
else
6731+
ret = lzc_release(ha.nvl, NULL);
6732+
6733+
fnvlist_free(ha.nvl);
6734+
return (ret);
6735+
}
6736+
66806737
static int
66816738
zfs_do_hold_rele_impl(int argc, char **argv, boolean_t holding)
66826739
{
@@ -6725,9 +6782,16 @@ zfs_do_hold_rele_impl(int argc, char **argv, boolean_t holding)
67256782

67266783
delim = strchr(path, '@');
67276784
if (delim == NULL) {
6728-
(void) fprintf(stderr,
6729-
gettext("'%s' is not a snapshot\n"), path);
6730-
++errors;
6785+
zhp = zfs_open(g_zfs, path,
6786+
ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME);
6787+
if (zhp == NULL) {
6788+
++errors;
6789+
continue;
6790+
}
6791+
if (zfs_dataset_hold_rele_recurse(zhp, tag, holding,
6792+
recursive) != 0)
6793+
++errors;
6794+
zfs_close(zhp);
67316795
continue;
67326796
}
67336797
(void) strlcpy(parent, path, MIN(sizeof (parent),
@@ -6865,7 +6929,7 @@ holds_callback(zfs_handle_t *zhp, void *data)
68656929
const char *zname = zfs_get_name(zhp);
68666930
size_t znamelen = strlen(zname);
68676931

6868-
if (cbp->cb_recursive) {
6932+
if (cbp->cb_recursive && cbp->cb_snapname != NULL) {
68696933
const char *snapname;
68706934
const char *delim = strchr(zname, '@');
68716935
if (delim == NULL)
@@ -6955,9 +7019,14 @@ zfs_do_holds(int argc, char **argv)
69557019

69567020
delim = strchr(snapshot, '@');
69577021
if (delim == NULL) {
6958-
(void) fprintf(stderr,
6959-
gettext("'%s' is not a snapshot\n"), snapshot);
6960-
errors = B_TRUE;
7022+
cb.cb_recursive = recursive;
7023+
cb.cb_snapname = NULL;
7024+
cb.cb_nvlp = &nvl;
7025+
ret = zfs_for_each(1, argv + i, flags,
7026+
ZFS_TYPE_FILESYSTEM | ZFS_TYPE_VOLUME,
7027+
NULL, NULL, limit, holds_callback, &cb);
7028+
if (ret != 0)
7029+
errors = B_TRUE;
69617030
continue;
69627031
}
69637032
snapname = delim + 1;

man/man8/zfs-hold.8

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -30,61 +30,62 @@
3030
.\" Copyright 2018 Nexenta Systems, Inc.
3131
.\" Copyright 2019 Joyent, Inc.
3232
.\"
33-
.Dd November 8, 2022
33+
.Dd May 9, 2026
3434
.Dt ZFS-HOLD 8
3535
.Os
3636
.
3737
.Sh NAME
3838
.Nm zfs-hold
39-
.Nd hold ZFS snapshots to prevent their removal
39+
.Nd hold ZFS snapshots or datasets to prevent their removal
4040
.Sh SYNOPSIS
4141
.Nm zfs
4242
.Cm hold
4343
.Op Fl r
44-
.Ar tag Ar snapshot Ns
44+
.Ar tag Ar snapshot Ns | Ns Ar dataset Ns
4545
.Nm zfs
4646
.Cm holds
4747
.Op Fl rHp
48-
.Ar snapshot Ns
48+
.Ar snapshot Ns | Ns Ar dataset Ns
4949
.Nm zfs
5050
.Cm release
5151
.Op Fl r
52-
.Ar tag Ar snapshot Ns
52+
.Ar tag Ar snapshot Ns | Ns Ar dataset Ns
5353
.
5454
.Sh DESCRIPTION
5555
.Bl -tag -width ""
5656
.It Xo
5757
.Nm zfs
5858
.Cm hold
5959
.Op Fl r
60-
.Ar tag Ar snapshot Ns
60+
.Ar tag Ar snapshot Ns | Ns Ar dataset Ns
6161
.Xc
6262
Adds a single reference, named with the
6363
.Ar tag
64-
argument, to the specified snapshots.
65-
Each snapshot has its own tag namespace, and tags must be unique within that
64+
argument, to the specified snapshots or datasets
65+
.Pq filesystems and volumes .
66+
Each target has its own tag namespace, and tags must be unique within that
6667
space.
6768
.Pp
68-
If a hold exists on a snapshot, attempts to destroy that snapshot by using the
69+
If a hold exists on a snapshot or dataset, attempts to destroy it with
6970
.Nm zfs Cm destroy
70-
command return
71+
return
7172
.Sy EBUSY .
7273
.Bl -tag -width "-r"
7374
.It Fl r
74-
Specifies that a hold with the given tag is applied recursively to the snapshots
75-
of all descendent file systems.
75+
Apply the hold recursively to the named target and all descendent
76+
file systems
77+
.Pq for snapshots, to the same-named snapshot of each descendent .
7678
.El
7779
.It Xo
7880
.Nm zfs
7981
.Cm holds
8082
.Op Fl rHp
81-
.Ar snapshot Ns
83+
.Ar snapshot Ns | Ns Ar dataset Ns
8284
.Xc
83-
Lists all existing user references for the given snapshot or snapshots.
85+
Lists all existing user references for the given snapshots or datasets.
8486
.Bl -tag -width "-r"
8587
.It Fl r
86-
Lists the holds that are set on the named descendent snapshots, in addition to
87-
listing the holds on the named snapshot.
88+
List holds set on the named target and on all descendent file systems.
8889
.It Fl H
8990
Do not print headers, use tab-delimited output.
9091
.It Fl p
@@ -94,20 +95,20 @@ Prints holds timestamps as Unix epoch timestamps.
9495
.Nm zfs
9596
.Cm release
9697
.Op Fl r
97-
.Ar tag Ar snapshot Ns
98+
.Ar tag Ar snapshot Ns | Ns Ar dataset Ns
9899
.Xc
99100
Removes a single reference, named with the
100101
.Ar tag
101-
argument, from the specified snapshot or snapshots.
102-
The tag must already exist for each snapshot.
103-
If a hold exists on a snapshot, attempts to destroy that snapshot by using the
102+
argument, from the specified snapshots or datasets.
103+
The tag must already exist for each target.
104+
If a hold exists on a snapshot or dataset, attempts to destroy it with
104105
.Nm zfs Cm destroy
105-
command return
106+
return
106107
.Sy EBUSY .
107108
.Bl -tag -width "-r"
108109
.It Fl r
109-
Recursively releases a hold with the given tag on the snapshots of all
110-
descendent file systems.
110+
Recursively release the hold from the named target and all descendent
111+
file systems.
111112
.El
112113
.El
113114
.

man/man8/zfs.8

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -186,10 +186,10 @@ Creates snapshots with the given names.
186186
.It Xr zfs-rollback 8
187187
Roll back the given dataset to a previous snapshot.
188188
.It Xr zfs-hold 8 Ns / Ns Xr zfs-release 8
189-
Add or remove a hold reference to the specified snapshot or snapshots.
190-
If a hold exists on a snapshot, attempts to destroy that snapshot by using the
189+
Add or remove a hold reference on the specified snapshots or datasets.
190+
If a hold exists, attempts to destroy the held target with
191191
.Nm zfs Cm destroy
192-
command return
192+
return
193193
.Sy EBUSY .
194194
.It Xr zfs-diff 8
195195
Display the difference between a snapshot of a given filesystem and another

module/zfs/dsl_dataset.c

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -676,13 +676,14 @@ dsl_dataset_hold_obj(dsl_pool_t *dp, uint64_t dsobj, const void *tag,
676676
} else {
677677
if (zfs_flags & ZFS_DEBUG_SNAPNAMES)
678678
err = dsl_dataset_get_snapname(ds);
679-
if (err == 0 &&
680-
dsl_dataset_phys(ds)->ds_userrefs_obj != 0) {
681-
err = zap_count(
682-
ds->ds_dir->dd_pool->dp_meta_objset,
683-
dsl_dataset_phys(ds)->ds_userrefs_obj,
684-
&ds->ds_userrefs);
685-
}
679+
}
680+
681+
if (err == 0 &&
682+
dsl_dataset_phys(ds)->ds_userrefs_obj != 0) {
683+
err = zap_count(
684+
ds->ds_dir->dd_pool->dp_meta_objset,
685+
dsl_dataset_phys(ds)->ds_userrefs_obj,
686+
&ds->ds_userrefs);
686687
}
687688

688689
if (err == 0 && !ds->ds_is_snapshot) {

module/zfs/dsl_destroy.c

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,9 @@ dsl_destroy_head_check_impl(dsl_dataset_t *ds, int expected_holds)
776776
if (zfs_refcount_count(&ds->ds_longholds) != expected_holds)
777777
return (SET_ERROR(EBUSY));
778778

779+
if (ds->ds_userrefs > 0)
780+
return (SET_ERROR(EBUSY));
781+
779782
ASSERT0(ds->ds_dir->dd_activity_waiters);
780783

781784
mos = ds->ds_dir->dd_pool->dp_meta_objset;
@@ -1159,7 +1162,10 @@ dsl_destroy_head_sync_impl(dsl_dataset_t *ds, dmu_tx_t *tx)
11591162

11601163
ASSERT0(dsl_dataset_phys(ds)->ds_next_clones_obj);
11611164
ASSERT0(dsl_dataset_phys(ds)->ds_props_obj);
1162-
ASSERT0(dsl_dataset_phys(ds)->ds_userrefs_obj);
1165+
if (dsl_dataset_phys(ds)->ds_userrefs_obj != 0) {
1166+
VERIFY0(zap_destroy(mos,
1167+
dsl_dataset_phys(ds)->ds_userrefs_obj, tx));
1168+
}
11631169
dsl_dir_rele(ds->ds_dir, ds);
11641170
ds->ds_dir = NULL;
11651171
dmu_object_free_zapified(mos, obj, tx);

module/zfs/dsl_userhold.c

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,8 @@ dsl_dataset_user_hold_check(void *arg, dmu_tx_t *tx)
118118
int error = 0;
119119
const char *htag, *name;
120120

121-
/* must be a snapshot */
122121
name = nvpair_name(pair);
123-
if (strchr(name, '@') == NULL)
124-
error = SET_ERROR(EINVAL);
125-
126-
if (error == 0)
127-
error = nvpair_value_string(pair, &htag);
122+
error = nvpair_value_string(pair, &htag);
128123

129124
if (error == 0)
130125
error = dsl_dataset_hold(dp, name, FTAG, &ds);
@@ -375,9 +370,6 @@ dsl_dataset_user_release_check_one(dsl_dataset_user_release_arg_t *ddura,
375370
objset_t *mos;
376371
int numholds;
377372

378-
if (!ds->ds_is_snapshot)
379-
return (SET_ERROR(EINVAL));
380-
381373
if (nvlist_empty(holds))
382374
return (0);
383375

module/zfs/zfs_ioctl.c

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1401,9 +1401,16 @@ zfs_secpolicy_hold(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr)
14011401
for (pair = nvlist_next_nvpair(holds, NULL); pair != NULL;
14021402
pair = nvlist_next_nvpair(holds, pair)) {
14031403
char fsname[ZFS_MAX_DATASET_NAME_LEN];
1404-
error = dmu_fsname(nvpair_name(pair), fsname);
1405-
if (error != 0)
1406-
return (error);
1404+
const char *name = nvpair_name(pair);
1405+
if (strchr(name, '@') != NULL) {
1406+
error = dmu_fsname(name, fsname);
1407+
if (error != 0)
1408+
return (error);
1409+
} else {
1410+
if (strlcpy(fsname, name, sizeof (fsname)) >=
1411+
sizeof (fsname))
1412+
return (SET_ERROR(ENAMETOOLONG));
1413+
}
14071414
error = zfs_secpolicy_write_perms(fsname,
14081415
ZFS_DELEG_PERM_HOLD, cr);
14091416
if (error != 0)
@@ -1422,9 +1429,16 @@ zfs_secpolicy_release(zfs_cmd_t *zc, nvlist_t *innvl, cred_t *cr)
14221429
for (pair = nvlist_next_nvpair(innvl, NULL); pair != NULL;
14231430
pair = nvlist_next_nvpair(innvl, pair)) {
14241431
char fsname[ZFS_MAX_DATASET_NAME_LEN];
1425-
error = dmu_fsname(nvpair_name(pair), fsname);
1426-
if (error != 0)
1427-
return (error);
1432+
const char *name = nvpair_name(pair);
1433+
if (strchr(name, '@') != NULL) {
1434+
error = dmu_fsname(name, fsname);
1435+
if (error != 0)
1436+
return (error);
1437+
} else {
1438+
if (strlcpy(fsname, name, sizeof (fsname)) >=
1439+
sizeof (fsname))
1440+
return (SET_ERROR(ENAMETOOLONG));
1441+
}
14281442
error = zfs_secpolicy_write_perms(fsname,
14291443
ZFS_DELEG_PERM_RELEASE, cr);
14301444
if (error != 0)

tests/runfiles/common.run

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ tests = ['zfs_clone_livelist_condense_and_disable',
232232
'zfs_destroy_007_neg', 'zfs_destroy_008_pos', 'zfs_destroy_009_pos',
233233
'zfs_destroy_010_pos', 'zfs_destroy_011_pos', 'zfs_destroy_012_pos',
234234
'zfs_destroy_013_neg', 'zfs_destroy_014_pos', 'zfs_destroy_015_pos',
235-
'zfs_destroy_016_pos', 'zfs_destroy_clone_livelist',
235+
'zfs_destroy_016_pos', 'zfs_destroy_017_neg', 'zfs_destroy_clone_livelist',
236236
'zfs_destroy_dev_removal', 'zfs_destroy_dev_removal_condense']
237237
tags = ['functional', 'cli_root', 'zfs_destroy']
238238

tests/zfs-tests/tests/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,7 @@ nobase_dist_datadir_zfs_tests_tests_SCRIPTS += \
743743
functional/cli_root/zfs_destroy/zfs_destroy_014_pos.ksh \
744744
functional/cli_root/zfs_destroy/zfs_destroy_015_pos.ksh \
745745
functional/cli_root/zfs_destroy/zfs_destroy_016_pos.ksh \
746+
functional/cli_root/zfs_destroy/zfs_destroy_017_neg.ksh \
746747
functional/cli_root/zfs_destroy/zfs_destroy_clone_livelist.ksh \
747748
functional/cli_root/zfs_destroy/zfs_destroy_dev_removal_condense.ksh \
748749
functional/cli_root/zfs_destroy/zfs_destroy_dev_removal.ksh \

0 commit comments

Comments
 (0)