Device Attributes and sysfs Files

A device attribute is the device-model’s typed wrapper around a raw sysfs file: a struct device_attribute bundles a filename and permission bits (an embedded struct attribute) with a show/store callback pair whose signatures take a struct device * directly, so a driver author writes static ssize_t speed_show(struct device *dev, struct device_attribute *attr, char *buf) instead of fishing the device out of a generic struct kobject by hand (device.h, v6.12). The DEVICE_ATTR_RO/RW/WO macros declare these in one line; attribute groups (struct attribute_group, materialized by ATTRIBUTE_GROUPS) bundle many attributes so the driver core creates and destroys them atomically alongside the device, the driver, the bus, or the class that owns them. This note covers the device-model-specific attribute layer. The generic sysfs/kobject/kernfs plumbing and the page-buffer show/store contract live in sysfs and the Kernel Object Hierarchy — read that first; this note assumes it and extends it downward into struct device. Pinned to the 6.12 Long-Term-Support (LTS) kernel (released 2024-11-17).

The Generic Contract, in One Paragraph

Everything in /sys is a kobject (kernel object) projected as a directory, and every file in that directory is a struct attribute — a two-field struct holding a name and umode_t mode (permission bits). A read or write on the file is routed by the Virtual File System through the kobject’s type (kobj_type), whose sysfs_ops supplies a generic show(struct kobject *, struct attribute *, char *) / store(...) pair. sysfs hands show() a one-page (PAGE_SIZE, 4096 bytes on x86) buffer and calls it exactly once per read; the method must fill the entire value in one shot, and should format it with the bounds-checked sysfs_emit(). The cardinal rule is one value per file (sysfs.rst). All of that — kernfs, the page-buffer protocol, sysfs_emit, __ATTR_RO/RW/WO, the kobject/kset/ktype triad — is explained in sysfs and the Kernel Object Hierarchy and is not re-derived here. What follows is the layer the device model adds on top: a device_attribute whose callbacks already carry a struct device *, and groups that are wired to the device’s groups, the driver’s dev_groups, the bus’s dev_groups, and the class’s dev_groups so files appear and vanish at exactly the right lifecycle moment.

Mental Model

Think of a device attribute as a down-cast of the generic attribute machinery, performed once, in the driver core, so every driver doesn’t have to. The kernel registers one generic sysfs_ops for all devices (dev_sysfs_ops, whose show is dev_attr_show). That single function uses container_of() to recover the struct device_attribute from the generic struct attribute, and kobj_to_dev() to recover the struct device from the generic struct kobject, then calls your attribute’s show(dev, dev_attr, buf). So the driver author never touches a struct kobject — by the time their callback runs, the generic types have already been translated to the device types. Groups are the second idea: rather than registering attributes one at a time (which is racy and leaks on error paths), you declare an array of attributes, wrap it in a struct attribute_group, and point one of the four *_groups fields at it. The driver core then walks the group at the right lifecycle event and creates every file in it as a unit — and removes them as a unit when the device unregisters, the driver unbinds, or the bus/class goes away.

flowchart TB
  RD["userspace: cat /sys/.../speed"] --> VFS["VFS read()"]
  VFS --> KOBJ["kobject sysfs_ops.show<br/>= dev_attr_show()"]
  KOBJ -->|"container_of(attr,<br/>device_attribute, attr)"| DA["struct device_attribute"]
  KOBJ -->|"kobj_to_dev(kobj)"| DEV["struct device *dev"]
  DA -->|"dev_attr->show(dev, dev_attr, buf)"| CB["your speed_show(dev, attr, buf)"]
  CB -->|"sysfs_emit(buf, ...)"| RD

  subgraph GRP["Where a device's files come from (lifecycle)"]
    CLSG["class->dev_groups<br/>(at device_add)"]
    TYPG["dev->type->groups<br/>(at device_add)"]
    DEVG["dev->groups<br/>(at device_add)"]
    BUSG["bus->dev_groups<br/>(when joining bus)"]
    DRVG["driver->dev_groups<br/>(at bind/probe)"]
  end
  GRP -.->|"each materialized as files"| DEV

The two halves of a device attribute. What it shows (top): a cat on a sysfs file is dispatched through the one generic dev_attr_show, which container_ofs up to your device_attribute and kobj_to_devs up to the struct device, then calls your typed show — the down-cast you never write yourself. (bottom): the five sources of a device’s attribute files and when each is created — class/type/device groups at device_add(), the bus’s device groups when the device joins the bus, and the driver’s dev_groups only at bind time. The insight: which *_groups field you choose decides the lifetime of the files, not just their content.

