| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Virtual ALSA driver for PCM testing/fuzzing | |
| * | |
| * Copyright 2023 Ivan Orlov <ivan.orlov0322@gmail.com> | |
| * | |
| * This is a simple virtual ALSA driver, which can be used for audio applications/PCM middle layer | |
| * testing or fuzzing. | |
| * It can: | |
| * - Simulate 'playback' and 'capture' actions | |
| * - Generate random or pattern-based capture data | |
| * - Check playback buffer for containing looped template, and notify about the results | |
| * through the debugfs entry | |
| * - Inject delays into the playback and capturing processes. See 'inject_delay' parameter. | |
| * - Inject errors during the PCM callbacks. | |
| * - Register custom RESET ioctl and notify when it is called through the debugfs entry | |
| * - Work in interleaved and non-interleaved modes | |
| * - Support up to 8 substreams | |
| * - Support up to 4 channels | |
| * - Support framerates from 8 kHz to 48 kHz | |
| * | |
| * When driver works in the capture mode with multiple channels, it duplicates the looped | |
| * pattern to each separate channel. For example, if we have 2 channels, format = U8, interleaved | |
| * access mode and pattern 'abacaba', the DMA buffer will look like aabbccaabbaaaa..., so buffer for | |
| * each channel will contain abacabaabacaba... Same for the non-interleaved mode. | |
| * | |
| * However, it may break the capturing on the higher framerates with small period size, so it is | |
| * better to choose larger period sizes. | |
| * | |
| * You can find the corresponding selftest in the 'alsa' selftests folder. | |
| */ | |
| #include <linux/module.h> | |
| #include <linux/init.h> | |
| #include <sound/pcm.h> | |
| #include <sound/core.h> | |
| #include <linux/dma-mapping.h> | |
| #include <linux/platform_device.h> | |
| #include <linux/string.h> | |
| #include <linux/timer.h> | |
| #include <linux/random.h> | |
| #include <linux/debugfs.h> | |
| #include <linux/delay.h> | |
| #define TIMER_PER_SEC 5 | |
| #define TIMER_INTERVAL (HZ / TIMER_PER_SEC) | |
| #define DELAY_JIFFIES HZ | |
| #define PLAYBACK_SUBSTREAM_CNT 8 | |
| #define CAPTURE_SUBSTREAM_CNT 8 | |
| #define MAX_CHANNELS_NUM 4 | |
| #define DEFAULT_PATTERN "abacaba" | |
| #define DEFAULT_PATTERN_LEN 7 | |
| #define FILL_MODE_RAND 0 | |
| #define FILL_MODE_PAT 1 | |
| #define MAX_PATTERN_LEN 4096 | |
| static int index = -1; | |
| static char *id = "pcmtest"; | |
| static bool enable = true; | |
| static int inject_delay; | |
| static bool inject_hwpars_err; | |
| static bool inject_prepare_err; | |
| static bool inject_trigger_err; | |
| static bool inject_open_err; | |
| static short fill_mode = FILL_MODE_PAT; | |
| static u8 playback_capture_test; | |
| static u8 ioctl_reset_test; | |
| static struct dentry *driver_debug_dir; | |
| module_param(index, int, 0444); | |
| MODULE_PARM_DESC(index, "Index value for pcmtest soundcard"); | |
| module_param(id, charp, 0444); | |
| MODULE_PARM_DESC(id, "ID string for pcmtest soundcard"); | |
| module_param(enable, bool, 0444); | |
| MODULE_PARM_DESC(enable, "Enable pcmtest soundcard."); | |
| module_param(fill_mode, short, 0600); | |
| MODULE_PARM_DESC(fill_mode, "Buffer fill mode: rand(0) or pattern(1)"); | |
| module_param(inject_delay, int, 0600); | |
| MODULE_PARM_DESC(inject_delay, "Inject delays during playback/capture (in jiffies)"); | |
| module_param(inject_hwpars_err, bool, 0600); | |
| MODULE_PARM_DESC(inject_hwpars_err, "Inject EBUSY error in the 'hw_params' callback"); | |
| module_param(inject_prepare_err, bool, 0600); | |
| MODULE_PARM_DESC(inject_prepare_err, "Inject EINVAL error in the 'prepare' callback"); | |
| module_param(inject_trigger_err, bool, 0600); | |
| MODULE_PARM_DESC(inject_trigger_err, "Inject EINVAL error in the 'trigger' callback"); | |
| module_param(inject_open_err, bool, 0600); | |
| MODULE_PARM_DESC(inject_open_err, "Inject EBUSY error in the 'open' callback"); | |
| struct pcmtst { | |
| struct snd_pcm *pcm; | |
| struct snd_card *card; | |
| struct platform_device *pdev; | |
| }; | |
| struct pcmtst_buf_iter { | |
| size_t buf_pos; // position in the DMA buffer | |
| size_t period_pos; // period-relative position | |
| size_t b_rw; // Bytes to write on every timer tick | |
| size_t s_rw_ch; // Samples to write to one channel on every tick | |
| unsigned int sample_bytes; // sample_bits / 8 | |
| bool is_buf_corrupted; // playback test result indicator | |
| size_t period_bytes; // bytes in a one period | |
| bool interleaved; // Interleaved/Non-interleaved mode | |
| size_t total_bytes; // Total bytes read/written | |
| size_t chan_block; // Bytes in one channel buffer when non-interleaved | |
| struct snd_pcm_substream *substream; | |
| bool suspend; // We need to pause timer without shutting it down | |
| struct timer_list timer_instance; | |
| }; | |
| static struct snd_pcm_hardware snd_pcmtst_hw __ro_after_init = { | |
| .info = (SNDRV_PCM_INFO_INTERLEAVED | | |
| SNDRV_PCM_INFO_BLOCK_TRANSFER | | |
| SNDRV_PCM_INFO_NONINTERLEAVED | | |
| SNDRV_PCM_INFO_MMAP_VALID | | |
| SNDRV_PCM_INFO_PAUSE), | |
| .formats = SNDRV_PCM_FMTBIT_U8 | SNDRV_PCM_FMTBIT_S16_LE, | |
| .rates = SNDRV_PCM_RATE_8000_48000, | |
| .rate_min = 8000, | |
| .rate_max = 48000, | |
| .channels_min = 1, | |
| .channels_max = MAX_CHANNELS_NUM, | |
| .buffer_bytes_max = 128 * 1024, | |
| .period_bytes_min = 4096, | |
| .period_bytes_max = 32768, | |
| .periods_min = 1, | |
| .periods_max = 1024, | |
| }; | |
| struct pattern_buf { | |
| char *buf; | |
| u32 len; | |
| }; | |
| static int buf_allocated __ro_after_init; | |
| static struct pattern_buf patt_bufs[MAX_CHANNELS_NUM]; | |
| static inline void inc_buf_pos(struct pcmtst_buf_iter *v_iter, size_t by, size_t bytes) | |
| { | |
| v_iter->total_bytes += by; | |
| v_iter->buf_pos += by; | |
| if (v_iter->buf_pos >= bytes) | |
| v_iter->buf_pos %= bytes; | |
| } | |
| /* | |
| * Position in the DMA buffer when we are in the non-interleaved mode. We increment buf_pos | |
| * every time we write a byte to any channel, so the position in the current channel buffer is | |
| * (position in the DMA buffer) / count_of_channels + size_of_channel_buf * current_channel | |
| */ | |
| static inline size_t buf_pos_n(struct pcmtst_buf_iter *v_iter, unsigned int channels, | |
| unsigned int chan_num) | |
| { | |
| return v_iter->buf_pos / channels + v_iter->chan_block * chan_num; | |
| } | |
| /* | |
| * Get the count of bytes written for the current channel in the interleaved mode. | |
| * This is (count of samples written for the current channel) * bytes_in_sample + | |
| * (relative position in the current sample) | |
| */ | |
| static inline size_t ch_pos_i(size_t b_total, unsigned int channels, unsigned int b_sample) | |
| { | |
| return b_total / channels / b_sample * b_sample + (b_total % b_sample); | |
| } | |
| static void check_buf_block_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| size_t i; | |
| short ch_num; | |
| u8 current_byte; | |
| for (i = 0; i < v_iter->b_rw; i++) { | |
| current_byte = runtime->dma_area[v_iter->buf_pos]; | |
| if (!current_byte) | |
| break; | |
| ch_num = (v_iter->total_bytes / v_iter->sample_bytes) % runtime->channels; | |
| if (current_byte != patt_bufs[ch_num].buf[ch_pos_i(v_iter->total_bytes, | |
| runtime->channels, | |
| v_iter->sample_bytes) | |
| % patt_bufs[ch_num].len]) { | |
| v_iter->is_buf_corrupted = true; | |
| break; | |
| } | |
| inc_buf_pos(v_iter, 1, runtime->dma_bytes); | |
| } | |
| // If we broke during the loop, add remaining bytes to the buffer position. | |
| inc_buf_pos(v_iter, v_iter->b_rw - i, runtime->dma_bytes); | |
| } | |
| static void check_buf_block_ni(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| unsigned int channels = runtime->channels; | |
| size_t i; | |
| short ch_num; | |
| u8 current_byte; | |
| for (i = 0; i < v_iter->b_rw; i++) { | |
| ch_num = i % channels; | |
| current_byte = runtime->dma_area[buf_pos_n(v_iter, channels, ch_num)]; | |
| if (!current_byte) | |
| break; | |
| if (current_byte != patt_bufs[ch_num].buf[(v_iter->total_bytes / channels) | |
| % patt_bufs[ch_num].len]) { | |
| v_iter->is_buf_corrupted = true; | |
| break; | |
| } | |
| inc_buf_pos(v_iter, 1, runtime->dma_bytes); | |
| } | |
| inc_buf_pos(v_iter, v_iter->b_rw - i, runtime->dma_bytes); | |
| } | |
| /* | |
| * Check one block of the buffer. Here we iterate the buffer until we find '0'. This condition is | |
| * necessary because we need to detect when the reading/writing ends, so we assume that the pattern | |
| * doesn't contain zeros. | |
| */ | |
| static void check_buf_block(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| if (v_iter->interleaved) | |
| check_buf_block_i(v_iter, runtime); | |
| else | |
| check_buf_block_ni(v_iter, runtime); | |
| } | |
| /* | |
| * Fill buffer in the non-interleaved mode. The order of samples is C0, ..., C0, C1, ..., C1, C2... | |
| * The channel buffers lay in the DMA buffer continuously (see default copy | |
| * handlers in the pcm_lib.c file). | |
| * | |
| * Here we increment the DMA buffer position every time we write a byte to any channel 'buffer'. | |
| * We need this to simulate the correct hardware pointer moving. | |
| */ | |
| static void fill_block_pattern_n(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| size_t i; | |
| unsigned int channels = runtime->channels; | |
| short ch_num; | |
| for (i = 0; i < v_iter->b_rw; i++) { | |
| ch_num = i % channels; | |
| runtime->dma_area[buf_pos_n(v_iter, channels, ch_num)] = | |
| patt_bufs[ch_num].buf[(v_iter->total_bytes / channels) | |
| % patt_bufs[ch_num].len]; | |
| inc_buf_pos(v_iter, 1, runtime->dma_bytes); | |
| } | |
| } | |
| // Fill buffer in the interleaved mode. The order of samples is C0, C1, C2, C0, C1, C2, ... | |
| static void fill_block_pattern_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| size_t sample; | |
| size_t pos_in_ch, pos_pattern; | |
| short ch, pos_sample; | |
| pos_in_ch = ch_pos_i(v_iter->total_bytes, runtime->channels, v_iter->sample_bytes); | |
| for (sample = 0; sample < v_iter->s_rw_ch; sample++) { | |
| for (ch = 0; ch < runtime->channels; ch++) { | |
| for (pos_sample = 0; pos_sample < v_iter->sample_bytes; pos_sample++) { | |
| pos_pattern = (pos_in_ch + sample * v_iter->sample_bytes | |
| + pos_sample) % patt_bufs[ch].len; | |
| runtime->dma_area[v_iter->buf_pos] = patt_bufs[ch].buf[pos_pattern]; | |
| inc_buf_pos(v_iter, 1, runtime->dma_bytes); | |
| } | |
| } | |
| } | |
| } | |
| static void fill_block_pattern(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| if (v_iter->interleaved) | |
| fill_block_pattern_i(v_iter, runtime); | |
| else | |
| fill_block_pattern_n(v_iter, runtime); | |
| } | |
| static void fill_block_rand_n(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| unsigned int channels = runtime->channels; | |
| // Remaining space in all channel buffers | |
| size_t bytes_remain = runtime->dma_bytes - v_iter->buf_pos; | |
| unsigned int i; | |
| for (i = 0; i < channels; i++) { | |
| if (v_iter->b_rw <= bytes_remain) { | |
| //b_rw - count of bytes must be written for all channels at each timer tick | |
| get_random_bytes(runtime->dma_area + buf_pos_n(v_iter, channels, i), | |
| v_iter->b_rw / channels); | |
| } else { | |
| // Write to the end of buffer and start from the beginning of it | |
| get_random_bytes(runtime->dma_area + buf_pos_n(v_iter, channels, i), | |
| bytes_remain / channels); | |
| get_random_bytes(runtime->dma_area + v_iter->chan_block * i, | |
| (v_iter->b_rw - bytes_remain) / channels); | |
| } | |
| } | |
| inc_buf_pos(v_iter, v_iter->b_rw, runtime->dma_bytes); | |
| } | |
| static void fill_block_rand_i(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| size_t in_cur_block = runtime->dma_bytes - v_iter->buf_pos; | |
| if (v_iter->b_rw <= in_cur_block) { | |
| get_random_bytes(&runtime->dma_area[v_iter->buf_pos], v_iter->b_rw); | |
| } else { | |
| get_random_bytes(&runtime->dma_area[v_iter->buf_pos], in_cur_block); | |
| get_random_bytes(runtime->dma_area, v_iter->b_rw - in_cur_block); | |
| } | |
| inc_buf_pos(v_iter, v_iter->b_rw, runtime->dma_bytes); | |
| } | |
| static void fill_block_random(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| if (v_iter->interleaved) | |
| fill_block_rand_i(v_iter, runtime); | |
| else | |
| fill_block_rand_n(v_iter, runtime); | |
| } | |
| static void fill_block(struct pcmtst_buf_iter *v_iter, struct snd_pcm_runtime *runtime) | |
| { | |
| switch (fill_mode) { | |
| case FILL_MODE_RAND: | |
| fill_block_random(v_iter, runtime); | |
| break; | |
| case FILL_MODE_PAT: | |
| fill_block_pattern(v_iter, runtime); | |
| break; | |
| } | |
| } | |
| /* | |
| * Here we iterate through the buffer by (buffer_size / iterates_per_second) bytes. | |
| * The driver uses timer to simulate the hardware pointer moving, and notify the PCM middle layer | |
| * about period elapsed. | |
| */ | |
| static void timer_timeout(struct timer_list *data) | |
| { | |
| struct pcmtst_buf_iter *v_iter; | |
| struct snd_pcm_substream *substream; | |
| v_iter = timer_container_of(v_iter, data, timer_instance); | |
| substream = v_iter->substream; | |
| if (v_iter->suspend) | |
| return; | |
| if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK && !v_iter->is_buf_corrupted) | |
| check_buf_block(v_iter, substream->runtime); | |
| else if (substream->stream == SNDRV_PCM_STREAM_CAPTURE) | |
| fill_block(v_iter, substream->runtime); | |
| else | |
| inc_buf_pos(v_iter, v_iter->b_rw, substream->runtime->dma_bytes); | |
| v_iter->period_pos += v_iter->b_rw; | |
| if (v_iter->period_pos >= v_iter->period_bytes) { | |
| v_iter->period_pos %= v_iter->period_bytes; | |
| snd_pcm_period_elapsed(substream); | |
| } | |
| if (!v_iter->suspend) | |
| mod_timer(&v_iter->timer_instance, jiffies + TIMER_INTERVAL + inject_delay); | |
| } | |
| static int snd_pcmtst_pcm_open(struct snd_pcm_substream *substream) | |
| { | |
| struct snd_pcm_runtime *runtime = substream->runtime; | |
| struct pcmtst_buf_iter *v_iter; | |
| if (inject_open_err) | |
| return -EBUSY; | |
| v_iter = kzalloc_obj(*v_iter); | |
| if (!v_iter) | |
| return -ENOMEM; | |
| v_iter->substream = substream; | |
| runtime->hw = snd_pcmtst_hw; | |
| runtime->private_data = v_iter; | |
| playback_capture_test = 0; | |
| ioctl_reset_test = 0; | |
| timer_setup(&v_iter->timer_instance, timer_timeout, 0); | |
| return 0; | |
| } | |
| static int snd_pcmtst_pcm_close(struct snd_pcm_substream *substream) | |
| { | |
| struct pcmtst_buf_iter *v_iter = substream->runtime->private_data; | |
| timer_shutdown_sync(&v_iter->timer_instance); | |
| playback_capture_test = !v_iter->is_buf_corrupted; | |
| kfree(v_iter); | |
| return 0; | |
| } | |
| static inline void reset_buf_iterator(struct pcmtst_buf_iter *v_iter) | |
| { | |
| v_iter->buf_pos = 0; | |
| v_iter->is_buf_corrupted = false; | |
| v_iter->period_pos = 0; | |
| v_iter->total_bytes = 0; | |
| } | |
| static inline void start_pcmtest_timer(struct pcmtst_buf_iter *v_iter) | |
| { | |
| v_iter->suspend = false; | |
| mod_timer(&v_iter->timer_instance, jiffies + TIMER_INTERVAL); | |
| } | |
| static int snd_pcmtst_pcm_trigger(struct snd_pcm_substream *substream, int cmd) | |
| { | |
| struct pcmtst_buf_iter *v_iter = substream->runtime->private_data; | |
| if (inject_trigger_err) | |
| return -EINVAL; | |
| switch (cmd) { | |
| case SNDRV_PCM_TRIGGER_START: | |
| reset_buf_iterator(v_iter); | |
| start_pcmtest_timer(v_iter); | |
| break; | |
| case SNDRV_PCM_TRIGGER_PAUSE_RELEASE: | |
| start_pcmtest_timer(v_iter); | |
| break; | |
| case SNDRV_PCM_TRIGGER_STOP: | |
| case SNDRV_PCM_TRIGGER_PAUSE_PUSH: | |
| // We can't call timer_shutdown_sync here, as it is forbidden to sleep here | |
| v_iter->suspend = true; | |
| timer_delete(&v_iter->timer_instance); | |
| break; | |
| } | |
| return 0; | |
| } | |
| static snd_pcm_uframes_t snd_pcmtst_pcm_pointer(struct snd_pcm_substream *substream) | |
| { | |
| struct pcmtst_buf_iter *v_iter = substream->runtime->private_data; | |
| return bytes_to_frames(substream->runtime, v_iter->buf_pos); | |
| } | |
| static int snd_pcmtst_free(struct pcmtst *pcmtst) | |
| { | |
| if (!pcmtst) | |
| return 0; | |
| kfree(pcmtst); | |
| return 0; | |
| } | |
| // These callbacks are required, but empty - all freeing occurs in pdev_remove | |
| static int snd_pcmtst_dev_free(struct snd_device *device) | |
| { | |
| return 0; | |
| } | |
| static void pcmtst_pdev_release(struct device *dev) | |
| { | |
| } | |
| static int snd_pcmtst_pcm_prepare(struct snd_pcm_substream *substream) | |
| { | |
| struct snd_pcm_runtime *runtime = substream->runtime; | |
| struct pcmtst_buf_iter *v_iter = runtime->private_data; | |
| if (inject_prepare_err) | |
| return -EINVAL; | |
| v_iter->sample_bytes = samples_to_bytes(runtime, 1); | |
| v_iter->period_bytes = snd_pcm_lib_period_bytes(substream); | |
| v_iter->interleaved = true; | |
| if (runtime->access == SNDRV_PCM_ACCESS_RW_NONINTERLEAVED || | |
| runtime->access == SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED) { | |
| v_iter->chan_block = snd_pcm_lib_buffer_bytes(substream) / runtime->channels; | |
| v_iter->interleaved = false; | |
| } | |
| // We want to record RATE * ch_cnt samples per sec, it is rate * sample_bytes * ch_cnt bytes | |
| v_iter->s_rw_ch = runtime->rate / TIMER_PER_SEC; | |
| v_iter->b_rw = v_iter->s_rw_ch * v_iter->sample_bytes * runtime->channels; | |
| return 0; | |
| } | |
| static int snd_pcmtst_pcm_hw_params(struct snd_pcm_substream *substream, | |
| struct snd_pcm_hw_params *params) | |
| { | |
| if (inject_hwpars_err) | |
| return -EBUSY; | |
| return 0; | |
| } | |
| static int snd_pcmtst_pcm_hw_free(struct snd_pcm_substream *substream) | |
| { | |
| return 0; | |
| } | |
| static int snd_pcmtst_ioctl(struct snd_pcm_substream *substream, unsigned int cmd, void *arg) | |
| { | |
| switch (cmd) { | |
| case SNDRV_PCM_IOCTL1_RESET: | |
| ioctl_reset_test = 1; | |
| break; | |
| } | |
| return snd_pcm_lib_ioctl(substream, cmd, arg); | |
| } | |
| static int snd_pcmtst_sync_stop(struct snd_pcm_substream *substream) | |
| { | |
| struct pcmtst_buf_iter *v_iter = substream->runtime->private_data; | |
| timer_delete_sync(&v_iter->timer_instance); | |
| return 0; | |
| } | |
| static const struct snd_pcm_ops snd_pcmtst_playback_ops = { | |
| .open = snd_pcmtst_pcm_open, | |
| .close = snd_pcmtst_pcm_close, | |
| .trigger = snd_pcmtst_pcm_trigger, | |
| .hw_params = snd_pcmtst_pcm_hw_params, | |
| .ioctl = snd_pcmtst_ioctl, | |
| .sync_stop = snd_pcmtst_sync_stop, | |
| .hw_free = snd_pcmtst_pcm_hw_free, | |
| .prepare = snd_pcmtst_pcm_prepare, | |
| .pointer = snd_pcmtst_pcm_pointer, | |
| }; | |
| static const struct snd_pcm_ops snd_pcmtst_capture_ops = { | |
| .open = snd_pcmtst_pcm_open, | |
| .close = snd_pcmtst_pcm_close, | |
| .trigger = snd_pcmtst_pcm_trigger, | |
| .hw_params = snd_pcmtst_pcm_hw_params, | |
| .hw_free = snd_pcmtst_pcm_hw_free, | |
| .ioctl = snd_pcmtst_ioctl, | |
| .sync_stop = snd_pcmtst_sync_stop, | |
| .prepare = snd_pcmtst_pcm_prepare, | |
| .pointer = snd_pcmtst_pcm_pointer, | |
| }; | |
| static int snd_pcmtst_new_pcm(struct pcmtst *pcmtst) | |
| { | |
| struct snd_pcm *pcm; | |
| int err; | |
| err = snd_pcm_new(pcmtst->card, "PCMTest", 0, PLAYBACK_SUBSTREAM_CNT, | |
| CAPTURE_SUBSTREAM_CNT, &pcm); | |
| if (err < 0) | |
| return err; | |
| pcm->private_data = pcmtst; | |
| strscpy(pcm->name, "PCMTest"); | |
| pcmtst->pcm = pcm; | |
| snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &snd_pcmtst_playback_ops); | |
| snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &snd_pcmtst_capture_ops); | |
| err = snd_pcm_set_managed_buffer_all(pcm, SNDRV_DMA_TYPE_DEV, &pcmtst->pdev->dev, | |
| 0, 128 * 1024); | |
| return err; | |
| } | |
| static int snd_pcmtst_create(struct snd_card *card, struct platform_device *pdev, | |
| struct pcmtst **r_pcmtst) | |
| { | |
| struct pcmtst *pcmtst; | |
| int err; | |
| static const struct snd_device_ops ops = { | |
| .dev_free = snd_pcmtst_dev_free, | |
| }; | |
| pcmtst = kzalloc_obj(*pcmtst); | |
| if (!pcmtst) | |
| return -ENOMEM; | |
| pcmtst->card = card; | |
| pcmtst->pdev = pdev; | |
| err = snd_device_new(card, SNDRV_DEV_LOWLEVEL, pcmtst, &ops); | |
| if (err < 0) | |
| goto _err_free_chip; | |
| err = snd_pcmtst_new_pcm(pcmtst); | |
| if (err < 0) | |
| goto _err_free_chip; | |
| *r_pcmtst = pcmtst; | |
| return 0; | |
| _err_free_chip: | |
| snd_pcmtst_free(pcmtst); | |
| return err; | |
| } | |
| static int pcmtst_probe(struct platform_device *pdev) | |
| { | |
| struct snd_card *card; | |
| struct pcmtst *pcmtst; | |
| int err; | |
| err = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); | |
| if (err) | |
| return err; | |
| err = snd_devm_card_new(&pdev->dev, index, id, THIS_MODULE, 0, &card); | |
| if (err < 0) | |
| return err; | |
| err = snd_pcmtst_create(card, pdev, &pcmtst); | |
| if (err < 0) | |
| return err; | |
| strscpy(card->driver, "PCM-TEST Driver"); | |
| strscpy(card->shortname, "PCM-Test"); | |
| strscpy(card->longname, "PCM-Test virtual driver"); | |
| err = snd_card_register(card); | |
| if (err < 0) | |
| return err; | |
| platform_set_drvdata(pdev, pcmtst); | |
| return 0; | |
| } | |
| static void pdev_remove(struct platform_device *pdev) | |
| { | |
| struct pcmtst *pcmtst = platform_get_drvdata(pdev); | |
| snd_pcmtst_free(pcmtst); | |
| } | |
| static struct platform_device pcmtst_pdev = { | |
| .name = "pcmtest", | |
| .dev.release = pcmtst_pdev_release, | |
| }; | |
| static struct platform_driver pcmtst_pdrv = { | |
| .probe = pcmtst_probe, | |
| .remove = pdev_remove, | |
| .driver = { | |
| .name = "pcmtest", | |
| }, | |
| }; | |
| static ssize_t pattern_write(struct file *file, const char __user *u_buff, size_t len, loff_t *off) | |
| { | |
| struct pattern_buf *patt_buf = file->f_inode->i_private; | |
| ssize_t to_write = len; | |
| if (*off + to_write > MAX_PATTERN_LEN) | |
| to_write = MAX_PATTERN_LEN - *off; | |
| // Crop silently everything over the buffer | |
| if (to_write <= 0) | |
| return len; | |
| if (copy_from_user(patt_buf->buf + *off, u_buff, to_write)) | |
| return -EFAULT; | |
| patt_buf->len = *off + to_write; | |
| *off += to_write; | |
| return to_write; | |
| } | |
| static ssize_t pattern_read(struct file *file, char __user *u_buff, size_t len, loff_t *off) | |
| { | |
| struct pattern_buf *patt_buf = file->f_inode->i_private; | |
| ssize_t to_read = len; | |
| if (*off + to_read >= MAX_PATTERN_LEN) | |
| to_read = MAX_PATTERN_LEN - *off; | |
| if (to_read <= 0) | |
| return 0; | |
| if (copy_to_user(u_buff, patt_buf->buf + *off, to_read)) | |
| return -EFAULT; | |
| *off += to_read; | |
| return to_read; | |
| } | |
| static const struct file_operations fill_pattern_fops = { | |
| .read = pattern_read, | |
| .write = pattern_write, | |
| }; | |
| static int setup_patt_bufs(void) | |
| { | |
| size_t i; | |
| for (i = 0; i < ARRAY_SIZE(patt_bufs); i++) { | |
| patt_bufs[i].buf = kmalloc(MAX_PATTERN_LEN, GFP_KERNEL); | |
| if (!patt_bufs[i].buf) | |
| break; | |
| strscpy_pad(patt_bufs[i].buf, DEFAULT_PATTERN, MAX_PATTERN_LEN); | |
| patt_bufs[i].len = DEFAULT_PATTERN_LEN; | |
| } | |
| return i; | |
| } | |
| static const char * const pattern_files[] = { "fill_pattern0", "fill_pattern1", | |
| "fill_pattern2", "fill_pattern3"}; | |
| static int init_debug_files(int buf_count) | |
| { | |
| size_t i; | |
| char len_file_name[32]; | |
| driver_debug_dir = debugfs_create_dir("pcmtest", NULL); | |
| if (IS_ERR(driver_debug_dir)) | |
| return PTR_ERR(driver_debug_dir); | |
| debugfs_create_u8("pc_test", 0444, driver_debug_dir, &playback_capture_test); | |
| debugfs_create_u8("ioctl_test", 0444, driver_debug_dir, &ioctl_reset_test); | |
| for (i = 0; i < buf_count; i++) { | |
| debugfs_create_file(pattern_files[i], 0600, driver_debug_dir, | |
| &patt_bufs[i], &fill_pattern_fops); | |
| snprintf(len_file_name, sizeof(len_file_name), "%s_len", pattern_files[i]); | |
| debugfs_create_u32(len_file_name, 0444, driver_debug_dir, &patt_bufs[i].len); | |
| } | |
| return 0; | |
| } | |
| static void free_pattern_buffers(void) | |
| { | |
| int i; | |
| for (i = 0; i < buf_allocated; i++) | |
| kfree(patt_bufs[i].buf); | |
| } | |
| static void clear_debug_files(void) | |
| { | |
| debugfs_remove_recursive(driver_debug_dir); | |
| } | |
| static int __init mod_init(void) | |
| { | |
| int err = 0; | |
| buf_allocated = setup_patt_bufs(); | |
| if (!buf_allocated) | |
| return -ENOMEM; | |
| snd_pcmtst_hw.channels_max = buf_allocated; | |
| err = init_debug_files(buf_allocated); | |
| if (err) | |
| goto err_free_patterns; | |
| err = platform_device_register(&pcmtst_pdev); | |
| if (err) | |
| goto err_clear_debug; | |
| err = platform_driver_register(&pcmtst_pdrv); | |
| if (err) { | |
| platform_device_unregister(&pcmtst_pdev); | |
| goto err_clear_debug; | |
| } | |
| return 0; | |
| err_clear_debug: | |
| clear_debug_files(); | |
| err_free_patterns: | |
| free_pattern_buffers(); | |
| return err; | |
| } | |
| static void __exit mod_exit(void) | |
| { | |
| clear_debug_files(); | |
| free_pattern_buffers(); | |
| platform_driver_unregister(&pcmtst_pdrv); | |
| platform_device_unregister(&pcmtst_pdev); | |
| } | |
| MODULE_DESCRIPTION("Virtual ALSA driver for PCM testing/fuzzing"); | |
| MODULE_LICENSE("GPL"); | |
| MODULE_AUTHOR("Ivan Orlov"); | |
| module_init(mod_init); | |
| module_exit(mod_exit); |