struct device_attribute: Why the Signature Differs

The base struct attribute carries no behavior — just name and mode. The device model’s wrapper, from include/linux/device.h, embeds it and adds the two callbacks:

struct device_attribute {
	struct attribute	attr;
	ssize_t (*show)(struct device *dev, struct device_attribute *attr,
			char *buf);
	ssize_t (*store)(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count);
};

Contrast this with the raw kobject sysfs_ops signature, show(struct kobject *, struct attribute *, char *). The device-model show/store take a struct device * and a struct device_attribute * instead of the generic kobject and attribute pointers. That difference is the whole point of the layer. The translation happens in one place — dev_attr_show in drivers/base/core.c:

#define to_dev_attr(_attr) container_of(_attr, struct device_attribute, attr)
 
static ssize_t dev_attr_show(struct kobject *kobj, struct attribute *attr,
			     char *buf)
{
	struct device_attribute *dev_attr = to_dev_attr(attr);
	struct device *dev = kobj_to_dev(kobj);
	ssize_t ret = -EIO;
 
	if (dev_attr->show)
		ret = dev_attr->show(dev, dev_attr, buf);
	if (ret >= (ssize_t)PAGE_SIZE) {
		printk("dev_attr_show: %pS returned bad count\n",
				dev_attr->show);
	}
	return ret;
}
 
static const struct sysfs_ops dev_sysfs_ops = {
	.show	= dev_attr_show,
	.store	= dev_attr_store,
};

Walk it: to_dev_attr(attr) is container_of(attr, struct device_attribute, attr) — given the address of the embedded attr field, it computes the address of the enclosing device_attribute by subtracting the field’s offset. kobj_to_dev(kobj) does the same trick to recover the struct device whose kobj member was passed in. Then it calls your show(dev, dev_attr, buf). The if (ret >= PAGE_SIZE) check catches the classic bug of a show that overran its one-page buffer (see sysfs and the Kernel Object Hierarchy for the page-buffer contract). dev_sysfs_ops is registered once, on the device_ktype that every struct device shares — so this dispatch is the single chokepoint for all device attribute reads in the kernel.

Declaring Attributes: DEVICE_ATTR and Its Shorthands

Hand-initializing a device_attribute is verbose, so device.h provides macros. The fully explicit one:

#define DEVICE_ATTR(_name, _mode, _show, _store) \
	struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

DEVICE_ATTR(foo, 0644, foo_show, foo_store) expands (per the doc comment) to a struct device_attribute dev_attr_foo with .attr = { .name = "foo", .mode = 0644 }, .show = foo_show, .store = foo_store. The dev_attr_ prefix on the symbol name is a convention the macros enforce — group arrays and device_create_file() both expect to find dev_attr_foo. The convenience trio, which is what almost all modern drivers use, hard-codes the mode and infers the callback names:

#define DEVICE_ATTR_RW(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_RW(_name)   /* 0644, foo_show + foo_store */
#define DEVICE_ATTR_RO(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_RO(_name)   /* 0444, foo_show only */
#define DEVICE_ATTR_WO(_name) \
	struct device_attribute dev_attr_##_name = __ATTR_WO(_name)   /* 0200, foo_store only */

The mode bits are not arbitrary: __ATTR_RO forces 0444 (world-readable, no write), __ATTR_WO forces 0200 (root write only, no read), and __ATTR_RW forces 0644 (world-read, root-write) (sysfs.h). So DEVICE_ATTR_RW(speed) requires you to have defined speed_show and speed_store with the device-model signatures; the macro stitches them together and names the result dev_attr_speed. There are also DEVICE_ATTR_ADMIN_RO/ADMIN_RW (mode 0400/0600, root-only even for read) for sensitive values, and self-writing wrappers DEVICE_ULONG_ATTR/DEVICE_INT_ATTR/DEVICE_BOOL_ATTR/DEVICE_STRING_ATTR_RO that bind the file directly to a variable via a struct dev_ext_attribute (which carries a void *var) and the prebuilt device_show_ulong/device_store_ulong/etc. handlers — no custom callback needed at all for a simple scalar knob.

The sibling subsystems get parallel macro families with the same embedding trick but their own object pointer in the callback: BUS_ATTR_RO/RW/WO define a struct bus_attribute (callback takes a struct bus_type *), DRIVER_ATTR_RO/RW/WO a struct driver_attribute (takes struct device_driver *), and CLASS_ATTR_RO/RW/WO a struct class_attribute (takes struct class *) (bus.h, driver.h, class.h). Same pattern, four levels of the object hierarchy.

Attribute Groups: Atomic Create and Remove

Registering attributes one-by-one with device_create_file() is discouraged: each call is a separate syscall-like operation, the error-unwinding is tedious, and there is a window where some files exist and others don’t. The modern idiom is the group, from sysfs.h:

struct attribute_group {
	const char		*name;
	umode_t			(*is_visible)(struct kobject *, struct attribute *, int);
	umode_t			(*is_bin_visible)(struct kobject *, struct bin_attribute *, int);
	struct attribute	**attrs;
	struct bin_attribute	**bin_attrs;
};

attrs is a NULL-terminated array of plain struct attribute *. If name is set, the group’s files go in a subdirectory of that name (e.g. power/); if name is NULL, they land directly in the device’s directory. The ATTRIBUTE_GROUPS(foo) macro is the boilerplate-killer:

#define ATTRIBUTE_GROUPS(_name)					\
static const struct attribute_group _name##_group = {		\
	.attrs = _name##_attrs,					\
};								\
__ATTRIBUTE_GROUPS(_name)        /* -> static const struct attribute_group *_name##_groups[] = { &_name##_group, NULL }; */

So given an array static struct attribute *foo_attrs[] = { &dev_attr_speed.attr, &dev_attr_duplex.attr, NULL };, the single line ATTRIBUTE_GROUPS(foo) produces both foo_group (the group) and foo_groups (the NULL-terminated array of group pointers) — exactly the type the *_groups fields below expect.

The Four *_groups Fields and Their Lifetimes

This is the device-model-specific heart of the matter: which field you attach a group to determines when its files exist. The driver core assembles a device’s attributes from several sources at device_add() time, in drivers/base/core.c:

static int device_add_attrs(struct device *dev)
{
	const struct class *class = dev->class;
	const struct device_type *type = dev->type;
	int error;
 
	if (class) {
		error = device_add_groups(dev, class->dev_groups);   /* class-wide device attrs */
		...
	}
	if (type) {
		error = device_add_groups(dev, type->groups);        /* device_type attrs */
		...
	}
	error = device_add_groups(dev, dev->groups);             /* this instance's attrs */
	...
}

So three of the four sources fire when the device is added: dev->class->dev_groups (attributes every device of this class gets — e.g. every net device gets address, mtu), dev->type->groups (attributes for this device_type), and dev->groups (attributes this specific instance was given). All three are torn down symmetrically in device_remove_attrs() when the device unregisters.

The bus’s device attributes are added separately, when the device joins the bus, in drivers/base/bus.c: device_add_groups(dev, sp->bus->dev_groups). Every device on, say, the PCI bus gets the PCI bus’s dev_groups (e.g. vendor, device, config).

The driver’s device attributes are the interesting case: driver->dev_groups is added only at bind time, inside really_probe() in drivers/base/dd.c:

	ret = device_add_groups(dev, drv->dev_groups);
	if (ret) {
		dev_err(dev, "device_add_groups() failed\n");
		goto dev_groups_failed;
	}

and removed at unbind (device_remove_groups(dev, dev->driver->dev_groups)). The doc comment on struct device_driver says it plainly: dev_groups are “Additional attributes attached to device instance once it is bound to the driver” (driver.h). This is why dev_groups on a driver auto-creates and auto-removes files at bind/unbind — the files physically appear in the device’s sysfs directory the instant .probe() succeeds and vanish the instant the driver is unbound (e.g. echo <dev> > /sys/bus/.../driver/unbind). Note the distinction from the driver’s own groups field, which creates attributes on the driver’s directory (/sys/bus/.../drivers/<drv>/), not the device’s. The full per-device attribute set is therefore the union: class device groups ∪ type groups ∪ instance groups ∪ bus device groups ∪ (bound) driver device groups.

Conditional Visibility: is_visible

Sometimes an attribute should exist only on some devices of a class — a temperature sensor’s crit file only if the hardware has a critical threshold. Rather than building different groups per device, you keep one group and supply an is_visible callback. For each attribute in the group, the core calls is_visible(kobj, attr, n) (where n is the attribute’s index in the array) and uses the returned umode_t as the file’s mode — returning 0 omits the file entirely (sysfs.h). The doc: “Must return 0 if an attribute is not visible. The returned value will replace static permissions defined in struct attribute.” A sketch:

static umode_t sensor_is_visible(struct kobject *kobj, struct attribute *a, int n)
{
	struct device *dev = kobj_to_dev(kobj);
	struct sensor *s = dev_get_drvdata(dev);
 
	if (a == &dev_attr_crit.attr && !s->has_crit_threshold)
		return 0;            /* hide crit on hardware that lacks it */
	return a->mode;              /* otherwise use the declared mode */
}
 
static const struct attribute_group sensor_group = {
	.attrs = sensor_attrs,
	.is_visible = sensor_is_visible,
};

is_bin_visible is the equivalent hook for binary attributes. For named groups, is_visible/is_bin_visible can additionally return SYSFS_GROUP_INVISIBLE to hide the entire subdirectory; the DEFINE_SYSFS_GROUP_VISIBLE / SYSFS_GROUP_VISIBLE helper macros wire up separate group-level and attribute-level predicates (sysfs.h).

Binary Attributes

Text-with-one-value is the rule, but some data is inherently binary and larger than a page — a device’s raw EEPROM, a PCI device’s config space, firmware blobs. For these, the device model uses struct bin_attribute (sysfs.h):

struct bin_attribute {
	struct attribute	attr;
	size_t			size;
	void			*private;
	struct address_space *(*f_mapping)(void);
	ssize_t (*read)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
	ssize_t (*write)(struct file *, struct kobject *, struct bin_attribute *, char *, loff_t, size_t);
	loff_t (*llseek)(struct file *, struct kobject *, struct bin_attribute *, loff_t, int);
	int (*mmap)(struct file *, struct kobject *, struct bin_attribute *, struct vm_area_struct *);
};

The key differences from a text attribute: read/write take an explicit loff_t offset and a size_t count, so the file is seekable and chunked rather than the one-shot page-buffer model — this is how /sys/bus/pci/devices/.../config lets you pread() 4 bytes at offset 0x10 to read a Base Address Register. size advertises the total length; an mmap hook can even map the data directly. Declared with BIN_ATTR_RO(name, size) / BIN_ATTR_RW(name, size), grouped with bin_attrs in an attribute_group (or the BIN_ATTRIBUTE_GROUPS macro), and attached to a device with device_create_bin_file() (device.h). Binary attributes deliberately escape the one-value-per-file discipline because their content is opaque bytes, not a human-parseable value.

A Complete Worked Example

A minimal driver exposing one read-only and one read-write device attribute, grouped:

/* show: read the current value out of driver state */
static ssize_t speed_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct mydev *m = dev_get_drvdata(dev);
	return sysfs_emit(buf, "%u\n", m->link_speed);     /* one value, bounds-checked */
}
static DEVICE_ATTR_RO(speed);                          /* -> dev_attr_speed, mode 0444 */
 
/* store: parse one value back into driver state */
static ssize_t mtu_store(struct device *dev, struct device_attribute *attr,
			 const char *buf, size_t count)
{
	struct mydev *m = dev_get_drvdata(dev);
	unsigned int v;
	int ret = kstrtouint(buf, 10, &v);                 /* parse the whole buffer */
	if (ret)
		return ret;                                /* bad input -> error to userspace */
	m->mtu = v;
	return count;                                      /* consumed all bytes */
}
static ssize_t mtu_show(struct device *dev, struct device_attribute *attr, char *buf)
{
	struct mydev *m = dev_get_drvdata(dev);
	return sysfs_emit(buf, "%u\n", m->mtu);
}
static DEVICE_ATTR_RW(mtu);                            /* -> dev_attr_mtu, mode 0644 */
 
static struct attribute *mydev_attrs[] = {
	&dev_attr_speed.attr,
	&dev_attr_mtu.attr,
	NULL,                                              /* NULL terminator is mandatory */
};
ATTRIBUTE_GROUPS(mydev);                               /* defines mydev_group + mydev_groups */
 
/* attach to *every device this driver binds*, created at probe, removed at unbind: */
static struct platform_driver mydev_driver = {
	.driver = {
		.name       = "mydev",
		.dev_groups = mydev_groups,                /* <-- bind-time attribute set */
	},
	.probe  = mydev_probe,
	.remove = mydev_remove,
};

Commentary: speed_show and mtu_show use sysfs_emit() — the bounds-checked formatter mandated for new code (see sysfs and the Kernel Object Hierarchy). mtu_store parses the entire buffer with kstrtouint and returns count to signal it consumed everything; returning a short count or an error has defined meanings. The two attributes are gathered into mydev_attrs (NULL-terminated), wrapped by ATTRIBUTE_GROUPS(mydev), and the resulting mydev_groups is attached to the driver’s dev_groups. The payoff: /sys/devices/.../speed and /sys/devices/.../mtu materialize the moment mydev_probe succeeds and disappear the moment the driver unbinds — no manual device_create_file/device_remove_file bookkeeping, no error-path leaks. Had the same mydev_groups been attached to dev->groups instead, the files would instead live for the device’s whole lifetime regardless of which driver (if any) is bound.

Failure Modes and Common Misunderstandings

  • Wrong callback signature. Copying a raw-kobject show(struct kobject *, struct attribute *, char *) into a place expecting a device_attribute show(struct device *, struct device_attribute *, char *) compiles only if the function-pointer types happen to match by luck and otherwise fails — the two are distinct. The DEVICE_ATTR_RO/RW/WO macros prevent the mismatch by requiring name_show/name_store with the device-model signatures; use them rather than hand-rolling.
  • Forgetting the NULL terminator in the attrs[] array. The core walks the array until it sees NULL; a missing terminator reads off the end and corrupts memory or creates garbage files. ATTRIBUTE_GROUPS does not add it for you — you must end the array with NULL.
  • Attaching to the wrong *_groups field. Putting a group on dev->groups when you wanted bind-time lifetime (it should have been the driver’s dev_groups) means the files persist after unbind and reappear stale; putting it on the driver’s groups (driver directory) when you meant dev_groups (device directory) puts the files in the wrong place entirely. The four fields look similar and are a frequent mix-up.
  • is_visible returning the wrong type. It returns a umode_t mode, not a bool. Returning 1 makes a file with mode 0001 (world-execute only — unreadable garbage); you must return attr->mode (or 0 to hide). This is a classic bug.
  • Racing attribute creation against probe. Creating attributes manually inside .probe() with device_create_file() races with userspace that is already watching the [[Uevents and the Kernel-Userspace Netlink Channel|KOBJ_ADD uevent]] — udev may read the device before the files exist. Using dev_groups avoids this because the core creates the group before it emits the bind-completion signal. This is a real reason groups are preferred over manual creation.
  • Exceeding one page in a binary-ish text attribute. If a value genuinely needs more than ~4 KB or arbitrary offsets, it is a bin_attribute, not a text attribute. Forcing large structured data through a text show() truncates at PAGE_SIZE (the dev_attr_show printk warning above fires).

Alternatives and When to Choose Them

  • Manual device_create_file() / device_remove_file() — still exists for the rare case where an attribute’s existence depends on runtime state discovered after device_add() and is_visible is awkward. The cost is manual lifetime management and the probe race above; prefer groups.
  • debugfs files — for throwaway debugging knobs with no Application Binary Interface (ABI) promise. Device attributes in /sys are ABI: once shipped, tools parse the exact path and single-value format, and the kernel cannot rename or restructure them without breaking userspace (every new sysfs attribute must be documented under Documentation/ABI, per sysfs-rules.rst). If a value is debug-only, it belongs in debugfs, not as a device attribute.
  • ioctl on the device’s /dev node — for complex, structured, or high-frequency control that does not fit the one-value-per-file model. Device attributes are for simple scalar knobs and status; an ioctl is for transactions. See file_operations and the Character Driver Interface.
  • A bin_attribute — when the data is genuinely binary and/or larger than a page (config space, EEPROM, firmware). Covered above.

Production Notes

The device-attribute layer is the everyday face of Linux hardware management. ethtool and ip read /sys/class/net/<iface>/speed, mtu, address — class dev_groups on the net class. lspci -v and libpci read PCI bus dev_groups (vendor, device, class) and the config binary attribute. CPU frequency governors write /sys/devices/system/cpu/cpu0/cpufreq/scaling_setspeed — a DEVICE_ATTR-style store. Hardware-monitoring (hwmon/lm-sensors) is built almost entirely on conditionally-visible groups: a chip driver declares one big group of tempN_input, tempN_crit, fanN_input, … and an is_visible callback that exposes only the channels the specific chip actually has — this is the canonical real-world use of is_visible, and reading the drivers/hwmon/ tree is the best way to see groups and conditional visibility used at scale. Because these are ABI, the stability matters operationally: a monitoring agent that parses /sys/class/hwmon/hwmon0/temp1_input can rely on that path and its single-integer (millidegree) format across kernel versions.

See Also